Manage certificates on NixOS using ACME and Google Domains

Certificate management has significantly simplified over the past decade, though the tools used, DNS provider selected, and the Certificate Authority (CA) chosen may introduce complexities. In this article, I will guide you through the process of setting up ACME on NixOS for a domain hosted on Google Domains, using both Let’s Encrypt and Google’s own CA (called Google Trust Services).

In our increasingly digital world, the security and integrity of data transactions have never been more crucial. Transport Layer Security (TLS) and HTTPS, which sit at the heart of secure internet communications, ensure the confidentiality and integrity of information shared over the web. By encrypting data in transit, these technologies have become indispensable in today’s internet.

The management of these security certificates has undergone a significant transformation in recent years, primarily due to the ACME protocol and Let’s Encrypt. Together, they have democratized certificate management, simplifying the process and making it accessible to all.

Google Domains

In 2014, Google launched Google Domains, a domain registration service. The service took off with the introduction of the .dev top-level domain (TLD), marketed as a “secure domain for developers and technology”. A key feature of this TLD is its presence on the HSTS preload list, requiring HTTPS for all connections to .dev websites across browsers.

Since its launch, Google Domains has seen significant improvements. One of the most recent updates is the implementation of the ACME DNS API (more on this later).

Despite my strong preference for Google Domains, due to its affordability and ease of setting up a new domain, it’s important to acknowledge its shortcomings. Specifically, it lacks certain functionalities (that I personally miss very much), such as forwarding emails from a single alias to multiple addresses, a feature commonly provided by many DNS registrars.

ACME and Let’s Encrypt

ACME (Automated Certificate Management Environment) is a protocol that automates the process of certificate issuance and management. Its function is often conflated with Let’s Encrypt, a free, open, and automated certificate authority (CA) that utilizes ACME. To clarify, ACME is the protocol, while Let’s Encrypt is a CA that leverages this protocol. Similarly, Google Domains offers a CA for its customers, all operating under the umbrella of the ACME protocol.

Here’s a brief rundown of the steps in the ACME protocol:

  1. Account Creation: The user (or ACME client) initiates communication with the ACME server to create a new account.
  2. Certificate Request: The user makes a new order for a domain through the ACME server. The server then returns the authorizations needed for the requested domain.
  3. Domain Validation: The user responds with a key authorization to prove ownership of the domain. The ACME server validates this information.
  4. Certificate Issuance: Once the domain ownership is confirmed, the user requests the ACME server to issue a certificate. The ACME server communicates this request to the CA. The CA, upon approval, issues the certificate, which is then passed on to the user by the ACME server.

This automated process simplifies the usually complex procedure of certificate issuance, making it easier and quicker to set up TLS-protected communication (for example HTTPS on a web server).

One important aspect of the ACME protocol is the External Account Binding (EAB). EAB is a mechanism that allows a new ACME account to be associated with an existing account on a non-ACME system, such as a CA’s customer database. This is used to ensure that the new ACME account is controlled by the same entity that controls the existing non-ACME account. Google Trust Services use EAB to authorize new account creations.

ACME on NixOS

NixOS offers the security.acme module that handles ACME protocol tasks to manage certificates. Under the hood it uses a tool called lego, a “Let’s Encrypt client and ACME library written in Go”.

Experimenting with lego

Before diving into how security.acme works, let’s see some example commands for using lego with Google Domains.

You can install it from nixpkgs:

nix-shell -p lego

Note: make sure to use at least version 4.11.0 as Google Domains is only available since that version.

We will use DNS challenge in these examples which requires setting up an access token:

  1. Sign in to Google Domains
  2. Select the domain you want to use (click Manage in the domain list)
  3. Go to Security in the menu on the left
  4. Under the ACME DNS API section click Create token
  5. Copy the token and save it in an environment variable called GOOGLE_DOMAINS_ACCESS_TOKEN:
export GOOGLE_DOMAINS_ACCESS_TOKEN="<YOUR_TOKEN>"

Find more details about setting up an access token in the documentation.

Once the token is configured, lego is ready to issue Let’s Encrypt certificates for a domain hosted on Google Domains using the DNS challenge method:

lego --email="you@example.com" --domains="example.com" --accept-tos --dns="googledomains" run

Once the process concludes, you should be able to see the following files under the .lego directory:

tree .lego

.lego
├── accounts
│   └── acme-v02.api.letsencrypt.org
│       └── you@example.com
│           ├── account.json
│           └── keys
│               └── you@example.com.key
└── certificates
    ├── example.com.crt
    ├── example.com.issuer.crt
    ├── example.com.json
    └── example.com.key

5 directories, 6 files

Inspecting the certificate should reveal that it was issued by Let’s Encrypt:

openssl x509 -in .lego/certificates/example.com.crt -noout -issuer

issuer= /C=US/O=Let's Encrypt/CN=R3

(Keep in mind that Let’s Encrypt certs have to be renewed every 90 days. The security.acme module takes care of that using the lego renew command, but it’s out of scope for this post)

Generally, using Let’s Encrypt suffices for most scenarios. However, just to explore further (or for fun, if not for any particular reason), let’s try using Google’s CA as well.

