1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}:
8
9let
10 inherit (lib) literalExpression mkOption types;
11 cfg = config.security.dhparams;
12 opt = options.security.dhparams;
13
14 bitType = types.addCheck types.int (b: b >= 16) // {
15 name = "bits";
16 description = "integer of at least 16 bits";
17 };
18
19 paramsSubmodule =
20 { name, config, ... }:
21 {
22 options.bits = mkOption {
23 type = bitType;
24 default = cfg.defaultBitSize;
25 defaultText = literalExpression "config.${opt.defaultBitSize}";
26 description = ''
27 The bit size for the prime that is used during a Diffie-Hellman
28 key exchange.
29 '';
30 };
31
32 options.path = mkOption {
33 type = types.path;
34 readOnly = true;
35 description = ''
36 The resulting path of the generated Diffie-Hellman parameters
37 file for other services to reference. This could be either a
38 store path or a file inside the directory specified by
39 {option}`security.dhparams.path`.
40 '';
41 };
42
43 config.path =
44 let
45 generated = pkgs.runCommand "dhparams-${name}.pem" {
46 nativeBuildInputs = [ pkgs.openssl ];
47 } "openssl dhparam -out \"$out\" ${toString config.bits}";
48 in
49 if cfg.stateful then "${cfg.path}/${name}.pem" else generated;
50 };
51
52in
53{
54 options = {
55 security.dhparams = {
56 enable = mkOption {
57 type = types.bool;
58 default = false;
59 description = ''
60 Whether to generate new DH params and clean up old DH params.
61 '';
62 };
63
64 params = mkOption {
65 type =
66 with types;
67 let
68 coerce = bits: { inherit bits; };
69 in
70 attrsOf (coercedTo int coerce (submodule paramsSubmodule));
71 default = { };
72 example = lib.literalExpression "{ nginx.bits = 3072; }";
73 description = ''
74 Diffie-Hellman parameters to generate.
75
76 The value is the size (in bits) of the DH params to generate. The
77 generated DH params path can be found in
78 `config.security.dhparams.params.«name».path`.
79
80 ::: {.note}
81 The name of the DH params is taken as being the name of
82 the service it serves and the params will be generated before the
83 said service is started.
84 :::
85
86 ::: {.warning}
87 If you are removing all dhparams from this list, you
88 have to leave {option}`security.dhparams.enable` for at
89 least one activation in order to have them be cleaned up. This also
90 means if you rollback to a version without any dhparams the
91 existing ones won't be cleaned up. Of course this only applies if
92 {option}`security.dhparams.stateful` is
93 `true`.
94 :::
95
96 ::: {.note}
97 **For module implementers:** It's recommended
98 to not set a specific bit size here, so that users can easily
99 override this by setting
100 {option}`security.dhparams.defaultBitSize`.
101 :::
102 '';
103 };
104
105 stateful = mkOption {
106 type = types.bool;
107 default = true;
108 description = ''
109 Whether generation of Diffie-Hellman parameters should be stateful or
110 not. If this is enabled, PEM-encoded files for Diffie-Hellman
111 parameters are placed in the directory specified by
112 {option}`security.dhparams.path`. Otherwise the files are
113 created within the Nix store.
114
115 ::: {.note}
116 If this is `false` the resulting store
117 path will be non-deterministic and will be rebuilt every time the
118 `openssl` package changes.
119 :::
120 '';
121 };
122
123 defaultBitSize = mkOption {
124 type = bitType;
125 default = 2048;
126 description = ''
127 This allows to override the default bit size for all of the
128 Diffie-Hellman parameters set in
129 {option}`security.dhparams.params`.
130 '';
131 };
132
133 path = mkOption {
134 type = types.str;
135 default = "/var/lib/dhparams";
136 description = ''
137 Path to the directory in which Diffie-Hellman parameters will be
138 stored. This only is relevant if
139 {option}`security.dhparams.stateful` is
140 `true`.
141 '';
142 };
143 };
144 };
145
146 config = lib.mkIf (cfg.enable && cfg.stateful) {
147 systemd.services = {
148 dhparams-init = {
149 description = "Clean Up Old Diffie-Hellman Parameters";
150
151 # Clean up even when no DH params is set
152 wantedBy = [ "multi-user.target" ];
153
154 serviceConfig.RemainAfterExit = true;
155 serviceConfig.Type = "oneshot";
156
157 script = ''
158 if [ ! -d ${cfg.path} ]; then
159 mkdir -p ${cfg.path}
160 fi
161
162 # Remove old dhparams
163 for file in ${cfg.path}/*; do
164 if [ ! -f "$file" ]; then
165 continue
166 fi
167 ${lib.concatStrings (
168 lib.mapAttrsToList (
169 name:
170 { bits, path, ... }:
171 ''
172 if [ "$file" = ${lib.escapeShellArg path} ] && \
173 ${pkgs.openssl}/bin/openssl dhparam -in "$file" -text \
174 | head -n 1 | grep "(${toString bits} bit)" > /dev/null; then
175 continue
176 fi
177 ''
178 ) cfg.params
179 )}
180 rm "$file"
181 done
182
183 # TODO: Ideally this would be removing the *former* cfg.path, though
184 # this does not seem really important as changes to it are quite
185 # unlikely
186 rmdir --ignore-fail-on-non-empty ${cfg.path}
187 '';
188 };
189 }
190 // lib.mapAttrs' (
191 name:
192 { bits, path, ... }:
193 lib.nameValuePair "dhparams-gen-${name}" {
194 description = "Generate Diffie-Hellman Parameters for ${name}";
195 after = [ "dhparams-init.service" ];
196 before = [ "${name}.service" ];
197 wantedBy = [ "multi-user.target" ];
198 unitConfig.ConditionPathExists = "!${path}";
199 serviceConfig.Type = "oneshot";
200 script = ''
201 mkdir -p ${lib.escapeShellArg cfg.path}
202 ${pkgs.openssl}/bin/openssl dhparam -out ${lib.escapeShellArg path} \
203 ${toString bits}
204 '';
205 }
206 ) cfg.params;
207 };
208
209 meta.maintainers = with lib.maintainers; [ ekleog ];
210}