Merge pull request #44556 from johanot/certmgr-module-init

nixos/certmgr: init

authored by Silvan Mosberger and committed by GitHub 1a3b9e1b cf627887

+344
+1
nixos/modules/module-list.nix
··· 623 ./services/search/hound.nix 624 ./services/search/kibana.nix 625 ./services/search/solr.nix 626 ./services/security/cfssl.nix 627 ./services/security/clamav.nix 628 ./services/security/fail2ban.nix
··· 623 ./services/search/hound.nix 624 ./services/search/kibana.nix 625 ./services/search/solr.nix 626 + ./services/security/certmgr.nix 627 ./services/security/cfssl.nix 628 ./services/security/clamav.nix 629 ./services/security/fail2ban.nix
+194
nixos/modules/services/security/certmgr.nix
···
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + 5 + let 6 + cfg = config.services.certmgr; 7 + 8 + specs = mapAttrsToList (n: v: rec { 9 + name = n + ".json"; 10 + path = if isAttrs v then pkgs.writeText name (builtins.toJSON v) else v; 11 + }) cfg.specs; 12 + 13 + allSpecs = pkgs.linkFarm "certmgr.d" specs; 14 + 15 + certmgrYaml = pkgs.writeText "certmgr.yaml" (builtins.toJSON { 16 + dir = allSpecs; 17 + default_remote = cfg.defaultRemote; 18 + svcmgr = cfg.svcManager; 19 + before = cfg.validMin; 20 + interval = cfg.renewInterval; 21 + inherit (cfg) metricsPort metricsAddress; 22 + }); 23 + 24 + specPaths = map dirOf (concatMap (spec: 25 + if isAttrs spec then 26 + collect isString (filterAttrsRecursive (n: v: isAttrs v || n == "path") spec) 27 + else 28 + [ spec ] 29 + ) (attrValues cfg.specs)); 30 + 31 + preStart = '' 32 + ${concatStringsSep " \\\n" (["mkdir -p"] ++ map escapeShellArg specPaths)} 33 + ${pkgs.certmgr}/bin/certmgr -f ${certmgrYaml} check 34 + ''; 35 + in 36 + { 37 + options.services.certmgr = { 38 + enable = mkEnableOption "certmgr"; 39 + 40 + defaultRemote = mkOption { 41 + type = types.str; 42 + default = "127.0.0.1:8888"; 43 + description = "The default CA host:port to use."; 44 + }; 45 + 46 + validMin = mkOption { 47 + default = "72h"; 48 + type = types.str; 49 + description = "The interval before a certificate expires to start attempting to renew it."; 50 + }; 51 + 52 + renewInterval = mkOption { 53 + default = "30m"; 54 + type = types.str; 55 + description = "How often to check certificate expirations and how often to update the cert_next_expires metric."; 56 + }; 57 + 58 + metricsAddress = mkOption { 59 + default = "127.0.0.1"; 60 + type = types.str; 61 + description = "The address for the Prometheus HTTP endpoint."; 62 + }; 63 + 64 + metricsPort = mkOption { 65 + default = 9488; 66 + type = types.ints.u16; 67 + description = "The port for the Prometheus HTTP endpoint."; 68 + }; 69 + 70 + specs = mkOption { 71 + default = {}; 72 + example = literalExample '' 73 + { 74 + exampleCert = 75 + let 76 + domain = "example.com"; 77 + secret = name: "/var/lib/secrets/''${name}.pem"; 78 + in { 79 + service = "nginx"; 80 + action = "reload"; 81 + authority = { 82 + file.path = secret "ca"; 83 + }; 84 + certificate = { 85 + path = secret domain; 86 + }; 87 + private_key = { 88 + owner = "root"; 89 + group = "root"; 90 + mode = "0600"; 91 + path = secret "''${domain}-key"; 92 + }; 93 + request = { 94 + CN = domain; 95 + hosts = [ "mail.''${domain}" "www.''${domain}" ]; 96 + key = { 97 + algo = "rsa"; 98 + size = 2048; 99 + }; 100 + names = { 101 + O = "Example Organization"; 102 + C = "USA"; 103 + }; 104 + }; 105 + }; 106 + otherCert = "/var/certmgr/specs/other-cert.json"; 107 + } 108 + ''; 109 + type = with types; attrsOf (either (submodule { 110 + options = { 111 + service = mkOption { 112 + type = nullOr str; 113 + default = null; 114 + description = "The service on which to perform &lt;action&gt; after fetching."; 115 + }; 116 + 117 + action = mkOption { 118 + type = addCheck str (x: cfg.svcManager == "command" || elem x ["restart" "reload" "nop"]); 119 + default = "nop"; 120 + description = "The action to take after fetching."; 121 + }; 122 + 123 + # These ought all to be specified according to certmgr spec def. 124 + authority = mkOption { 125 + type = attrs; 126 + description = "certmgr spec authority object."; 127 + }; 128 + 129 + certificate = mkOption { 130 + type = nullOr attrs; 131 + description = "certmgr spec certificate object."; 132 + }; 133 + 134 + private_key = mkOption { 135 + type = nullOr attrs; 136 + description = "certmgr spec private_key object."; 137 + }; 138 + 139 + request = mkOption { 140 + type = nullOr attrs; 141 + description = "certmgr spec request object."; 142 + }; 143 + }; 144 + }) path); 145 + description = '' 146 + Certificate specs as described by: 147 + <link xlink:href="https://github.com/cloudflare/certmgr#certificate-specs" /> 148 + These will be added to the Nix store, so they will be world readable. 149 + ''; 150 + }; 151 + 152 + svcManager = mkOption { 153 + default = "systemd"; 154 + type = types.enum [ "circus" "command" "dummy" "openrc" "systemd" "sysv" ]; 155 + description = '' 156 + This specifies the service manager to use for restarting or reloading services. 157 + See: <link xlink:href="https://github.com/cloudflare/certmgr#certmgryaml" />. 158 + For how to use the "command" service manager in particular, 159 + see: <link xlink:href="https://github.com/cloudflare/certmgr#command-svcmgr-and-how-to-use-it" />. 160 + ''; 161 + }; 162 + 163 + }; 164 + 165 + config = mkIf cfg.enable { 166 + assertions = [ 167 + { 168 + assertion = cfg.specs != {}; 169 + message = "Certmgr specs cannot be empty."; 170 + } 171 + { 172 + assertion = !any (hasAttrByPath [ "authority" "auth_key" ]) (attrValues cfg.specs); 173 + message = '' 174 + Inline services.certmgr.specs are added to the Nix store rendering them world readable. 175 + Specify paths as specs, if you want to use include auth_key - or use the auth_key_file option." 176 + ''; 177 + } 178 + ]; 179 + 180 + systemd.services.certmgr = { 181 + description = "certmgr"; 182 + path = mkIf (cfg.svcManager == "command") [ pkgs.bash ]; 183 + after = [ "network-online.target" ]; 184 + wantedBy = [ "multi-user.target" ]; 185 + inherit preStart; 186 + 187 + serviceConfig = { 188 + Restart = "always"; 189 + RestartSec = "10s"; 190 + ExecStart = "${pkgs.certmgr}/bin/certmgr -f ${certmgrYaml}"; 191 + }; 192 + }; 193 + }; 194 + }
+1
nixos/release.nix
··· 256 tests.buildbot = callTest tests/buildbot.nix {}; 257 tests.cadvisor = callTestOnMatchingSystems ["x86_64-linux"] tests/cadvisor.nix {}; 258 tests.ceph = callTestOnMatchingSystems ["x86_64-linux"] tests/ceph.nix {}; 259 tests.cfssl = callTestOnMatchingSystems ["x86_64-linux"] tests/cfssl.nix {}; 260 tests.chromium = (callSubTestsOnMatchingSystems ["x86_64-linux"] tests/chromium.nix {}).stable or {}; 261 tests.cjdns = callTest tests/cjdns.nix {};
··· 256 tests.buildbot = callTest tests/buildbot.nix {}; 257 tests.cadvisor = callTestOnMatchingSystems ["x86_64-linux"] tests/cadvisor.nix {}; 258 tests.ceph = callTestOnMatchingSystems ["x86_64-linux"] tests/ceph.nix {}; 259 + tests.certmgr = callSubTests tests/certmgr.nix {}; 260 tests.cfssl = callTestOnMatchingSystems ["x86_64-linux"] tests/cfssl.nix {}; 261 tests.chromium = (callSubTestsOnMatchingSystems ["x86_64-linux"] tests/chromium.nix {}).stable or {}; 262 tests.cjdns = callTest tests/cjdns.nix {};
+148
nixos/tests/certmgr.nix
···
··· 1 + { system ? builtins.currentSystem }: 2 + 3 + with import ../lib/testing.nix { inherit system; }; 4 + let 5 + mkSpec = { host, service ? null, action }: { 6 + inherit action; 7 + authority = { 8 + file = { 9 + group = "nobody"; 10 + owner = "nobody"; 11 + path = "/tmp/${host}-ca.pem"; 12 + }; 13 + label = "www_ca"; 14 + profile = "three-month"; 15 + remote = "localhost:8888"; 16 + }; 17 + certificate = { 18 + group = "nobody"; 19 + owner = "nobody"; 20 + path = "/tmp/${host}-cert.pem"; 21 + }; 22 + private_key = { 23 + group = "nobody"; 24 + mode = "0600"; 25 + owner = "nobody"; 26 + path = "/tmp/${host}-key.pem"; 27 + }; 28 + request = { 29 + CN = host; 30 + hosts = [ host "www.${host}" ]; 31 + key = { 32 + algo = "rsa"; 33 + size = 2048; 34 + }; 35 + names = [ 36 + { 37 + C = "US"; 38 + L = "San Francisco"; 39 + O = "Example, LLC"; 40 + ST = "CA"; 41 + } 42 + ]; 43 + }; 44 + inherit service; 45 + }; 46 + 47 + mkCertmgrTest = { svcManager, specs, testScript }: makeTest { 48 + name = "certmgr-" + svcManager; 49 + nodes = { 50 + machine = { config, lib, pkgs, ... }: { 51 + networking.firewall.allowedTCPPorts = with config.services; [ cfssl.port certmgr.metricsPort ]; 52 + networking.extraHosts = "127.0.0.1 imp.example.org decl.example.org"; 53 + 54 + services.cfssl.enable = true; 55 + systemd.services.cfssl.after = [ "cfssl-init.service" "networking.target" ]; 56 + 57 + systemd.services.cfssl-init = { 58 + description = "Initialize the cfssl CA"; 59 + wantedBy = [ "multi-user.target" ]; 60 + serviceConfig = { 61 + User = "cfssl"; 62 + Type = "oneshot"; 63 + WorkingDirectory = config.services.cfssl.dataDir; 64 + }; 65 + script = '' 66 + ${pkgs.cfssl}/bin/cfssl genkey -initca ${pkgs.writeText "ca.json" (builtins.toJSON { 67 + hosts = [ "ca.example.com" ]; 68 + key = { 69 + algo = "rsa"; size = 4096; }; 70 + names = [ 71 + { 72 + C = "US"; 73 + L = "San Francisco"; 74 + O = "Internet Widgets, LLC"; 75 + OU = "Certificate Authority"; 76 + ST = "California"; 77 + } 78 + ]; 79 + })} | ${pkgs.cfssl}/bin/cfssljson -bare ca 80 + ''; 81 + }; 82 + 83 + services.nginx = { 84 + enable = true; 85 + virtualHosts = lib.mkMerge (map (host: { 86 + ${host} = { 87 + sslCertificate = "/tmp/${host}-cert.pem"; 88 + sslCertificateKey = "/tmp/${host}-key.pem"; 89 + extraConfig = '' 90 + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 91 + ''; 92 + onlySSL = true; 93 + serverName = host; 94 + root = pkgs.writeTextDir "index.html" "It works!"; 95 + }; 96 + }) [ "imp.example.org" "decl.example.org" ]); 97 + }; 98 + 99 + systemd.services.nginx.wantedBy = lib.mkForce []; 100 + 101 + systemd.services.certmgr.after = [ "cfssl.service" ]; 102 + services.certmgr = { 103 + enable = true; 104 + inherit svcManager; 105 + inherit specs; 106 + }; 107 + 108 + }; 109 + }; 110 + inherit testScript; 111 + }; 112 + in 113 + { 114 + systemd = mkCertmgrTest { 115 + svcManager = "systemd"; 116 + specs = { 117 + decl = mkSpec { host = "decl.example.org"; service = "nginx"; action ="restart"; }; 118 + imp = toString (pkgs.writeText "test.json" (builtins.toJSON ( 119 + mkSpec { host = "imp.example.org"; service = "nginx"; action = "restart"; } 120 + ))); 121 + }; 122 + testScript = '' 123 + $machine->waitForUnit('cfssl.service'); 124 + $machine->waitUntilSucceeds('ls /tmp/decl.example.org-ca.pem'); 125 + $machine->waitUntilSucceeds('ls /tmp/decl.example.org-key.pem'); 126 + $machine->waitUntilSucceeds('ls /tmp/decl.example.org-cert.pem'); 127 + $machine->waitUntilSucceeds('ls /tmp/imp.example.org-ca.pem'); 128 + $machine->waitUntilSucceeds('ls /tmp/imp.example.org-key.pem'); 129 + $machine->waitUntilSucceeds('ls /tmp/imp.example.org-cert.pem'); 130 + $machine->waitForUnit('nginx.service'); 131 + $machine->succeed('[ "1" -lt "$(journalctl -u nginx | grep "Starting Nginx" | wc -l)" ]'); 132 + $machine->succeed('curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org'); 133 + $machine->succeed('curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org'); 134 + ''; 135 + }; 136 + 137 + command = mkCertmgrTest { 138 + svcManager = "command"; 139 + specs = { 140 + test = mkSpec { host = "command.example.org"; action = "touch /tmp/command.executed"; }; 141 + }; 142 + testScript = '' 143 + $machine->waitForUnit('cfssl.service'); 144 + $machine->waitUntilSucceeds('stat /tmp/command.executed'); 145 + ''; 146 + }; 147 + 148 + }