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