LettuceEncrypt

repository·main·Indexed 23 days ago

https://github.com/natemcmaster/lettuceencrypt

An ASP.NET Core library for automatic HTTPS (SSL/TLS) certificate management using the ACME protocol (e.g., Let's Encrypt). It automates certificate generation and Kestrel configuration, supporting storage options such as the machine X.509 store, local directories, and Azure Key Vault. It is specifically designed for Kestrel and does not support IIS, HTTP.sys, or Azure App Services.

Tokens
3K
Snippets
8
Records
9
Agent score
33%

What's inside LettuceEncrypt

  1. Determine supported web server scenarios

    main

    LettuceEncrypt is designed to configure HTTPS certificates directly within the ASP.NET Core Kestrel server. It is compatible with scenarios where Kestrel manages the TLS termination, but it cannot manage certificates for external web servers or reverse proxies.

    Supported Scenarios

    • ASP.NET Core with Kestrel: Kestrel is the default in-process HTTP server and exposes ports directly to the internet. LettuceEncrypt will configure Kestrel with an auto-generated certificate.
    • ASP.NET Core with Kestrel Behind a TCP Load Balancer (SSL pass-thru): A TCP load balancer (like nginx) forwards traffic without decrypting it to the host running Kestrel. LettuceEncrypt will configure Kestrel with an auto-generated certificate.

    Unsupported Scenarios

    • ASP.NET Core with IIS: IIS does not support dynamically configuring HTTPS certificates via this library. If you are using IIS, you must use a different tool for certificate automation.
    • ASP.NET Core with Kestrel Behind a Reverse Proxy: If HTTPS traffic is decrypted by a different web server (e.g., Azure App Service/WebApps or a managed reverse proxy) before reaching ASP.NET Core, LettuceEncrypt cannot be used. The certificates must be configured on the reverse proxy server itself.
  2. Install and basic usage of LettuceEncrypt

    main

    LettuceEncrypt allows ASP.NET Core projects to automatically manage HTTPS certificates via the ACME protocol (e.g., Let's Encrypt). It works by automatically generating certificates on startup and configuring Kestrel to use them.

    Prerequisites

    • You must be using Kestrel as your web server. This library does not support IIS or HTTP.sys.
    • This library is not intended for Azure App Services (WebApps).

    Installation

    Install the NuGet package into your project.

    Setup

    Call IServiceCollection.AddLettuceEncrypt() in your ConfigureServices method. You must also provide configuration settings, typically in appsettings.json.

    Required Configuration

    At a minimum, you must provide DomainNames and an EmailAddress. Setting AcceptTermsOfService to true prevents the application from requiring manual input at startup.

    {
        "LettuceEncrypt": {
            "AcceptTermsOfService": true,
            "DomainNames": [ "example.com", "www.example.com" ],
            "EmailAddress": "it-admin@example.com"
        }
    }
    using Microsoft.Extensions.DependencyInjection;
    
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLettuceEncrypt();
        }
    }
  3. Persist certificates to Azure Key Vault

    main

    You can use Azure Key Vault to store certificates and the CA account key. This requires the LettuceEncrypt.Azure NuGet package.

    Setup

    Call PersistCertificatesToAzureKeyVault() after AddLettuceEncrypt().

    Configuration

    Provide the vault endpoint in appsettings.json. You can optionally specify a custom secret name for the account key.

    {
        "LettuceEncrypt": {
            "AzureKeyVault": {
                "AzureKeyVaultEndpoint": "https://myaccount.vault.azure.net/",
                "AccountKeySecretName": "my-lets-encrypt-account"
            }
        }
    }
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddLettuceEncrypt()
            .PersistCertificatesToAzureKeyVault();
    }
  4. Configure LettuceEncrypt with custom Kestrel settings

    main

    If you are manually configuring Kestrel using .UseKestrel(), you must explicitly call UseLettuceEncrypt within your Kestrel configuration to ensure the library can intercept and apply the certificates.

    Using ConfigureHttpsDefaults

    If your code uses ConfigureHttpsDefaults, call UseLettuceEncrypt inside the configuration delegate:

    webBuilder.UseKestrel(k =>
    {
        var appServices = k.ApplicationServices;
        k.ConfigureHttpsDefaults(h =>
        {
            h.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
            h.UseLettuceEncrypt(appServices);
        });
    });

    Using Listen and UseHttps

    If you are manually binding to addresses and ports using Listen and UseHttps, call UseLettuceEncrypt within the UseHttps options:

    webBuilder.UseKestrel(k =>
    {
        k.Listen(
            IPAddress.Any, 443,
            o => o.UseHttps(h =>
            {
                h.UseLettuceEncrypt(appServices);
            }));
    });
    webBuilder.UseKestrel(k =>
    {
        var appServices = k.ApplicationServices;
        k.ConfigureHttpsDefaults(h =>
        {
            h.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
            h.UseLettuceEncrypt(appServices);
        });
    });
  5. Configure allowed ACME challenge types

    main

    You can manually select which ACME challenge types the library is allowed to use via the AllowedChallengeTypes configuration key. The default value is Any, which attempts all supported methods before failing.

    Supported values:

    • Http01: Uses a well-known URL on the server.
    • TlsAlpn01: Uses an ephemeral certificate in the TLS handshake.
    • Dns01: Uses a TXT record under the domain.
    • Any: (Default) Uses Http01 and/or TlsAlpn01 and Dns01.

    To set multiple types in appsettings.json, provide a comma-separated list:

    {
        "LettuceEncrypt": {
            "AllowedChallengeTypes": "Http01, TlsAlpn01, Dns01"
        }
    }
  6. Persist certificates to a local directory

    main

    By default, certificates are stored in the machine's X.509 store. You can change this to save and load PFX certificate files and the CA account key to a specific directory using PersistDataToDirectory.

    using LettuceEncrypt;
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddLettuceEncrypt()
            .PersistDataToDirectory(new DirectoryInfo("C:/data/LettuceEncrypt/"), "Password123");
    }
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddLettuceEncrypt()
            .PersistDataToDirectory(new DirectoryInfo("C:/data/LettuceEncrypt/"), "Password123");
    }
  7. Implement a custom account store for CA keys

    main

    The CA account key is used to interact with the certificate authority and is required for renewals. By default, it is saved to disk. To store this key elsewhere, implement IAccountStore and register it as a singleton.

    using LettuceEncrypt;
    using LettuceEncrypt.Accounts;
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLettuceEncrypt();
        services.AddSingleton<IAccountStore, MyAccountStore>();
    }
    
    class MyAccountStore : IAccountStore
    {
        public Task SaveAccountAsync(AccountModel account, CancellationToken cancellationToken)
        {
            // save the account object somewhere
            return Task.CompletedTask;
        }
    
        public Task<AccountModel?> GetAccountAsync(CancellationToken cancellationToken)
        {
            // return null if there is no account and one will be created for you
            return Task.FromResult<AccountModel?>(null);
        }
    }
    using LettuceEncrypt.Accounts;
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLettuceEncrypt();
        services.AddSingleton<IAccountStore, MyAccountStore>();
    }
    
    class MyAccountStore: IAccountStore
    {
        public Task SaveAccountAsync(AccountModel account, CancellationToken cancellationToken)
        {
            // save the account object somewhere
            return Task.CompletedTask;
        }
    
    
        public Task<AccountModel?> GetAccountAsync(CancellationToken cancellationToken)
        {
            // return null if there is no account and one will be created for you
            return Task.FromResult<AccountModel?>(null);
        }
    }
  8. Implement custom certificate storage and loading

    main

    To fully control how certificates are saved and retrieved, implement the ICertificateRepository and ICertificateSource interfaces and register them as singletons in your dependency injection container.

    • ICertificateRepository: Defines how to save a certificate (e.g., to a database or custom cloud storage).
    • ICertificateSource: Defines how to find existing certificates when the server starts.
    using LettuceEncrypt;
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLettuceEncrypt();
        services.AddSingleton<ICertificateRepository, MyCertRepo>();
        services.AddSingleton<ICertificateSource, MyCertSource>();
    }
    
    class MyCertRepo : ICertificateRepository
    {
        public async Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
        {
            byte[] certData = certificate.Export(X509ContentType.Pfx, "optionallySetPfxPassword");
            // save this data somehow
        }
    }
    
    class MyCertSource : ICertificateSource
    {
        public async Task<IEnumerable<X509Certificate2>> GetCertificatesAsync(CancellationToken cancellationToken)
        {
            // find and return certificate objects. Return an empty enumerable if none are found
            return Enumerable.Empty<X509Certificate2>();
        }
    }
    using Microsoft.Extensions.DependencyInjection;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLettuceEncrypt();
        services.AddSingleton<ICertificateRepository, MyCertRepo>();
        services.AddSingleton<ICertificateSource, MyCertSource>();
    }
    
    class MyCertRepo : ICertificateRepository
    {
        public async Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
        {
            byte[] certData = certificate.Export(X509ContentType.Pfx, "optionallySetPfxPassword");
            // save this data somehow
        }
    }
    
    class MyCertSource : ICertificateSource
    {
        public async Task<IEnumerable<X509Certificate2>> GetCertificatesAsync(CancellationToken cancellationToken)
        {
            // find and return certificate objects. Return an empty enumerable if none are found
            return Enumerable.Empty<X509Certificate2>();
        }
    }
  9. Implement a DNS-01 challenge provider

    main

    When using the Dns01 challenge type, you must implement the IDnsChallengeProvider interface to manage the required TXT records in your DNS provider. You must then register your implementation to replace the default NoOpDnsChallengeProvider.

    public class MyDnsChallengeProvider : IDnsChallengeProvider
    {
        private readonly ISomeExternalDnsClient _client;
    
        public MyDnsChallengeProvider(ISomeExternalDnsClient client) => _client = client;
    
        public Task AddTxtRecordAsync(string domainName, string txt, CancellationToken ct = default)
        {
            return _client.AddDnsTxtRecord(domainName, txt, ct);
        }
    
        public Task RemoveTxtRecordAsync(string domainName, string txt, CancellationToken ct = default)
        {
            return _client.RemoveDnsTxtRecord(domainName, txt, ct);
        }
    }
    {
        private readonly ISomeExternalDnsClient _client;
    
        public MyDnsChallengeProvider(ISomeExternalDnsClient client) => _client = client;
    
        public Task AddTxtRecordAsync(string domainName, string txt, CancellationToken ct = default)
        {
            return _client.AddDnsTxtRecord(domainName, txt, ct);
        }
    
        public Task RemoveTxtRecordAsync(string domainName, string txt, CancellationToken ct = default)
        {
            return _client.RemoveDnsTxtRecord(domainName, txt, ct);
        }
    }