diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix index 782f6c8f69df..f73660ed99de 100644 --- a/nixos/modules/misc/ids.nix +++ b/nixos/modules/misc/ids.nix @@ -323,6 +323,7 @@ mapred = 296; hadoop = 297; hydron = 298; + cfssl = 299; # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399! @@ -606,6 +607,7 @@ mapred = 296; hadoop = 297; hydron = 298; + cfssl = 299; # When adding a gid, make sure it doesn't match an existing # uid. Users and groups with the same name should have equal diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f5d94baf173c..5f96336de672 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -622,6 +622,7 @@ ./services/search/hound.nix ./services/search/kibana.nix ./services/search/solr.nix + ./services/security/cfssl.nix ./services/security/clamav.nix ./services/security/fail2ban.nix ./services/security/fprintd.nix diff --git a/nixos/modules/services/security/cfssl.nix b/nixos/modules/services/security/cfssl.nix new file mode 100644 index 000000000000..1eb2f65ba602 --- /dev/null +++ b/nixos/modules/services/security/cfssl.nix @@ -0,0 +1,209 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.cfssl; +in { + options.services.cfssl = { + enable = mkEnableOption "the CFSSL CA api-server"; + + dataDir = mkOption { + default = "/var/lib/cfssl"; + type = types.path; + description = "Cfssl work directory."; + }; + + address = mkOption { + default = "127.0.0.1"; + type = types.str; + description = "Address to bind."; + }; + + port = mkOption { + default = 8888; + type = types.ints.u16; + description = "Port to bind."; + }; + + ca = mkOption { + defaultText = "\${cfg.dataDir}/ca.pem"; + type = types.str; + description = "CA used to sign the new certificate -- accepts '[file:]fname' or 'env:varname'."; + }; + + caKey = mkOption { + defaultText = "file:\${cfg.dataDir}/ca-key.pem"; + type = types.str; + description = "CA private key -- accepts '[file:]fname' or 'env:varname'."; + }; + + caBundle = mkOption { + default = null; + type = types.nullOr types.path; + description = "Path to root certificate store."; + }; + + intBundle = mkOption { + default = null; + type = types.nullOr types.path; + description = "Path to intermediate certificate store."; + }; + + intDir = mkOption { + default = null; + type = types.nullOr types.path; + description = "Intermediates directory."; + }; + + metadata = mkOption { + default = null; + type = types.nullOr types.path; + description = '' + Metadata file for root certificate presence. + The content of the file is a json dictionary (k,v): each key k is + a SHA-1 digest of a root certificate while value v is a list of key + store filenames. + ''; + }; + + remote = mkOption { + default = null; + type = types.nullOr types.str; + description = "Remote CFSSL server."; + }; + + configFile = mkOption { + default = null; + type = types.nullOr types.str; + description = "Path to configuration file. Do not put this in nix-store as it might contain secrets."; + }; + + responder = mkOption { + default = null; + type = types.nullOr types.path; + description = "Certificate for OCSP responder."; + }; + + responderKey = mkOption { + default = null; + type = types.nullOr types.str; + description = "Private key for OCSP responder certificate. Do not put this in nix-store."; + }; + + tlsKey = mkOption { + default = null; + type = types.nullOr types.str; + description = "Other endpoint's CA private key. Do not put this in nix-store."; + }; + + tlsCert = mkOption { + default = null; + type = types.nullOr types.path; + description = "Other endpoint's CA to set up TLS protocol."; + }; + + mutualTlsCa = mkOption { + default = null; + type = types.nullOr types.path; + description = "Mutual TLS - require clients be signed by this CA."; + }; + + mutualTlsCn = mkOption { + default = null; + type = types.nullOr types.str; + description = "Mutual TLS - regex for whitelist of allowed client CNs."; + }; + + tlsRemoteCa = mkOption { + default = null; + type = types.nullOr types.path; + description = "CAs to trust for remote TLS requests."; + }; + + mutualTlsClientCert = mkOption { + default = null; + type = types.nullOr types.path; + description = "Mutual TLS - client certificate to call remote instance requiring client certs."; + }; + + mutualTlsClientKey = mkOption { + default = null; + type = types.nullOr types.path; + description = "Mutual TLS - client key to call remote instance requiring client certs. Do not put this in nix-store."; + }; + + dbConfig = mkOption { + default = null; + type = types.nullOr types.path; + description = "Certificate db configuration file. Path must be writeable."; + }; + + logLevel = mkOption { + default = 1; + type = types.enum [ 0 1 2 3 4 5 ]; + description = "Log level (0 = DEBUG, 5 = FATAL)."; + }; + }; + + config = { + users.extraGroups.cfssl = { + gid = config.ids.gids.cfssl; + }; + + users.extraUsers.cfssl = { + description = "cfssl user"; + createHome = true; + home = cfg.dataDir; + group = "cfssl"; + uid = config.ids.uids.cfssl; + }; + + systemd.services.cfssl = mkIf cfg.enable { + description = "CFSSL CA API server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + WorkingDirectory = cfg.dataDir; + StateDirectory = cfg.dataDir; + StateDirectoryMode = 700; + Restart = "always"; + User = "cfssl"; + + ExecStart = with cfg; let + opt = n: v: optionalString (v != null) ''-${n}="${v}"''; + in + lib.concatStringsSep " \\\n" [ + "${pkgs.cfssl}/bin/cfssl serve" + (opt "address" address) + (opt "port" (toString port)) + (opt "ca" ca) + (opt "ca-key" caKey) + (opt "ca-bundle" caBundle) + (opt "int-bundle" intBundle) + (opt "int-dir" intDir) + (opt "metadata" metadata) + (opt "remote" remote) + (opt "config" configFile) + (opt "responder" responder) + (opt "responder-key" responderKey) + (opt "tls-key" tlsKey) + (opt "tls-cert" tlsCert) + (opt "mutual-tls-ca" mutualTlsCa) + (opt "mutual-tls-cn" mutualTlsCn) + (opt "mutual-tls-client-key" mutualTlsClientKey) + (opt "mutual-tls-client-cert" mutualTlsClientCert) + (opt "tls-remote-ca" tlsRemoteCa) + (opt "db-config" dbConfig) + (opt "loglevel" (toString logLevel)) + ]; + }; + }; + + services.cfssl = { + ca = mkDefault "${cfg.dataDir}/ca.pem"; + caKey = mkDefault "${cfg.dataDir}/ca-key.pem"; + }; + }; +} diff --git a/nixos/release.nix b/nixos/release.nix index 413f2bec54c7..007859259b17 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -256,6 +256,7 @@ in rec { tests.buildbot = callTest tests/buildbot.nix {}; tests.cadvisor = callTestOnMatchingSystems ["x86_64-linux"] tests/cadvisor.nix {}; tests.ceph = callTestOnMatchingSystems ["x86_64-linux"] tests/ceph.nix {}; + tests.cfssl = callTestOnMatchingSystems ["x86_64-linux"] tests/cfssl.nix {}; tests.chromium = (callSubTestsOnMatchingSystems ["x86_64-linux"] tests/chromium.nix {}).stable or {}; tests.cjdns = callTest tests/cjdns.nix {}; tests.cloud-init = callTest tests/cloud-init.nix {}; diff --git a/nixos/tests/cfssl.nix b/nixos/tests/cfssl.nix new file mode 100644 index 000000000000..513ed8c45741 --- /dev/null +++ b/nixos/tests/cfssl.nix @@ -0,0 +1,67 @@ +import ./make-test.nix ({ pkgs, ...} : { + name = "cfssl"; + + machine = { config, lib, pkgs, ... }: + { + networking.firewall.allowedTCPPorts = [ config.services.cfssl.port ]; + + services.cfssl.enable = true; + systemd.services.cfssl.after = [ "cfssl-init.service" ]; + + systemd.services.cfssl-init = { + description = "Initialize the cfssl CA"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "cfssl"; + Type = "oneshot"; + WorkingDirectory = config.services.cfssl.dataDir; + }; + script = with 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"; + } + ]; + })} | ${cfssl}/bin/cfssljson -bare ca + ''; + }; + }; + + testScript = + let + cfsslrequest = with pkgs; writeScript "cfsslrequest" '' + curl -X POST -H "Content-Type: application/json" -d @${csr} \ + http://localhost:8888/api/v1/cfssl/newkey | ${cfssl}/bin/cfssljson /tmp/certificate + ''; + csr = pkgs.writeText "csr.json" (builtins.toJSON { + CN = "www.example.com"; + hosts = [ "example.com" "www.example.com" ]; + key = { + algo = "rsa"; + size = 2048; + }; + names = [ + { + C = "US"; + L = "San Francisco"; + O = "Example Company, LLC"; + OU = "Operations"; + ST = "California"; + } + ]; + }); + in + '' + $machine->waitForUnit('cfssl.service'); + $machine->waitUntilSucceeds('${cfsslrequest}'); + $machine->succeed('ls /tmp/certificate-key.pem'); + ''; +})