1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.certmgr;
9
10 specs = lib.mapAttrsToList (n: v: rec {
11 name = n + ".json";
12 path = if lib.isAttrs v then pkgs.writeText name (builtins.toJSON v) else v;
13 }) cfg.specs;
14
15 allSpecs = pkgs.linkFarm "certmgr.d" specs;
16
17 certmgrYaml = pkgs.writeText "certmgr.yaml" (
18 builtins.toJSON {
19 dir = allSpecs;
20 default_remote = cfg.defaultRemote;
21 svcmgr = cfg.svcManager;
22 before = cfg.validMin;
23 interval = cfg.renewInterval;
24 inherit (cfg) metricsPort metricsAddress;
25 }
26 );
27
28 specPaths = map dirOf (
29 lib.concatMap (
30 spec:
31 if lib.isAttrs spec then
32 lib.collect lib.isString (lib.filterAttrsRecursive (n: v: lib.isAttrs v || n == "path") spec)
33 else
34 [ spec ]
35 ) (lib.attrValues cfg.specs)
36 );
37
38 preStart = ''
39 ${lib.concatStringsSep " \\\n" ([ "mkdir -p" ] ++ map lib.escapeShellArg specPaths)}
40 ${cfg.package}/bin/certmgr -f ${certmgrYaml} check
41 '';
42in
43{
44 options.services.certmgr = {
45 enable = lib.mkEnableOption "certmgr";
46
47 package = lib.mkPackageOption pkgs "certmgr" { };
48
49 defaultRemote = lib.mkOption {
50 type = lib.types.str;
51 default = "127.0.0.1:8888";
52 description = "The default CA host:port to use.";
53 };
54
55 validMin = lib.mkOption {
56 default = "72h";
57 type = lib.types.str;
58 description = "The interval before a certificate expires to start attempting to renew it.";
59 };
60
61 renewInterval = lib.mkOption {
62 default = "30m";
63 type = lib.types.str;
64 description = "How often to check certificate expirations and how often to update the cert_next_expires metric.";
65 };
66
67 metricsAddress = lib.mkOption {
68 default = "127.0.0.1";
69 type = lib.types.str;
70 description = "The address for the Prometheus HTTP endpoint.";
71 };
72
73 metricsPort = lib.mkOption {
74 default = 9488;
75 type = lib.types.ints.u16;
76 description = "The port for the Prometheus HTTP endpoint.";
77 };
78
79 specs = lib.mkOption {
80 default = { };
81 example = lib.literalExpression ''
82 {
83 exampleCert =
84 let
85 domain = "example.com";
86 secret = name: "/var/lib/secrets/''${name}.pem";
87 in {
88 service = "nginx";
89 action = "reload";
90 authority = {
91 file.path = secret "ca";
92 };
93 certificate = {
94 path = secret domain;
95 };
96 private_key = {
97 owner = "root";
98 group = "root";
99 mode = "0600";
100 path = secret "''${domain}-key";
101 };
102 request = {
103 CN = domain;
104 hosts = [ "mail.''${domain}" "www.''${domain}" ];
105 key = {
106 algo = "rsa";
107 size = 2048;
108 };
109 names = {
110 O = "Example Organization";
111 C = "USA";
112 };
113 };
114 };
115 otherCert = "/var/certmgr/specs/other-cert.json";
116 }
117 '';
118 type =
119 with lib.types;
120 attrsOf (
121 either path (submodule {
122 options = {
123 service = lib.mkOption {
124 type = nullOr str;
125 default = null;
126 description = "The service on which to perform \\<action\\> after fetching.";
127 };
128
129 action = lib.mkOption {
130 type = addCheck str (
131 x:
132 cfg.svcManager == "command"
133 || lib.elem x [
134 "restart"
135 "reload"
136 "nop"
137 ]
138 );
139 default = "nop";
140 description = "The action to take after fetching.";
141 };
142
143 # These ought all to be specified according to certmgr spec def.
144 authority = lib.mkOption {
145 type = attrs;
146 description = "certmgr spec authority object.";
147 };
148
149 certificate = lib.mkOption {
150 type = nullOr attrs;
151 description = "certmgr spec certificate object.";
152 };
153
154 private_key = lib.mkOption {
155 type = nullOr attrs;
156 description = "certmgr spec private_key object.";
157 };
158
159 request = lib.mkOption {
160 type = nullOr attrs;
161 description = "certmgr spec request object.";
162 };
163 };
164 })
165 );
166 description = ''
167 Certificate specs as described by:
168 <https://github.com/cloudflare/certmgr#certificate-specs>
169 These will be added to the Nix store, so they will be world readable.
170 '';
171 };
172
173 svcManager = lib.mkOption {
174 default = "systemd";
175 type = lib.types.enum [
176 "circus"
177 "command"
178 "dummy"
179 "openrc"
180 "systemd"
181 "sysv"
182 ];
183 description = ''
184 This specifies the service manager to use for restarting or reloading services.
185 See: <https://github.com/cloudflare/certmgr#certmgryaml>.
186 For how to use the "command" service manager in particular,
187 see: <https://github.com/cloudflare/certmgr#command-svcmgr-and-how-to-use-it>.
188 '';
189 };
190
191 };
192
193 config = lib.mkIf cfg.enable {
194 assertions = [
195 {
196 assertion = cfg.specs != { };
197 message = "Certmgr specs cannot be empty.";
198 }
199 {
200 assertion =
201 !lib.any (lib.hasAttrByPath [
202 "authority"
203 "auth_key"
204 ]) (lib.attrValues cfg.specs);
205 message = ''
206 Inline services.certmgr.specs are added to the Nix store rendering them world readable.
207 Specify paths as specs, if you want to use include auth_key - or use the auth_key_file option."
208 '';
209 }
210 ];
211
212 systemd.services.certmgr = {
213 description = "certmgr";
214 path = lib.mkIf (cfg.svcManager == "command") [ pkgs.bash ];
215 wants = [ "network-online.target" ];
216 after = [ "network-online.target" ];
217 wantedBy = [ "multi-user.target" ];
218 inherit preStart;
219
220 serviceConfig = {
221 Restart = "always";
222 RestartSec = "10s";
223 ExecStart = "${cfg.package}/bin/certmgr -f ${certmgrYaml}";
224 };
225 };
226 };
227}