parent
6820e2f0dd
commit
004e7fb6fd
@ -0,0 +1,194 @@ |
||||
{ config, lib, pkgs, ... }: |
||||
|
||||
with lib; |
||||
|
||||
let |
||||
cfg = config.services.certmgr; |
||||
|
||||
specs = mapAttrsToList (n: v: rec { |
||||
name = n + ".json"; |
||||
path = if isAttrs v then pkgs.writeText name (builtins.toJSON v) else v; |
||||
}) cfg.specs; |
||||
|
||||
allSpecs = pkgs.linkFarm "certmgr.d" specs; |
||||
|
||||
certmgrYaml = pkgs.writeText "certmgr.yaml" (builtins.toJSON { |
||||
dir = allSpecs; |
||||
default_remote = cfg.defaultRemote; |
||||
svcmgr = cfg.svcManager; |
||||
before = cfg.validMin; |
||||
interval = cfg.renewInterval; |
||||
inherit (cfg) metricsPort metricsAddress; |
||||
}); |
||||
|
||||
specPaths = map dirOf (concatMap (spec: |
||||
if isAttrs spec then |
||||
collect isString (filterAttrsRecursive (n: v: isAttrs v || n == "path") spec) |
||||
else |
||||
[ spec ] |
||||
) (attrValues cfg.specs)); |
||||
|
||||
preStart = '' |
||||
${concatStringsSep " \\\n" (["mkdir -p"] ++ map escapeShellArg specPaths)} |
||||
${pkgs.certmgr}/bin/certmgr -f ${certmgrYaml} check |
||||
''; |
||||
in |
||||
{ |
||||
options.services.certmgr = { |
||||
enable = mkEnableOption "certmgr"; |
||||
|
||||
defaultRemote = mkOption { |
||||
type = types.str; |
||||
default = "127.0.0.1:8888"; |
||||
description = "The default CA host:port to use."; |
||||
}; |
||||
|
||||
validMin = mkOption { |
||||
default = "72h"; |
||||
type = types.str; |
||||
description = "The interval before a certificate expires to start attempting to renew it."; |
||||
}; |
||||
|
||||
renewInterval = mkOption { |
||||
default = "30m"; |
||||
type = types.str; |
||||
description = "How often to check certificate expirations and how often to update the cert_next_expires metric."; |
||||
}; |
||||
|
||||
metricsAddress = mkOption { |
||||
default = "127.0.0.1"; |
||||
type = types.str; |
||||
description = "The address for the Prometheus HTTP endpoint."; |
||||
}; |
||||
|
||||
metricsPort = mkOption { |
||||
default = 9488; |
||||
type = types.ints.u16; |
||||
description = "The port for the Prometheus HTTP endpoint."; |
||||
}; |
||||
|
||||
specs = mkOption { |
||||
default = {}; |
||||
example = literalExample '' |
||||
{ |
||||
exampleCert = |
||||
let |
||||
domain = "example.com"; |
||||
secret = name: "/var/lib/secrets/''${name}.pem"; |
||||
in { |
||||
service = "nginx"; |
||||
action = "reload"; |
||||
authority = { |
||||
file.path = secret "ca"; |
||||
}; |
||||
certificate = { |
||||
path = secret domain; |
||||
}; |
||||
private_key = { |
||||
owner = "root"; |
||||
group = "root"; |
||||
mode = "0600"; |
||||
path = secret "''${domain}-key"; |
||||
}; |
||||
request = { |
||||
CN = domain; |
||||
hosts = [ "mail.''${domain}" "www.''${domain}" ]; |
||||
key = { |
||||
algo = "rsa"; |
||||
size = 2048; |
||||
}; |
||||
names = { |
||||
O = "Example Organization"; |
||||
C = "USA"; |
||||
}; |
||||
}; |
||||
}; |
||||
otherCert = "/var/certmgr/specs/other-cert.json"; |
||||
} |
||||
''; |
||||
type = with types; attrsOf (either (submodule { |
||||
options = { |
||||
service = mkOption { |
||||
type = nullOr str; |
||||
default = null; |
||||
description = "The service on which to perform <action> after fetching."; |
||||
}; |
||||
|
||||
action = mkOption { |
||||
type = addCheck str (x: cfg.svcManager == "command" || elem x ["restart" "reload" "nop"]); |
||||
default = "nop"; |
||||
description = "The action to take after fetching."; |
||||
}; |
||||
|
||||
# These ought all to be specified according to certmgr spec def. |
||||
authority = mkOption { |
||||
type = attrs; |
||||
description = "certmgr spec authority object."; |
||||
}; |
||||
|
||||
certificate = mkOption { |
||||
type = nullOr attrs; |
||||
description = "certmgr spec certificate object."; |
||||
}; |
||||
|
||||
private_key = mkOption { |
||||
type = nullOr attrs; |
||||
description = "certmgr spec private_key object."; |
||||
}; |
||||
|
||||
request = mkOption { |
||||
type = nullOr attrs; |
||||
description = "certmgr spec request object."; |
||||
}; |
||||
}; |
||||
}) path); |
||||
description = '' |
||||
Certificate specs as described by: |
||||
<link xlink:href="https://github.com/cloudflare/certmgr#certificate-specs" /> |
||||
These will be added to the Nix store, so they will be world readable. |
||||
''; |
||||
}; |
||||
|
||||
svcManager = mkOption { |
||||
default = "systemd"; |
||||
type = types.enum [ "circus" "command" "dummy" "openrc" "systemd" "sysv" ]; |
||||
description = '' |
||||
This specifies the service manager to use for restarting or reloading services. |
||||
See: <link xlink:href="https://github.com/cloudflare/certmgr#certmgryaml" />. |
||||
For how to use the "command" service manager in particular, |
||||
see: <link xlink:href="https://github.com/cloudflare/certmgr#command-svcmgr-and-how-to-use-it" />. |
||||
''; |
||||
}; |
||||
|
||||
}; |
||||
|
||||
config = mkIf cfg.enable { |
||||
assertions = [ |
||||
{ |
||||
assertion = cfg.specs != {}; |
||||
message = "Certmgr specs cannot be empty."; |
||||
} |
||||
{ |
||||
assertion = !any (hasAttrByPath [ "authority" "auth_key" ]) (attrValues cfg.specs); |
||||
message = '' |
||||
Inline services.certmgr.specs are added to the Nix store rendering them world readable. |
||||
Specify paths as specs, if you want to use include auth_key - or use the auth_key_file option." |
||||
''; |
||||
} |
||||
]; |
||||
|
||||
systemd.services.certmgr = { |
||||
description = "certmgr"; |
||||
path = mkIf (cfg.svcManager == "command") [ pkgs.bash ]; |
||||
after = [ "network-online.target" ]; |
||||
wantedBy = [ "multi-user.target" ]; |
||||
inherit preStart; |
||||
|
||||
serviceConfig = { |
||||
Restart = "always"; |
||||
RestartSec = "10s"; |
||||
ExecStart = "${pkgs.certmgr}/bin/certmgr -f ${certmgrYaml}"; |
||||
}; |
||||
}; |
||||
}; |
||||
} |
@ -0,0 +1,148 @@ |
||||
{ system ? builtins.currentSystem }: |
||||
|
||||
with import ../lib/testing.nix { inherit system; }; |
||||
let |
||||
mkSpec = { host, service ? null, action }: { |
||||
inherit action; |
||||
authority = { |
||||
file = { |
||||
group = "nobody"; |
||||
owner = "nobody"; |
||||
path = "/tmp/${host}-ca.pem"; |
||||
}; |
||||
label = "www_ca"; |
||||
profile = "three-month"; |
||||
remote = "localhost:8888"; |
||||
}; |
||||
certificate = { |
||||
group = "nobody"; |
||||
owner = "nobody"; |
||||
path = "/tmp/${host}-cert.pem"; |
||||
}; |
||||
private_key = { |
||||
group = "nobody"; |
||||
mode = "0600"; |
||||
owner = "nobody"; |
||||
path = "/tmp/${host}-key.pem"; |
||||
}; |
||||
request = { |
||||
CN = host; |
||||
hosts = [ host "www.${host}" ]; |
||||
key = { |
||||
algo = "rsa"; |
||||
size = 2048; |
||||
}; |
||||
names = [ |
||||
{ |
||||
C = "US"; |
||||
L = "San Francisco"; |
||||
O = "Example, LLC"; |
||||
ST = "CA"; |
||||
} |
||||
]; |
||||
}; |
||||
inherit service; |
||||
}; |
||||
|
||||
mkCertmgrTest = { svcManager, specs, testScript }: makeTest { |
||||
name = "certmgr-" + svcManager; |
||||
nodes = { |
||||
machine = { config, lib, pkgs, ... }: { |
||||
networking.firewall.allowedTCPPorts = with config.services; [ cfssl.port certmgr.metricsPort ]; |
||||
networking.extraHosts = "127.0.0.1 imp.example.org decl.example.org"; |
||||
|
||||
services.cfssl.enable = true; |
||||
systemd.services.cfssl.after = [ "cfssl-init.service" "networking.target" ]; |
||||
|
||||
systemd.services.cfssl-init = { |
||||
description = "Initialize the cfssl CA"; |
||||
wantedBy = [ "multi-user.target" ]; |
||||
serviceConfig = { |
||||
User = "cfssl"; |
||||
Type = "oneshot"; |
||||
WorkingDirectory = config.services.cfssl.dataDir; |
||||
}; |
||||
script = '' |
||||
${pkgs.cfssl}/bin/cfssl genkey -initca ${pkgs.writeText "ca.json" (builtins.toJSON { |
||||
hosts = [ "ca.example.com" ]; |
||||
key = { |
||||
algo = "rsa"; size = 4096; }; |
||||
names = [ |
||||
{ |
||||
C = "US"; |
||||
L = "San Francisco"; |
||||
O = "Internet Widgets, LLC"; |
||||
OU = "Certificate Authority"; |
||||
ST = "California"; |
||||
} |
||||
]; |
||||
})} | ${pkgs.cfssl}/bin/cfssljson -bare ca |
||||
''; |
||||
}; |
||||
|
||||
services.nginx = { |
||||
enable = true; |
||||
virtualHosts = lib.mkMerge (map (host: { |
||||
${host} = { |
||||
sslCertificate = "/tmp/${host}-cert.pem"; |
||||
sslCertificateKey = "/tmp/${host}-key.pem"; |
||||
extraConfig = '' |
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; |
||||
''; |
||||
onlySSL = true; |
||||
serverName = host; |
||||
root = pkgs.writeTextDir "index.html" "It works!"; |
||||
}; |
||||
}) [ "imp.example.org" "decl.example.org" ]); |
||||
}; |
||||
|
||||
systemd.services.nginx.wantedBy = lib.mkForce []; |
||||
|
||||
systemd.services.certmgr.after = [ "cfssl.service" ]; |
||||
services.certmgr = { |
||||
enable = true; |
||||
inherit svcManager; |
||||
inherit specs; |
||||
}; |
||||
|
||||
}; |
||||
}; |
||||
inherit testScript; |
||||
}; |
||||
in |
||||
{ |
||||
systemd = mkCertmgrTest { |
||||
svcManager = "systemd"; |
||||
specs = { |
||||
decl = mkSpec { host = "decl.example.org"; service = "nginx"; action ="restart"; }; |
||||
imp = toString (pkgs.writeText "test.json" (builtins.toJSON ( |
||||
mkSpec { host = "imp.example.org"; service = "nginx"; action = "restart"; } |
||||
))); |
||||
}; |
||||
testScript = '' |
||||
$machine->waitForUnit('cfssl.service'); |
||||
$machine->waitUntilSucceeds('ls /tmp/decl.example.org-ca.pem'); |
||||
$machine->waitUntilSucceeds('ls /tmp/decl.example.org-key.pem'); |
||||
$machine->waitUntilSucceeds('ls /tmp/decl.example.org-cert.pem'); |
||||
$machine->waitUntilSucceeds('ls /tmp/imp.example.org-ca.pem'); |
||||
$machine->waitUntilSucceeds('ls /tmp/imp.example.org-key.pem'); |
||||
$machine->waitUntilSucceeds('ls /tmp/imp.example.org-cert.pem'); |
||||
$machine->waitForUnit('nginx.service'); |
||||
$machine->succeed('[ "1" -lt "$(journalctl -u nginx | grep "Starting Nginx" | wc -l)" ]'); |
||||
$machine->succeed('curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org'); |
||||
$machine->succeed('curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org'); |
||||
''; |
||||
}; |
||||
|
||||
command = mkCertmgrTest { |
||||
svcManager = "command"; |
||||
specs = { |
||||
test = mkSpec { host = "command.example.org"; action = "touch /tmp/command.executed"; }; |
||||
}; |
||||
testScript = '' |
||||
$machine->waitForUnit('cfssl.service'); |
||||
$machine->waitUntilSucceeds('stat /tmp/command.executed'); |
||||
''; |
||||
}; |
||||
|
||||
} |
Loading…
Reference in new issue