Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<Project Path="src/Aspire.Hosting.Kafka/Aspire.Hosting.Kafka.csproj" />
<Project Path="src/Aspire.Hosting.Keycloak/Aspire.Hosting.Keycloak.csproj" />
<Project Path="src/Aspire.Hosting.Kubernetes/Aspire.Hosting.Kubernetes.csproj" />
<Project Path="src/Aspire.Hosting.LetsEncrypt/Aspire.Hosting.LetsEncrypt.csproj" />
<Project Path="src/Aspire.Hosting.Maui/Aspire.Hosting.Maui.csproj" />
<Project Path="src/Aspire.Hosting.Milvus/Aspire.Hosting.Milvus.csproj" />
<Project Path="src/Aspire.Hosting.MongoDB/Aspire.Hosting.MongoDB.csproj" />
Expand Down Expand Up @@ -421,6 +422,7 @@
<Project Path="tests/Aspire.Hosting.Kafka.Tests/Aspire.Hosting.Kafka.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Keycloak.Tests/Aspire.Hosting.Keycloak.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Kubernetes.Tests/Aspire.Hosting.Kubernetes.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.LetsEncrypt.Tests/Aspire.Hosting.LetsEncrypt.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Milvus.Tests/Aspire.Hosting.Milvus.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.MongoDB.Tests/Aspire.Hosting.MongoDB.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.MySql.Tests/Aspire.Hosting.MySql.Tests.csproj" />
Expand Down
20 changes: 20 additions & 0 deletions src/Aspire.Hosting.LetsEncrypt/Aspire.Hosting.LetsEncrypt.csproj
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 src/Aspire.Hosting.LetsEncrypt/CertbotContainerImageTags.cs
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";
}
56 changes: 56 additions & 0 deletions src/Aspire.Hosting.LetsEncrypt/CertbotResource.cs
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 src/Aspire.Hosting.LetsEncrypt/LetsEncryptBuilderExtensions.cs
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>(
Copy link
Member

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 WithServerAuthenticationCertificate to 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:

var certbot = builder.AddCertbot("mydomaincert", "mydomain", "email")
    .RunAsDevelopmentCertificate();

builder.AddYarp("gateway")
    .WithServerAuthenticationCertificate(certbot);

and have everything work at both run and publish.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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

Copy link
Member

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.

Copy link
Member

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.

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);
}
}
100 changes: 100 additions & 0 deletions src/Aspire.Hosting.LetsEncrypt/README.md
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
Loading