nixos/certmgr: init
This commit is contained in:
parent
6820e2f0dd
commit
004e7fb6fd
@ -622,6 +622,7 @@
|
|||||||
./services/search/hound.nix
|
./services/search/hound.nix
|
||||||
./services/search/kibana.nix
|
./services/search/kibana.nix
|
||||||
./services/search/solr.nix
|
./services/search/solr.nix
|
||||||
|
./services/security/certmgr.nix
|
||||||
./services/security/cfssl.nix
|
./services/security/cfssl.nix
|
||||||
./services/security/clamav.nix
|
./services/security/clamav.nix
|
||||||
./services/security/fail2ban.nix
|
./services/security/fail2ban.nix
|
||||||
|
194
nixos/modules/services/security/certmgr.nix
Normal file
194
nixos/modules/services/security/certmgr.nix
Normal file
@ -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}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -256,6 +256,7 @@ in rec {
|
|||||||
tests.buildbot = callTest tests/buildbot.nix {};
|
tests.buildbot = callTest tests/buildbot.nix {};
|
||||||
tests.cadvisor = callTestOnMatchingSystems ["x86_64-linux"] tests/cadvisor.nix {};
|
tests.cadvisor = callTestOnMatchingSystems ["x86_64-linux"] tests/cadvisor.nix {};
|
||||||
tests.ceph = callTestOnMatchingSystems ["x86_64-linux"] tests/ceph.nix {};
|
tests.ceph = callTestOnMatchingSystems ["x86_64-linux"] tests/ceph.nix {};
|
||||||
|
tests.certmgr = callSubTests tests/certmgr.nix {};
|
||||||
tests.cfssl = callTestOnMatchingSystems ["x86_64-linux"] tests/cfssl.nix {};
|
tests.cfssl = callTestOnMatchingSystems ["x86_64-linux"] tests/cfssl.nix {};
|
||||||
tests.chromium = (callSubTestsOnMatchingSystems ["x86_64-linux"] tests/chromium.nix {}).stable or {};
|
tests.chromium = (callSubTestsOnMatchingSystems ["x86_64-linux"] tests/chromium.nix {}).stable or {};
|
||||||
tests.cjdns = callTest tests/cjdns.nix {};
|
tests.cjdns = callTest tests/cjdns.nix {};
|
||||||
|
148
nixos/tests/certmgr.nix
Normal file
148
nixos/tests/certmgr.nix
Normal file
@ -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
Block a user