diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index 21e92469a38e..b3cb945e868b 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -20,6 +20,16 @@ let mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}"; accountDirRoot = "/var/lib/acme/.lego/accounts/"; + isIP = + address: + let + isIPv6 = lib.length (lib.splitString ":" address) > 2; + isIPv4 = (lib.match "^([0-9]+\\.){3}[0-9]+$" address) != null; + in + isIPv6 || isIPv4; + + isDomainName = name: !isIP name; + # Lockdir is acme-setup.service's RuntimeDirectory. # Since that service is a oneshot with RemainAfterExit, # the folder will exist during all renewal services. @@ -389,10 +399,14 @@ let exit 0 fi - minica \ - --ca-key ca/key.pem \ - --ca-cert ca/cert.pem \ - --domains ${lib.escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))} + minica ${ + lib.cli.toCommandLineShellGNU { } { + ca-key = "ca/key.pem"; + ca-cert = "ca/cert.pem"; + domains = lib.concatStringsSep "," (lib.filter isDomainName ([ data.domain ] ++ extraDomains)); + ip-addresses = lib.concatStringsSep "," (lib.filter isIP ([ data.domain ] ++ extraDomains)); + } + } # Create files to match directory layout for real certificates ( diff --git a/nixos/tests/acme/http01-builtin.nix b/nixos/tests/acme/http01-builtin.nix index a82ef6f5652a..fce54470941f 100644 --- a/nixos/tests/acme/http01-builtin.nix +++ b/nixos/tests/acme/http01-builtin.nix @@ -1,11 +1,11 @@ { - config, lib, pkgs, ... }: let domain = "example.test"; + ip = "192.168.1.2"; in { name = "http01-builtin"; @@ -34,6 +34,7 @@ in environment.systemPackages = [ pkgs.openssl ]; security.acme.certs."${config.networking.fqdn}" = { + extraDomainNames = [ ip ]; listenHTTP = ":80"; }; @@ -124,6 +125,7 @@ in [ alt_names ] DNS.1 = ${config.networking.fqdn} + IP.1 = ${ip} ''; csrData = pkgs.runCommand "csr-and-key" @@ -142,6 +144,7 @@ in security.acme.certs."${config.networking.fqdn}" = { csr = "${csrData}/request.csr"; csrKey = "${csrData}/key.pem"; + extraDomainNames = lib.mkForce [ ]; }; }; }; @@ -158,6 +161,7 @@ in ${(import ./utils.nix).pythonUtils} domain = "${domain}" + ip = "${ip}" cert = "${certName}" cert2 = "builtin-2." + domain cert3 = "builtin-3." + domain @@ -173,6 +177,7 @@ in check_issuer(builtin, cert, "pebble") check_domain(builtin, cert, cert) + check_ip(builtin, cert, ip) with subtest("Validate permissions"): check_permissions(builtin, cert, "acme") @@ -300,6 +305,7 @@ in builtin.wait_for_unit("renew-triggered.target") check_issuer(builtin, cert, "pebble") + check_ip(builtin, cert, ip) with subtest("Generate self-signed certs"): acme.shutdown() diff --git a/nixos/tests/acme/python-utils.py b/nixos/tests/acme/python-utils.py index d542324084aa..304e4f2e3a68 100644 --- a/nixos/tests/acme/python-utils.py +++ b/nixos/tests/acme/python-utils.py @@ -84,6 +84,11 @@ def check_domain(node, cert_name, domain, fail=False) -> None: cmd = f"openssl x509 -noout -checkhost '{domain}' -in /var/lib/acme/{cert_name}/cert.pem" run(node, cmd, fail=fail) +# Ensures the provided ip address matches with the given cert +def check_ip(node, cert_name, ip_address, fail=False) -> None: + cmd = f"openssl x509 -noout -checkip '{ip_address}' -in /var/lib/acme/{cert_name}/cert.pem" + run(node, cmd, fail=fail) + # Ensures the required values for OCSP stapling are present # Pebble doesn't provide a full OCSP responder, so just checks the URL def check_stapling(node, cert_name, ca_domain, fail=False):