As mentioned earlier, Google’s CA is not available without authorization, so setting up an EAB key is required. To achieve this, follow the steps above until step 3. However, instead of acquiring an ACME DNS API token, navigate to the Google Trust Services section under SSL/TLS Certificates for your domain and select Get EAB Key. Make sure to take note of the EAB Key ID and EAB HMAC Key in the appearing dialog.

Find more details about setting up an EAB key in the documentation.

Once the EAB key is obtained, lego is ready to issue certificates using Google’s CA:

lego --eab --server "https://dv.acme-v02.api.pki.goog/directory" --kid "YOUR_EAB_KEY_ID" --hmac "YOUR_EAB_HMAC_KEY" --email "you@gmail.com" --domains="example.com" --accept-tos --dns="googledomains" run

Inspecting the contents of the .lego directory again should reveal minimal differences, given that we used the same domain in our example:

tree .lego

.lego
├── accounts
│   ├── acme-v02.api.letsencrypt.org
│   │   └── you@example.com
│   │       ├── account.json
│   │       └── keys
│   │           └── you@example.com.key
│   └── dv.acme-v02.api.pki.goog
│       └── you@gmail.com
│           ├── account.json
│           └── keys
│               └── you@gmail.com.key
└── certificates
    ├── example.com.crt
    ├── example.com.issuer.crt
    ├── example.com.json
    └── example.com.key

8 directories, 8 files

Upon a more detailed inspection of the certificate, however, it should reveal a different issuer:

openssl x509 -in .lego/certificates/example.com.crt -noout -issuer

issuer= /C=US/O=Google Trust Services LLC/CN=GTS CA 1P5

Be advised, the EAB key can be used only once. Therefore, if you remove the .lego directory or intend to run this on a different machine, you will need to generate a new key.

Configuring ACME on NixOS

Now that we know how lego works under the hood, we can now proceed to configure the security.acme module in NixOS.

Depending on the specifics of your NixOS setup, the actual configuration may slightly vary. Therefore, I’ve provided generic snippets below for broader applicability.

First, let’s configure the module to use Let’s Encrypt:

{
  security.acme = {
    defaults = {
      email = "you@example.com";
    };

    acceptTerms = true;

    certs = {
	  "example.com" = {
	    domain = "example.com";
	    dnsProvider = "googledomains";
	    credentialsFile = "/var/src/secrets/example.com-googledomains-api-token";
	  };
    };
  };
}

The credentialsFile attribute should point to a file containing the required environment variables for the selected DNS provider, adhering to the EnvironmentFile format used by systemd. In this particular scenario, something like this:

GOOGLE_DOMAINS_ACCESS_TOKEN=<YOUR_TOKEN>

After applying the configuration, you should be able to find the certificate in the /var/lib/acme directory:

tree /var/lib/acme

/var/lib/acme
└── example.com
    ├── cert.pem -> fullchain.pem
    ├── chain.pem
    ├── fullchain.pem
    ├── full.pem
    └── key.pem

1 directory, 5 files

Unsurprisingly, upon inspecting the certificate, you should see similar results as before:

openssl x509 -in /var/lib/acme/example.com/full.pem -noout -issuer
issuer=C = US, O = Let's Encrypt, CN = R3

Using Google’s CA with this module presents a bit more of a challenge, but it is certainly doable. To begin with, make sure that you generate a new EAB key (as explained in the previous section). Unfortunately, given the current design of the module, your EAB key must be input directly into the module config in plaintext. The silver lining here is that the EAB key can only be used once, minimizing any potential security concerns. (Alternatively, you always have the option to stick with Let’s Encrypt instead)

{
  security.acme = {
    defaults = {
      email = "you@gmail.com";
    };

    acceptTerms = true;

    certs = {
	  "example.com" = {
	    domain = "example.com";
	    dnsProvider = "googledomains";
	    credentialsFile = "/var/src/secrets/example.com-googledomains-api-token";
	    server = "https://dv.acme-v02.api.pki.goog/directory";
	    extraLegoFlags = ["--eab" "--kid" "<YOUR_EAB_KEY_ID>" "--hmac" "<YOUR_EAB_HMAC_KEY>"];
	  };
    };
  };
}

Once again, inspecting the certificate shouldn’t hold any surprises:

openssl x509 -in /var/lib/acme/example.com/full.pem -noout -issuer
issuer=C = US, O = Google Trust Services LLC, CN = GTS CA 1P5

Summary

A common concern I often encounter (or even experience myself) is the fear that NixOS’s approach to masking every configuration behind modules cannot possibly accommodate all potential use cases. While this sentiment may hold some truth, NixOS continues to impress me positively. There was absolutely no need for me to use Google’s CA; Let’s Encrypt would have been perfect for my situation. Yet, I was able to make it work nonetheless.

The icing on the cake? If I choose to relocate this entire setup to another machine, I can do so with ease simply by transferring the configuration. I won’t have to recall how I configured Certbot or lego, nor worry about setting up a cron job. Everything just works seamlessly, reaffirming my decision to use Nix and NixOS every single day.