1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
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 '';
35in
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 <action> 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}