-
Notifications
You must be signed in to change notification settings - Fork 749
Add Aspire.Hosting.Certbot integration for Certbot container support #13248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
17
commits into
main
Choose a base branch
from
copilot/add-lets-encrypt-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+646
−0
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
4611271
Initial plan
Copilot 41ca003
Add Aspire.Hosting.LetsEncrypt integration with Certbot container sup…
Copilot ac3b0f7
Pin certbot container image to stable version v5.1.0
Copilot 3d6ccad
Add CertificatePath and PrivateKeyPath properties to CertbotResource
Copilot 87bb3cd
Rename Aspire.Hosting.LetsEncrypt to Aspire.Hosting.Certbot and WithC…
Copilot 67cbe2d
Refactor Certbot API: make HTTP endpoint opt-in, split args into With…
Copilot 793c0ae
Refactor args composition: centralize in AddCertbot WithArgs callback…
Copilot ad1b0c7
Add WaitForCompletion to usage examples for resources that depend on …
Copilot 1a7c32d
Add WithCertbotCertificate method that combines volume mount and Wait…
Copilot a5aac1d
Update method name from WithServerAuthenticationCertificateConfigurat…
Copilot b2f5493
Merge remote-tracking branch 'origin/main' into copilot/add-lets-encr…
Copilot e6bba87
Update documentation to reference correct HTTPS certificate APIs and …
Copilot 2b408c0
Remove WithCertificateVolume method and related tests
Copilot 9cd0d37
Update src/Aspire.Hosting.Certbot/Aspire.Hosting.Certbot.csproj
davidfowl e7b75bc
Simplify README: remove verbose usage explanation and Connection Prop…
Copilot ed7ec9e
Simplify README further: remove Configuration section and focus on mi…
Copilot 44d566d
Fix markdown linting: remove multiple consecutive blank lines
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>$(DefaultTargetFramework)</TargetFramework> | ||
| <IsPackable>true</IsPackable> | ||
| <SuppressFinalPackageVersion>true</SuppressFinalPackageVersion> | ||
| <PackageTags>aspire integration hosting certbot letsencrypt ssl tls https acme</PackageTags> | ||
| <Description>Certbot support for Aspire.</Description> | ||
| <!-- Disable package validation for new packages without a baseline version --> | ||
| <EnablePackageValidation>false</EnablePackageValidation> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <InternalsVisibleTo Include="Aspire.Hosting.Certbot.Tests"/> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for adding Certbot resources to the application model. | ||
| /// </summary> | ||
| public static class CertbotBuilderExtensions | ||
| { | ||
| /// <summary> | ||
| /// Adds a Certbot container to the application model. | ||
| /// </summary> | ||
| /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="domain">The parameter containing the domain name to obtain a certificate for.</param> | ||
| /// <param name="email">The parameter containing the email address for certificate registration.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method adds a Certbot container that obtains SSL/TLS certificates using the ACME protocol. | ||
| /// By default, no challenge method is configured. Use <see cref="WithHttp01Challenge"/> or other challenge methods to configure how certificates are obtained. | ||
| /// </para> | ||
| /// <para> | ||
| /// The certificates are stored in a shared volume named "letsencrypt" at /etc/letsencrypt. | ||
| /// Other resources can mount this volume to access the certificates. | ||
| /// </para> | ||
| /// <para> | ||
| /// Certificate permissions are automatically set to allow non-root containers to read them. | ||
| /// </para> | ||
| /// This version of the package defaults to the <inheritdoc cref="CertbotContainerImageTags.Tag"/> tag of the <inheritdoc cref="CertbotContainerImageTags.Image"/> container image. | ||
davidfowl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// <example> | ||
| /// Use in application host: | ||
| /// <code lang="csharp"> | ||
| /// var domain = builder.AddParameter("domain"); | ||
| /// var email = builder.AddParameter("email"); | ||
| /// | ||
| /// var certbot = builder.AddCertbot("certbot", domain, email) | ||
| /// .WithHttp01Challenge(); | ||
| /// | ||
| /// var myService = builder.AddContainer("myservice", "myimage") | ||
| /// .WithCertbotCertificate(certbot); | ||
| /// </code> | ||
| /// </example> | ||
| /// </remarks> | ||
| public static IResourceBuilder<CertbotResource> AddCertbot( | ||
| this IDistributedApplicationBuilder builder, | ||
| [ResourceName] string name, | ||
| IResourceBuilder<ParameterResource> domain, | ||
| IResourceBuilder<ParameterResource> email) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(name); | ||
| ArgumentNullException.ThrowIfNull(domain); | ||
| ArgumentNullException.ThrowIfNull(email); | ||
|
|
||
| var resource = new CertbotResource(name, domain.Resource, email.Resource); | ||
|
|
||
| return builder.AddResource(resource) | ||
| .WithImage(CertbotContainerImageTags.Image, CertbotContainerImageTags.Tag) | ||
| .WithImageRegistry(CertbotContainerImageTags.Registry) | ||
| .WithVolume(CertbotResource.CertificatesVolumeName, CertbotResource.CertificatesPath) | ||
| .WithArgs(context => | ||
| { | ||
| var certbotResource = (CertbotResource)context.Resource; | ||
|
|
||
| // Only add args if a challenge method is configured | ||
| if (certbotResource.ChallengeMethod is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Common arguments for all challenge methods | ||
| context.Args.Add("certonly"); | ||
| context.Args.Add("--non-interactive"); | ||
| context.Args.Add("--agree-tos"); | ||
| context.Args.Add("-v"); | ||
| context.Args.Add("--keep-until-expiring"); | ||
|
|
||
| // Challenge-specific arguments | ||
| switch (certbotResource.ChallengeMethod) | ||
| { | ||
| case CertbotChallengeMethod.Http01: | ||
| context.Args.Add("--standalone"); | ||
| break; | ||
| } | ||
|
|
||
| // Always set permissions to allow non-root containers to read certificates | ||
| context.Args.Add("--deploy-hook"); | ||
| context.Args.Add("chmod -R 755 /etc/letsencrypt/live && chmod -R 755 /etc/letsencrypt/archive"); | ||
|
|
||
| // Email and domain arguments | ||
| context.Args.Add("--email"); | ||
| context.Args.Add(certbotResource.EmailParameter); | ||
| context.Args.Add("-d"); | ||
| context.Args.Add(certbotResource.DomainParameter); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures Certbot to use the HTTP-01 challenge for domain validation. | ||
| /// </summary> | ||
| /// <param name="builder">The Certbot resource builder.</param> | ||
| /// <param name="port">The host port to publish for the HTTP-01 challenge. Defaults to 80.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// The HTTP-01 challenge requires Certbot to be accessible on port 80 from the internet. | ||
| /// This method configures the container to listen on the specified port and sets up | ||
| /// the standalone mode for ACME challenge validation. | ||
| /// </para> | ||
| /// <example> | ||
| /// <code lang="csharp"> | ||
| /// var certbot = builder.AddCertbot("certbot", domain, email) | ||
| /// .WithHttp01Challenge(port: 8080); | ||
| /// </code> | ||
| /// </example> | ||
| /// </remarks> | ||
| public static IResourceBuilder<CertbotResource> WithHttp01Challenge( | ||
| this IResourceBuilder<CertbotResource> builder, | ||
| int? port = 80) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| builder.Resource.ChallengeMethod = CertbotChallengeMethod.Http01; | ||
|
|
||
| return builder | ||
| .WithHttpEndpoint(port: port, targetPort: 80, name: CertbotResource.HttpEndpointName) | ||
| .WithExternalHttpEndpoints(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures the container to use SSL/TLS certificates from a Certbot resource. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the container resource.</typeparam> | ||
| /// <param name="builder">The resource builder for the container resource that needs access to the certificates.</param> | ||
| /// <param name="certbot">The Certbot resource builder.</param> | ||
| /// <param name="mountPath">The path where the certificates volume should be mounted. Defaults to /etc/letsencrypt.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method mounts the certificates volume and ensures the container waits for the Certbot | ||
| /// resource to complete certificate acquisition before starting. | ||
| /// </para> | ||
| /// <para> | ||
| /// <strong>Note:</strong> Do not use this method together with <c>WithHttpsCertificate</c> | ||
| /// or <c>WithHttpsCertificateConfiguration</c> | ||
| /// at runtime, as they will conflict. However, you can use Certbot in publish mode while using the other methods in development mode | ||
| /// by wrapping the Certbot configuration in an <c>ExecutionContext.IsPublishMode</c> check. | ||
| /// </para> | ||
| /// <example> | ||
| /// <code lang="csharp"> | ||
| /// var domain = builder.AddParameter("domain"); | ||
| /// var email = builder.AddParameter("email"); | ||
| /// | ||
| /// var certbot = builder.AddCertbot("certbot", domain, email) | ||
| /// .WithHttp01Challenge(); | ||
| /// | ||
| /// var yarp = builder.AddContainer("yarp", "myimage") | ||
| /// .WithCertbotCertificate(certbot); | ||
| /// </code> | ||
| /// </example> | ||
| /// </remarks> | ||
| public static IResourceBuilder<T> WithCertbotCertificate<T>( | ||
| this IResourceBuilder<T> builder, | ||
| IResourceBuilder<CertbotResource> certbot, | ||
| string mountPath = CertbotResource.CertificatesPath) where T : ContainerResource, IResourceWithWaitSupport | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(certbot); | ||
|
|
||
| return builder | ||
| .WithVolume(CertbotResource.CertificatesVolumeName, mountPath) | ||
| .WaitForCompletion(certbot); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Aspire.Hosting; | ||
|
|
||
| internal static class CertbotContainerImageTags | ||
| { | ||
| /// <remarks>docker.io</remarks> | ||
| public const string Registry = "docker.io"; | ||
|
|
||
| /// <remarks>certbot/certbot</remarks> | ||
| public const string Image = "certbot/certbot"; | ||
|
|
||
| /// <remarks>v5.1.0</remarks> | ||
| public const string Tag = "v5.1.0"; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Aspire.Hosting.ApplicationModel; | ||
|
|
||
| /// <summary> | ||
| /// Represents a Certbot container resource for obtaining and renewing SSL/TLS certificates. | ||
| /// </summary> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="domain">A parameter containing the domain name to obtain a certificate for.</param> | ||
| /// <param name="email">A parameter containing the email address for certificate registration and notifications.</param> | ||
| public class CertbotResource(string name, ParameterResource domain, ParameterResource email) : ContainerResource(name) | ||
| { | ||
| internal const string HttpEndpointName = "http"; | ||
| internal const string CertificatesVolumeName = "letsencrypt"; | ||
| internal const string CertificatesPath = "/etc/letsencrypt"; | ||
|
|
||
| private EndpointReference? _httpEndpoint; | ||
|
|
||
| /// <summary> | ||
| /// Gets the HTTP endpoint for the Certbot ACME challenge server. | ||
| /// </summary> | ||
| public EndpointReference HttpEndpoint => _httpEndpoint ??= new(this, HttpEndpointName); | ||
|
|
||
| /// <summary> | ||
| /// Gets the parameter that contains the domain name. | ||
| /// </summary> | ||
| public ParameterResource DomainParameter { get; } = domain ?? throw new ArgumentNullException(nameof(domain)); | ||
|
|
||
| /// <summary> | ||
| /// Gets the parameter that contains the email address for certificate registration. | ||
| /// </summary> | ||
| public ParameterResource EmailParameter { get; } = email ?? throw new ArgumentNullException(nameof(email)); | ||
davidfowl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// <summary> | ||
| /// Gets or sets the challenge method to use for domain validation. | ||
| /// </summary> | ||
| internal CertbotChallengeMethod? ChallengeMethod { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets an expression representing the path to the SSL/TLS certificate (fullchain.pem) for the domain. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The certificate path follows the standard convention: <c>/etc/letsencrypt/live/{domain}/fullchain.pem</c>. | ||
| /// This property returns a <see cref="ReferenceExpression"/> that resolves to the actual path at runtime | ||
| /// based on the domain parameter value. | ||
| /// </remarks> | ||
| public ReferenceExpression CertificatePath => | ||
| ReferenceExpression.Create($"{CertificatesPath}/live/{DomainParameter}/fullchain.pem"); | ||
|
|
||
| /// <summary> | ||
| /// Gets an expression representing the path to the private key (privkey.pem) for the domain. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The private key path follows the standard convention: <c>/etc/letsencrypt/live/{domain}/privkey.pem</c>. | ||
| /// This property returns a <see cref="ReferenceExpression"/> that resolves to the actual path at runtime | ||
| /// based on the domain parameter value. | ||
| /// </remarks> | ||
| public ReferenceExpression PrivateKeyPath => | ||
| ReferenceExpression.Create($"{CertificatesPath}/live/{DomainParameter}/privkey.pem"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Represents the ACME challenge method to use for domain validation. | ||
| /// </summary> | ||
| internal enum CertbotChallengeMethod | ||
| { | ||
| /// <summary> | ||
| /// HTTP-01 challenge using standalone mode. | ||
| /// </summary> | ||
| Http01 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Aspire.Hosting.Certbot library | ||
|
|
||
| Provides extension methods and resource definitions for an Aspire AppHost to configure a Certbot resource. | ||
|
|
||
| ## Getting started | ||
|
|
||
| ### Install the package | ||
|
|
||
| In your AppHost project, install the Aspire Certbot Hosting library with [NuGet](https://www.nuget.org): | ||
|
|
||
| ```dotnetcli | ||
| dotnet add package Aspire.Hosting.Certbot | ||
| ``` | ||
|
|
||
| ## Usage example | ||
|
|
||
| Then, in the _AppHost.cs_ file of `AppHost`, add a Certbot resource and consume the certificates: | ||
|
|
||
| ```csharp | ||
| var domain = builder.AddParameter("domain"); | ||
| var email = builder.AddParameter("email"); | ||
|
|
||
| var certbot = builder.AddCertbot("certbot", domain, email) | ||
| .WithHttp01Challenge(); | ||
|
|
||
| var myService = builder.AddContainer("myservice", "myimage") | ||
| .WithCertbotCertificate(certbot); | ||
| ``` | ||
|
|
||
| ## Certificate Locations | ||
|
|
||
| Certificates obtained by Certbot are stored at: | ||
|
|
||
| | Path | Description | | ||
| |------|-------------| | ||
| | `/etc/letsencrypt/live/{domain}/fullchain.pem` | The full certificate chain | | ||
| | `/etc/letsencrypt/live/{domain}/privkey.pem` | The private key | | ||
|
|
||
| These paths are accessible via the `CertbotResource.CertificatePath` and `CertbotResource.PrivateKeyPath` properties. | ||
|
|
||
| ## Additional documentation | ||
|
|
||
| * https://certbot.eff.org/docs/ | ||
| * https://letsencrypt.org/docs/ | ||
|
|
||
| ## Feedback & contributing | ||
|
|
||
| https://github.com/dotnet/aspire |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.