Jump to content

ACME: Difference between revisions

From NixOS Wiki
imported>Onny
mNo edit summary
Tie-ling (talk | contribs)
m update formatting
 
(17 intermediate revisions by 10 users not shown)
Line 1: Line 1:
NixOS supports automatic domain validation & certificate retrieval and renewal using the ACME protocol. Any provider can be used, but by default NixOS uses Let's Encrypt. The alternative ACME client [https://go-acme.github.io/lego/ lego] is used under the hood.  
NixOS supports automatic domain validation & certificate retrieval and renewal using the ACME protocol. Any provider can be used, but by default NixOS uses Let's Encrypt. The alternative ACME client [https://go-acme.github.io/lego/ lego] is used under the hood.  


== Setup ==
= Basics =
 
This process should generate three key files.  The naming and usage of
the three key files is common to all programs and services in NixOS.
 
We let <code>sslCertDir =
config.security.acme.certs.${domainName}.directory;</code> in the
following paragraph.
 
The three key files and their location are
 
* <code>sslServerCert = "/var/host.cert";</code> Path to server SSL certificate. Located at <code>"${sslCertDir}/fullchain.pem"</code>.
 
* <code>sslServerChain = "/var/ca.pem";</code> Path to server SSL chain file. Located at <code>"${sslCertDir}/chain.pem"</code>.
 
* <code>sslServerKey = "/var/host.key";</code> Path to server SSL certificate key. Located at <code>"${sslCertDir}/key.pem"</code>.
 
Beginning in late 2024, user @ThinkChaos started working on unifying
modules options to use the same interface for specifying certificates.
 
