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