SparrowCert - Automatic Renewal of SSL Certificates within your .NET Core API

A New Library for Automatic Renewal of Let's Encrypt Certificates

By Thomas

Automatic Renewal of Let’s Encrypt Certificates without Lock yourself to any Cloud Provider

As software engineers, we constantly strive to strike a balance between security and efficiency. SSL certificate management is a prime example of this. While Let’s Encrypt has revolutionized web security by offering free SSL certificates, many companies are still hesitant to adopt it. Why is this the case?

Barriers to Adopting Let’s Encrypt

  • Short certificate validity period of 90 days
  • Lack of support for OV (Organization Validation) and EV (Extended Validation)
  • Limited technical support (mainly reliant on community Q&A)

For large corporations or e-commerce sites that require OV/EV certificates, the use of Let’s Encrypt may be limited. However, the other two issues can be resolved technically.

The Need for Automation

Certificate management typically falls on the operations team, DevSecOps team, or security team, and is often carried out manually during off-peak hours. A minor mistake during the renewal process can cause the entire website or API to become inaccessible.

If wildcard domains like *.domain.com are not used for security reasons, the complexity increases with multiple servers (auth.domain.com, api.domain.com, user.domain.com, etc.), increasing the likelihood of mistakes during the process.

However, if we could incorporate a method to automate certificate issuance and renewal from the development stage, it would be beneficial. While searching for a viable method for .NET Core projects, I found Certes (https://github.com/fszlin/certes) and FluffySpoon.AspNet.EncryptWeMust (https://github.com/ffMathy/FluffySpoon.AspNet.EncryptWeMust).

After reviewing these methods and codes, I simplified and added additional features to create a Nuget package library that anyone can use as library.

SparrowCert

SparrowCert is a certificate generation/renewal automation library that can be used in any .NET Core-based web service. Its main features include:

  • Built-in ACME protocol support (see below diagram for more details)
  • Usable in on-premises, cloud, and container environments, including Kubernetes
  • Zero dependency on any cloud providers (No lock-in to any cloud provider)
  • Supports Linux, Windows, macOS (operates as a .NetCore Kestrel plugin Middleware)
  • Automatic issuance and renewal of certificates through Let’s Encrypt

How ACME Works (from https://www.abetterinternet.org/documents/letsencryptCCS2019.pdf)

Added Features from previous implementations

  • Supports .NET Core 8.0
  • Removed and updated old dependencies with security vulnerabilities (because it is old)
  • User-customizable configuration file (cert.json)
  • Container-friendly (tested with Kubernetes, Docker, NAT routers, Nginx Proxy)
  • Differentiates Let’s Encrypt Staging/Production mode
  • Automatic notification via email or Slack for certificate-related events (issuance, renewal, failure)

How to run it

  1. Clone the repository (or get it from NuGet.org)
  2. Navigate to the project ‘SparrowCert.Runner’ directory
  3. Create your cert.json file (see below), save it into your project directory (or for testing, SparrowCert.Runner project directory)
  4. Run dotnet build to build the project
  5. Run dotnet run to start the service

Configuration

The service is configured through a cert.json file. Here is an example configuration:

[Note] Of course, your ‘cert.json’ file SHOULD NOT BE in your repository, ideally put in any secret vault or GitHub Actions Secrets to be injected to your server at runtime.

{
  "Domains": [
    "www.own-domain.org",
    "api.own-domain.org"                      // your domain name (include sub-domains, if you have)
  ],
  "Email": "your@email.address",              // your email address for Let's Encrypt account
  "RenewBeforeExpiry": "30.00:00:00",         // renew 30 days before expiry (default 30 days)
  "RenewAfterIssued": "80.00:00:00",          // renew 80 days after issued (default null, which means do nothing)
  "UseStaging": true,                         // trying with Let's Encrypt Staging first
  "CertSigningRequest": {
    "CountryName": "CA",                      // your country code
    "State": "Ontario",                       // your state
    "Locality": "Toronto",                    // your city
    "Organization": "Acme Inc.",              // your organization name
    "OrganizationUnit": "IT",                 // your organization unit (department)
    "CommonName": "own-domain.org"            // your domain name (primary domain)
  },
  "RenewalFailMode": 1,                       // 0: unhandled, 1: continue, 2: retry
  "KeyAlgorithm": 1,                          // 0: RSA256, 1: ES256, 2: ES384, 3: ES512
  "RenewalStartupDelay": "00:00:00",          // delay on startup before renewing

  "HttpPort": 5080,                           // for NAT, customizable HTTP port (80 -> i.e. 5080)
  "HttpsPort": 5443,                          // for NAT, customizable HTTPS port (443 -> i.e. 5443)
  "CertFriendlyName": "own-domain.org",
  "StorePath": "/sparrow-cert/",              // where to store the new certificates (typically, with K8s Persistent Volume)
  "CertPwd": "your-cert-password",            // your certificate password when it is issued (or renewed)

  "Notify": {
    "Slack": {
      "Enabled": true,                        // default false, discarded if not enabled
      "Channels": [ "T0*******HZ" ],          // your Slack channel IDs for notification (get it from Slack channel details)
      "Token": "<your-slack-token>",          // your Slack token, typically starts with 'xoxb-'
      "Body": "\n\n\nPlease store the attachment securely, and take the necessary action accordingly.\n\n\n"
    },
    "Email": {
      "Enabled": true,                        // default false, discarded if not enabled
      "SenderName": "cert-bot",               // your sender name
      "SenderEmail": "<your@email.address>",
      "Recipient": "<recipient@email.address>",
      "SmtpHost": "<your-smtp-server>",
      "SmtpPort": 587,                        // your SMTP port, typically 587
      "SmtpUser": "<your-smtp-user-name>",
      "SmtpPwd": "<your-smtp-password>",
      "Html": false,
      "Body": "\n\n\nPlease store the attachment securely, and take the necessary action accordingly.\n\n\n"
    }
  }
}

How to integrate into your project


public static void Main(string[] args) {

    ....
  
  // Load the configuration from the 'cert.json' file      
  var configPath = "<path to your 'cert.json' file>";
  var config = CertJsonConfiguration.FromFile(configPath);
  var buildArgs = SparrowCertStartup.SetConfiguration(config);
   
   
  CreateWebHostBuilder(buildArgs).Build().Run();
}
 
private static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
  WebHost.CreateDefaultBuilder(args)
    
    .... 
    
    // Use Kestrel with custom configuration
    .UseKestrel(o => { 
        o.ListenAnyIP(args.HttpPort);
        o.ListenAnyIP(args.HttpsPort, listenOptions => {
           listenOptions.UseHttps(SelfSignedCertGenerator.GenerateCertificate(args.Domain));
    });
        
        
   })
   .UseStartup<SparrowCertStartup>();


Credit

This project is started from reviewing and trying to utilize FluffySpoon.AspNet.EncryptWeMust and Certes, ended-up refactoring quite a bit for my use cases. Kudos to the authors for the initial implementation.

References

All the credit for pioneering the approach goes to the authors. I just updated the code for my use cases, and hopping this useful to others.

Dependencies

To do

  • Add integration test
  • Check with mobile devices (iOS, Android) for the certificate compatibility ECDHE-RSA-AES256-GCM-SHA384
  • Add certificate renewal notice as Slack message (including delivery of new certificate to the channel)
  • Thinking it runs as one certificate renewal service for multiple services (i.e. as a sidecar container)

License

MIT

Share: Twitter Facebook LinkedIn