Currently, in 2025-09, the <code>useACMEHost</code> option can be used
with a wide variety of services
[https://search.nixos.org/options?channel=25.05&query=useACMEHost],
which simplifies the configuration and enables the automatic checking
of correct private and public key permissions during nixos-rebuild.
 
= Obtaining a new certificate =
 
== Basics ==
 
You need to agree to the Terms of Service, provide an email address,
provide a domain name, and, if any, extra domain names.
 
DNS challenge supports obtaining certificates for wildcard domains,
such as <code>*.example.org</code>.
 
<syntaxhighlight lang="nix">
let
  domainName = "example.org";
in
{
  security.acme = {
    acceptTerms = true;
    defaults.email = "admin@${domainName}";
    certs = {
      "${domainName}" = {
        group = config.services.nginx.group;
        extraDomainNames = [
          "mail.${domainName}"
          "www.${domainName}"
        ];
      };
    };
  };
}
</syntaxhighlight>
 
== HTTP challenge ==
 
To use HTTP challenge, you need to have your DNS record pointing to
this computer.  You also need to enable a web server and allow
plaintext traffic on port 80.  This example is based on the previous section:
 
<syntaxhighlight lang="nix">
  security.acme = {
    defaults.webroot = "/var/lib/acme/acme-challenge/";
    # We are using nginx as webserver, therefore set correct key permissions
    certs."${domainName}".group = config.services.nginx.group;
  };
 
  # for acme plain http challenge
  networking.firewall.allowedTCPPorts = [ 80 ];
 
  # webserver for http challenge
  services.nginx = {
    enable = true;
    virtualHosts."${domainName}" = {
      forceSSL = true;
      useACMEHost = "${domainName}";
      locations."/.well-known/".root = "/var/lib/acme/acme-challenge/";
    };
  };
 
</syntaxhighlight>
 
 
== DNS challenge ==
 
=== With inwx as DNS provider ===


Following example setup generates certificates using DNS validation. [https://letsencrypt.org/repository/ Let's Encrypt ToS] has to be accepted. Further the contact mail <code>admin+acme@example.com</code> is defined.
Following example setup generates certificates using DNS validation. [https://letsencrypt.org/repository/ Let's Encrypt ToS] has to be accepted. Further the contact mail <code>admin+acme@example.com</code> is defined.
Line 8: Line 96:
security.acme = {
security.acme = {
   acceptTerms = true;
   acceptTerms = true;
   defaults.email = "admin+acme@example.com";
   defaults.email = "admin+acme@example.org";
   certs."mx1.example.org" = {
   certs."mx1.example.org" = {
     dnsProvider = "inwx";
     dnsProvider = "inwx";
    credentialsFile = config.sops.secrets.lego-inwx-credentials.path;
     # Supplying password files like this will make your credentials world-readable
     # Suplying password files like this will make your credentials world-readable
     # in the Nix store. This is for demonstration purpose only, do not use this in production.
     # in the Nix store. This is for demonstration purpose only, do not use this in production.
     credentialsFile = "${pkgs.writeText "inwx-creds" ''
     environmentFile = "${pkgs.writeText "inwx-creds" ''
       INWX_USERNAME=xxxxxxxxxx
       INWX_USERNAME=xxxxxxxxxx
       INWX_PASSWORD=yyyyyyyyyy
       INWX_PASSWORD=yyyyyyyyyy
Line 22: Line 109:
</syntaxhighlight>
</syntaxhighlight>


Certificates are getting generated for the domain <code>mx1.example.org</code> using the DNS provider <code>inwx</code>. See [https://go-acme.github.io/lego/dns upstream documentation] on available providers and their specific configuration for the <code>credentialsFile</code> option.
Certificates are getting generated for the domain
<code>mx1.example.org</code> using the DNS provider
<code>inwx</code>. See [https://go-acme.github.io/lego/dns upstream
documentation] on available providers and their specific configuration
for the <code>credentialsFile</code> option.


== See also ==
=== With Cloudflare as DNS provider ===
 
The next example issues a wildcard certificate and uses Cloudflare for validation. We're also adding the group "nginx" here so that the certificate files can be used by nginx later on.<syntaxhighlight lang="nix">
security.acme = {
  acceptTerms = true;
  defaults.email = "admin@example.org";
  certs = {
    "example.org" = {
      domain = "*.example.org";
      group = "nginx";
      dnsProvider = "cloudflare";
      # location of your CLOUDFLARE_DNS_API_TOKEN=[value]
      # https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#EnvironmentFile=
      environmentFile = "/home/admin/cloudflare";
    };
  };
};
</syntaxhighlight>
 
== TLS challenge ==
 
Todo.
 
= Integration with service modules =
 
== Setting file permission with postRun ==
 
Use the <code>security.acme.certs.*.postRun</code> to set permissions
on the key directory and the key files:
 
<syntaxhighlight lang="nix">
security.acme.certs.${domainName}.postRun = ''
  # set permission on dir
  ${pkgs.acl}/bin/setfacl -m \
  u:nginx:rx,u:turnserver:rx,u:prosody:rx,u:dovecot2:rx,u:postfix:rx \
  /var/lib/acme/${domainName}
 
  # set permission on key file
  ${pkgs.acl}/bin/setfacl -m \
  u:nginx:r,u:turnserver:r,u:prosody:r,u:dovecot2:r,u:postfix:r \
  /var/lib/acme/${domainName}/*.pem
'';
</syntaxhighlight>
 
== Reload services after renewal ==
 
<syntaxhighlight lang="nix">
security.acme.certs.${domainName}.reloadServices = [
  "prosody"
  "coturn"
  "nginx"
  "dovecot2"
  "postfix"
];
</syntaxhighlight>
 
== Using useACMEHost ==
 
Many service modules support obtaining certificates.  But if you were
to configure certificate options separately for each service module,
it would be time consuming and risks hitting the certificate renewal
limits of the service provider.
 
Instead, centrally manage certificate options within the security.acme
module; then point other services to security.acme with
<code>useACMEHost</code> option.
 
<syntaxhighlight lang="nix">
security.acme.certs."example.org".extraDomainNames = [
  "syncplay.example.org"
  "reposilite.example.org"
  "site2.example.org"
];
 
services.syncplay.useACMEHost = "example.org";
services.reposilite.useACMEHost = "example.org";
services.nginx.virtualHosts."site2.example.org".useACMEHost = "example.org";
</syntaxhighlight>
 
 
= Using Let's Encrypt Staging =
 
If you'd like to use the Let's Encrypt [https://letsencrypt.org/docs/staging-environment/ staging environment], eg for its less stringent rate limits, set
 
<syntaxhighlight lang="nix">
security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
</syntaxhighlight>
 
= See also =


* NixOS manual on [https://nixos.org/manual/nixos/stable/index.html#module-security-acme SSL/TLS Certificates with ACME]
* NixOS manual on [https://nixos.org/manual/nixos/stable/index.html#module-security-acme SSL/TLS Certificates with ACME]
[[Category: Server]]
[[Category: Networking]]

Latest revision as of 06:58, 19 September 2025

NixOS supports automatic domain validation & certificate retrieval and renewal using the ACME protocol. Any provider can be used, but by default NixOS uses Let's Encrypt. The alternative ACME client lego is used under the hood.

Basics

This process should generate three key files. The naming and usage of the three key files is common to all programs and services in NixOS.

We let sslCertDir = config.security.acme.certs.${domainName}.directory; in the following paragraph.

The three key files and their location are

  • sslServerCert = "/var/host.cert"; Path to server SSL certificate. Located at "${sslCertDir}/fullchain.pem".
  • sslServerChain = "/var/ca.pem"; Path to server SSL chain file. Located at "${sslCertDir}/chain.pem".
  • sslServerKey = "/var/host.key"; Path to server SSL certificate key. Located at "${sslCertDir}/key.pem".

Beginning in late 2024, user @ThinkChaos started working on unifying modules options to use the same interface for specifying certificates.

Currently, in 2025-09, the useACMEHost option can be used with a wide variety of services [1], which simplifies the configuration and enables the automatic checking of correct private and public key permissions during nixos-rebuild.

Obtaining a new certificate

Basics

You need to agree to the Terms of Service, provide an email address, provide a domain name, and, if any, extra domain names.

DNS challenge supports obtaining certificates for wildcard domains, such as *.example.org.

let
  domainName = "example.org";
in
{
  security.acme = {
    acceptTerms = true;
    defaults.email = "admin@${domainName}";
    certs = {
      "${domainName}" = {
        group = config.services.nginx.group;
        extraDomainNames = [
          "mail.${domainName}"
          "www.${domainName}"
        ];
      };
    };
  };
}

HTTP challenge

To use HTTP challenge, you need to have your DNS record pointing to this computer. You also need to enable a web server and allow plaintext traffic on port 80. This example is based on the previous section:

  security.acme = {
    defaults.webroot = "/var/lib/acme/acme-challenge/";
    # We are using nginx as webserver, therefore set correct key permissions
    certs."${domainName}".group = config.services.nginx.group;
  };

  # for acme plain http challenge
  networking.firewall.allowedTCPPorts = [ 80 ];

  # webserver for http challenge
  services.nginx = {
    enable = true;
    virtualHosts."${domainName}" = {
      forceSSL = true;
      useACMEHost = "${domainName}";
      locations."/.well-known/".root = "/var/lib/acme/acme-challenge/";
    };
  };


DNS challenge

With inwx as DNS provider

Following example setup generates certificates using DNS validation. Let's Encrypt ToS has to be accepted. Further the contact mail admin+acme@example.com is defined.

security.acme = {
  acceptTerms = true;
  defaults.email = "admin+acme@example.org";
  certs."mx1.example.org" = {
    dnsProvider = "inwx";
    # Supplying password files like this will make your credentials world-readable
    # in the Nix store. This is for demonstration purpose only, do not use this in production.
    environmentFile = "${pkgs.writeText "inwx-creds" ''
      INWX_USERNAME=xxxxxxxxxx
      INWX_PASSWORD=yyyyyyyyyy
    ''}";
  };
};

Certificates are getting generated for the domain mx1.example.org using the DNS provider inwx. See [https://go-acme.github.io/lego/dns upstream documentation] on available providers and their specific configuration for the credentialsFile option.

With Cloudflare as DNS provider

The next example issues a wildcard certificate and uses Cloudflare for validation. We're also adding the group "nginx" here so that the certificate files can be used by nginx later on.

security.acme = {
  acceptTerms = true;
  defaults.email = "admin@example.org";
  certs = {
    "example.org" = {
      domain = "*.example.org";
      group = "nginx";
      dnsProvider = "cloudflare";
      # location of your CLOUDFLARE_DNS_API_TOKEN=[value]
      # https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#EnvironmentFile=
      environmentFile = "/home/admin/cloudflare";
    };
  };
};

TLS challenge

Todo.

Integration with service modules

Setting file permission with postRun

Use the security.acme.certs.*.postRun to set permissions on the key directory and the key files:

security.acme.certs.${domainName}.postRun = ''
  # set permission on dir
  ${pkgs.acl}/bin/setfacl -m \
  u:nginx:rx,u:turnserver:rx,u:prosody:rx,u:dovecot2:rx,u:postfix:rx \
  /var/lib/acme/${domainName}

  # set permission on key file
  ${pkgs.acl}/bin/setfacl -m \
  u:nginx:r,u:turnserver:r,u:prosody:r,u:dovecot2:r,u:postfix:r \
  /var/lib/acme/${domainName}/*.pem
'';

Reload services after renewal

security.acme.certs.${domainName}.reloadServices = [
  "prosody"
  "coturn"
  "nginx"
  "dovecot2"
  "postfix"
];

Using useACMEHost

Many service modules support obtaining certificates. But if you were to configure certificate options separately for each service module, it would be time consuming and risks hitting the certificate renewal limits of the service provider.

Instead, centrally manage certificate options within the security.acme module; then point other services to security.acme with useACMEHost option.

security.acme.certs."example.org".extraDomainNames = [
  "syncplay.example.org"
  "reposilite.example.org"
  "site2.example.org"
];

services.syncplay.useACMEHost = "example.org";
services.reposilite.useACMEHost = "example.org";
services.nginx.virtualHosts."site2.example.org".useACMEHost = "example.org";


Using Let's Encrypt Staging

If you'd like to use the Let's Encrypt staging environment, eg for its less stringent rate limits, set

security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";

See also