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