{ ... }: { _class = "clan.service"; manifest.name = "certificates"; manifest.description = "Sets up a PKI certificate chain using step-ca"; manifest.categories = [ "Network" ]; manifest.readme = builtins.readFile ./README.md; roles.ca = { description = "A certificate authority that issues and signs certificates for other machines."; interface = { lib, ... }: { options.acmeEmail = lib.mkOption { type = lib.types.str; default = "none@none.tld"; description = '' Email address for account creation and correspondence from the CA. It is recommended to use the same email for all certs to avoid account creation limits. ''; }; options.tlds = lib.mkOption { type = lib.types.listOf lib.types.str; description = "Top level domain for this CA. Certificates will be issued and trusted for *."; }; options.expire = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "When the certificate should expire."; default = "8760h"; example = "8760h"; }; }; perInstance = { settings, ... }: { nixosModule = { config, pkgs, lib, ... }: let domains = map (tld: "ca.${tld}") settings.tlds; in { security.acme.defaults.email = settings.acmeEmail; security.acme = { certs = builtins.listToAttrs ( map (domain: { name = domain; value = { server = "https://${domain}:1443/acme/acme/directory"; }; }) domains ); }; networking.firewall.allowedTCPPorts = [ 80 443 ]; services.nginx = { enable = true; recommendedProxySettings = true; virtualHosts = builtins.listToAttrs ( map (domain: { name = domain; value = { addSSL = true; enableACME = true; locations."/".proxyPass = "https://localhost:1443"; locations."= /ca.crt".alias = config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path; }; }) domains ); }; clan.core.vars.generators = { # Intermediate key generator "step-intermediate-key" = { files."intermediate.key" = { secret = true; deploy = true; owner = "step-ca"; group = "step-ca"; }; runtimeInputs = [ pkgs.step-cli ]; script = '' step crypto keypair --kty EC --curve P-256 --no-password --insecure $out/intermediate.pub $out/intermediate.key ''; }; # Intermediate certificate generator "step-intermediate-cert" = { files."intermediate.crt".secret = false; dependencies = [ "step-ca" "step-intermediate-key" ]; runtimeInputs = [ pkgs.step-cli ]; script = '' # Create intermediate certificate step certificate create \ --ca $in/step-ca/ca.crt \ --ca-key $in/step-ca/ca.key \ --ca-password-file /dev/null \ --key $in/step-intermediate-key/intermediate.key \ --template ${pkgs.writeText "intermediate.tmpl" '' { "subject": {{ toJson .Subject }}, "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, "maxPathLen": 0 }, "nameConstraints": { "critical": true, "permittedDNSDomains": [${ (lib.strings.concatStringsSep "," (map (tld: ''"${tld}"'') settings.tlds)) }] } } ''} ${lib.optionalString (settings.expire != null) "--not-after ${settings.expire}"} \ --not-before=-12h \ --no-password --insecure \ "Clan Intermediate CA" \ $out/intermediate.crt ''; }; }; services.step-ca = { enable = true; intermediatePasswordFile = "/dev/null"; address = "0.0.0.0"; port = 1443; settings = { root = config.clan.core.vars.generators.step-ca.files."ca.crt".path; crt = config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path; key = config.clan.core.vars.generators.step-intermediate-key.files."intermediate.key".path; dnsNames = domains; logger.format = "text"; db = { type = "badger"; dataSource = "/var/lib/step-ca/db"; }; authority = { provisioners = [ { type = "ACME"; name = "acme"; forceCN = true; } ]; claims = { maxTLSCertDuration = "2160h"; defaultTLSCertDuration = "2160h"; }; backdate = "1m0s"; }; tls = { cipherSuites = [ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" ]; minVersion = 1.2; maxVersion = 1.3; renegotiation = false; }; }; }; }; }; }; # Empty role, so we can add non-ca machins to the instance to trust the CA roles.default = { description = "A machine that trusts the CA and can get certificates issued by it."; interface = { lib, ... }: { options.acmeEmail = lib.mkOption { type = lib.types.str; default = "none@none.tld"; description = '' Email address for account creation and correspondence from the CA. It is recommended to use the same email for all certs to avoid account creation limits. ''; }; }; perInstance = { settings, ... }: { nixosModule.security.acme.defaults.email = settings.acmeEmail; }; }; # All machines (independent of role) will trust the CA perMachine.nixosModule = { pkgs, config, ... }: { # Root CA generator clan.core.vars.generators = { "step-ca" = { share = true; files."ca.key" = { secret = true; deploy = false; }; files."ca.crt".secret = false; runtimeInputs = [ pkgs.step-cli ]; script = '' step certificate create --template ${pkgs.writeText "root.tmpl" '' { "subject": {{ toJson .Subject }}, "issuer": {{ toJson .Subject }}, "keyUsage": ["certSign", "crlSign"], "basicConstraints": { "isCA": true, "maxPathLen": 1 } } ''} "Clan Root CA" $out/ca.crt $out/ca.key \ --kty EC --curve P-256 \ --not-after=8760h \ --not-before=-12h \ --no-password --insecure ''; }; }; security.pki.certificateFiles = [ config.clan.core.vars.generators."step-ca".files."ca.crt".path ]; environment.systemPackages = [ pkgs.openssl ]; security.acme.acceptTerms = true; }; }