-
Notifications
You must be signed in to change notification settings - Fork 745
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
Draft
Copilot
wants to merge
5
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.
Draft
Changes from 4 commits
Commits
Show all changes
5 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 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
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
20 changes: 20 additions & 0 deletions
20
src/Aspire.Hosting.LetsEncrypt/Aspire.Hosting.LetsEncrypt.csproj
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,20 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>$(DefaultTargetFramework)</TargetFramework> | ||
| <IsPackable>true</IsPackable> | ||
| <PackageTags>aspire integration hosting letsencrypt certbot ssl tls https</PackageTags> | ||
| <Description>Let's Encrypt 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.LetsEncrypt.Tests"/> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
16 changes: 16 additions & 0 deletions
16
src/Aspire.Hosting.LetsEncrypt/CertbotContainerImageTags.cs
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,56 @@ | ||
| // 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 Let's Encrypt 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 Let's Encrypt 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 Let's Encrypt registration. | ||
| /// </summary> | ||
| public ParameterResource EmailParameter { get; } = email ?? throw new ArgumentNullException(nameof(email)); | ||
|
|
||
| /// <summary> | ||
| /// Gets an expression representing the path to the SSL/TLS certificate (fullchain.pem) for the domain. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The certificate path follows the Let's Encrypt 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 Let's Encrypt 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"); | ||
| } |
116 changes: 116 additions & 0 deletions
116
src/Aspire.Hosting.LetsEncrypt/LetsEncryptBuilderExtensions.cs
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,116 @@ | ||
| // 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 Let's Encrypt Certbot resources to the application model. | ||
| /// </summary> | ||
| public static class LetsEncryptBuilderExtensions | ||
| { | ||
| /// <summary> | ||
| /// Adds a Let's Encrypt 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 Let's Encrypt 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 from Let's Encrypt. | ||
| /// Port 80 is published to the host for Let's Encrypt to reach the ACME challenge. | ||
| /// </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> | ||
| /// This version of the package defaults to the <inheritdoc cref="CertbotContainerImageTags.Tag"/> tag of the <inheritdoc cref="CertbotContainerImageTags.Image"/> container image. | ||
| /// <example> | ||
| /// Use in application host: | ||
| /// <code lang="csharp"> | ||
| /// var domain = builder.AddParameter("domain"); | ||
| /// var email = builder.AddParameter("letsencrypt-email"); | ||
| /// | ||
| /// var certbot = builder.AddCertbot("certbot", domain, email); | ||
| /// | ||
| /// var myService = builder.AddContainer("myservice", "myimage") | ||
| /// .WithVolume("letsencrypt", "/etc/letsencrypt"); | ||
| /// </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) | ||
| .WithHttpEndpoint(port: 80, targetPort: 80, name: CertbotResource.HttpEndpointName) | ||
| .WithExternalHttpEndpoints() | ||
| .WithArgs(context => | ||
| { | ||
| context.Args.Add("certonly"); | ||
| context.Args.Add("--standalone"); | ||
| context.Args.Add("--non-interactive"); | ||
| context.Args.Add("--agree-tos"); | ||
| context.Args.Add("-v"); | ||
| context.Args.Add("--keep-until-expiring"); | ||
| // Fix permissions so non-root containers can read the certs | ||
| context.Args.Add("--deploy-hook"); | ||
| context.Args.Add("chmod -R 755 /etc/letsencrypt/live && chmod -R 755 /etc/letsencrypt/archive"); | ||
| context.Args.Add("--email"); | ||
| context.Args.Add(resource.EmailParameter); | ||
| context.Args.Add("-d"); | ||
| context.Args.Add(resource.DomainParameter); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a reference to the Let's Encrypt certificates volume 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 adds the Let's Encrypt certificates volume to the specified container resource, | ||
| /// allowing it to access SSL/TLS certificates obtained by Certbot. | ||
| /// </para> | ||
| /// <example> | ||
| /// <code lang="csharp"> | ||
| /// var domain = builder.AddParameter("domain"); | ||
| /// var email = builder.AddParameter("letsencrypt-email"); | ||
| /// | ||
| /// var certbot = builder.AddCertbot("certbot", domain, email); | ||
| /// | ||
| /// var yarp = builder.AddContainer("yarp", "myimage") | ||
| /// .WithCertbotCertificates(certbot); | ||
| /// </code> | ||
| /// </example> | ||
| /// </remarks> | ||
| public static IResourceBuilder<T> WithCertbotCertificates<T>( | ||
| this IResourceBuilder<T> builder, | ||
| IResourceBuilder<CertbotResource> certbot, | ||
| string mountPath = CertbotResource.CertificatesPath) where T : ContainerResource | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(certbot); | ||
|
|
||
| return builder.WithVolume(CertbotResource.CertificatesVolumeName, mountPath); | ||
| } | ||
| } | ||
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,100 @@ | ||
| # Aspire.Hosting.LetsEncrypt library | ||
|
|
||
| Provides extension methods and resource definitions for an Aspire AppHost to configure a Let's Encrypt Certbot resource. | ||
|
|
||
| ## Getting started | ||
|
|
||
| ### Install the package | ||
|
|
||
| In your AppHost project, install the Aspire Let's Encrypt Hosting library with [NuGet](https://www.nuget.org): | ||
|
|
||
| ```dotnetcli | ||
| dotnet add package Aspire.Hosting.LetsEncrypt | ||
| ``` | ||
|
|
||
| ## Usage example | ||
|
|
||
| Then, in the _AppHost.cs_ file of `AppHost`, add a Certbot resource to obtain SSL/TLS certificates: | ||
|
|
||
| ```csharp | ||
| var domain = builder.AddParameter("domain"); | ||
| var email = builder.AddParameter("letsencrypt-email"); | ||
|
|
||
| var certbot = builder.AddCertbot("certbot", domain, email); | ||
|
|
||
| var myService = builder.AddContainer("myservice", "myimage") | ||
| .WithCertbotCertificates(certbot); | ||
| ``` | ||
|
|
||
| The certbot container will: | ||
|
|
||
| - Run in standalone mode to handle ACME challenges on port 80 | ||
| - Obtain certificates for the specified domain from Let's Encrypt | ||
| - Store certificates in a shared volume at `/etc/letsencrypt` | ||
| - Fix permissions so non-root containers can read the certificates | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Required Parameters | ||
|
|
||
| The Certbot resource requires two parameters: | ||
|
|
||
| | Parameter | Description | | ||
| |-----------|-------------| | ||
| | `domain` | The domain name to obtain a certificate for | | ||
| | `letsencrypt-email` | The email address for Let's Encrypt registration and notifications | | ||
|
|
||
| These parameters can be set via environment variables or configuration: | ||
|
|
||
| ```bash | ||
| Parameters__domain=example.com | ||
| [email protected] | ||
| ``` | ||
|
|
||
| ### Sharing Certificates with Other Resources | ||
|
|
||
| Use the `WithCertbotCertificates` extension method to mount the certificates volume in other containers: | ||
|
|
||
| ```csharp | ||
| var yarp = builder.AddContainer("yarp", "myimage") | ||
| .WithCertbotCertificates(certbot); | ||
| ``` | ||
|
|
||
| Or mount the volume directly: | ||
|
|
||
| ```csharp | ||
| var myService = builder.AddContainer("myservice", "myimage") | ||
| .WithVolume("letsencrypt", "/etc/letsencrypt"); | ||
| ``` | ||
|
|
||
| ### Certificate Locations | ||
|
|
||
| After Certbot obtains certificates, they are available at: | ||
|
|
||
| - Certificate: `/etc/letsencrypt/live/{domain}/fullchain.pem` | ||
| - Private Key: `/etc/letsencrypt/live/{domain}/privkey.pem` | ||
|
|
||
| The `CertbotResource` exposes these paths as `ReferenceExpression` properties that can be used to configure other resources: | ||
|
|
||
| ```csharp | ||
| var certbot = builder.AddCertbot("certbot", domain, email); | ||
|
|
||
| // Access the certificate and private key paths | ||
| var certificatePath = certbot.Resource.CertificatePath; // /etc/letsencrypt/live/{domain}/fullchain.pem | ||
| var privateKeyPath = certbot.Resource.PrivateKeyPath; // /etc/letsencrypt/live/{domain}/privkey.pem | ||
| ``` | ||
|
|
||
| ## Connection Properties | ||
|
|
||
| The Certbot resource does not expose connection properties through `WithReference`. This is because the Certbot resource is a certificate provisioning tool, not a service that other resources connect to. | ||
|
|
||
| Instead, use the `WithCertbotCertificates` extension method to share certificates with other containers via a mounted volume. See the [Sharing Certificates with Other Resources](#sharing-certificates-with-other-resources) section above for usage examples. | ||
|
|
||
| ## Additional documentation | ||
|
|
||
| * https://letsencrypt.org/docs/ | ||
| * https://certbot.eff.org/docs/ | ||
|
|
||
| ## Feedback & contributing | ||
|
|
||
| https://github.com/dotnet/aspire |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're going to have situations where users have multiple certificates (potentially even multiple Certbot certificates) for a resource for different purposes. There might be a server auth (HTTPS) certificate from Let's Encrypt, but also one or more client auth certificates from something like a Vault server on their network.
It's one of the reasons I named the new HTTPS APIs
WithServerAuthenticationCertificateto differentiate from future client certificate usage. I think we'll need a similar model for publish time where it's not enough to just have a certificate, we need to consider what it's used for as well.I'd love to see this integrate with those new APIs so we could do something like:
and have everything work at both run and publish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future we'd be able to have
builder.AddKeyVaultCertificate("mycert")with the same conventions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does let’s encrypt support issuing more than just server certificates?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see we should rename the method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not after February 2026 they don't; but Certbot can be used to retrieve certificates from other providers that support the same protocol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a great way to provision client certificates from a Vault server on a private network, for example.