diff options
author | aszlig <aszlig@nix.build> | 2018-05-08 02:09:46 +0200 |
---|---|---|
committer | aszlig <aszlig@nix.build> | 2018-05-08 02:09:46 +0200 |
commit | 78b4b90d6c9a3310b8a8ba3ac450240d03199bf0 (patch) | |
tree | 8483a3ca0be5a7616e90ccde499429d9d7ae1fe0 /nixos | |
parent | ec198337c4d50e4bd94e84db6bc886d375761564 (diff) | |
parent | a8b7372380725af56c213cdb01893640d5097c16 (diff) | |
download | nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.tar nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.tar.gz nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.tar.bz2 nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.tar.lz nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.tar.xz nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.tar.zst nixlib-78b4b90d6c9a3310b8a8ba3ac450240d03199bf0.zip |
Merge pull request #39526 (improve dhparams)
This introduces an option that allows us to turn off stateful generation of Diffie-Hellman parameters, which in some way is still "stateful" as the generated DH params file is non-deterministic. However what we can avoid with this is to have an increased surface for failures during system startup, because generation of the parameters is done during build-time. Aside from adding a NixOS VM test it also restructures the type of the security.dhparams.params option, so that it's a submodule. A new defaultBitSize option is also there to allow users to set a system-wide default. I added a release notes entry that described what has changed and also included a few notes for module developers using this module, as the first usage already popped up in NixOS/nixpkgs#39507. Thanks to @Ekleog and @abbradar for reviewing.
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-1809.xml | 52 | ||||
-rw-r--r-- | nixos/modules/security/dhparams.nix | 228 | ||||
-rw-r--r-- | nixos/release.nix | 1 | ||||
-rw-r--r-- | nixos/tests/dhparams.nix | 144 |
4 files changed, 344 insertions, 81 deletions
diff --git a/nixos/doc/manual/release-notes/rl-1809.xml b/nixos/doc/manual/release-notes/rl-1809.xml index 7136f4540502..7259be4c904c 100644 --- a/nixos/doc/manual/release-notes/rl-1809.xml +++ b/nixos/doc/manual/release-notes/rl-1809.xml @@ -175,6 +175,58 @@ $ nix-instantiate -E '(import <nixpkgsunstable> {}).gitFull' for further reference. </para> </listitem> + <listitem> + <para> + The module for <option>security.dhparams</option> has two new options now: + </para> + + <variablelist> + <varlistentry> + <term><option>security.dhparams.stateless</option></term> + <listitem><para> + Puts the generated Diffie-Hellman parameters into the Nix store instead + of managing them in a stateful manner in + <filename class="directory">/var/lib/dhparams</filename>. + </para></listitem> + </varlistentry> + <varlistentry> + <term><option>security.dhparams.defaultBitSize</option></term> + <listitem><para> + The default bit size to use for the generated Diffie-Hellman parameters. + </para></listitem> + </varlistentry> + </variablelist> + + <note><para> + The path to the actual generated parameter files should now be queried + using + <literal>config.security.dhparams.params.<replaceable>name</replaceable>.path</literal> + because it might be either in the Nix store or in a directory configured + by <option>security.dhparams.path</option>. + </para></note> + + <note> + <title>For developers:</title> + <para> + Module implementers should not set a specific bit size in order to let + users configure it by themselves if they want to have a different bit + size than the default (2048). + </para> + <para> + An example usage of this would be: +<programlisting> +{ config, ... }: + +{ + security.dhparams.params.myservice = {}; + environment.etc."myservice.conf".text = '' + dhparams = ${config.security.dhparams.params.myservice.path} + ''; +} +</programlisting> + </para> + </note> + </listitem> </itemizedlist> </section> </section> diff --git a/nixos/modules/security/dhparams.nix b/nixos/modules/security/dhparams.nix index 55c75713101d..e2b84c3e3b38 100644 --- a/nixos/modules/security/dhparams.nix +++ b/nixos/modules/security/dhparams.nix @@ -1,107 +1,173 @@ { config, lib, pkgs, ... }: -with lib; let + inherit (lib) mkOption types; cfg = config.security.dhparams; -in -{ + + bitType = types.addCheck types.int (b: b >= 16) // { + name = "bits"; + description = "integer of at least 16 bits"; + }; + + paramsSubmodule = { name, config, ... }: { + options.bits = mkOption { + type = bitType; + default = cfg.defaultBitSize; + description = '' + The bit size for the prime that is used during a Diffie-Hellman + key exchange. + ''; + }; + + options.path = mkOption { + type = types.path; + readOnly = true; + description = '' + The resulting path of the generated Diffie-Hellman parameters + file for other services to reference. This could be either a + store path or a file inside the directory specified by + <option>security.dhparams.path</option>. + ''; + }; + + config.path = let + generated = pkgs.runCommand "dhparams-${name}.pem" { + nativeBuildInputs = [ pkgs.openssl ]; + } "openssl dhparam -out \"$out\" ${toString config.bits}"; + in if cfg.stateful then "${cfg.path}/${name}.pem" else generated; + }; + +in { options = { security.dhparams = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to generate new DH params and clean up old DH params. + ''; + }; + params = mkOption { - description = - '' - Diffie-Hellman parameters to generate. - - The value is the size (in bits) of the DH params to generate. The - generated DH params path can be found in - <filename><replaceable>security.dhparams.path</replaceable>/<replaceable>name</replaceable>.pem</filename>. - - Note: The name of the DH params is taken as being the name of the - service it serves: the params will be generated before the said - service is started. - - Warning: If you are removing all dhparams from this list, you have - to leave security.dhparams.enable for at least one activation in - order to have them be cleaned up. This also means if you rollback to - a version without any dhparams the existing ones won't be cleaned - up. - ''; - type = with types; attrsOf int; + type = with types; let + coerce = bits: { inherit bits; }; + in attrsOf (coercedTo int coerce (submodule paramsSubmodule)); default = {}; - example = { nginx = 3072; }; + example = lib.literalExample "{ nginx.bits = 3072; }"; + description = '' + Diffie-Hellman parameters to generate. + + The value is the size (in bits) of the DH params to generate. The + generated DH params path can be found in + <literal>config.security.dhparams.params.<replaceable>name</replaceable>.path</literal>. + + <note><para>The name of the DH params is taken as being the name of + the service it serves and the params will be generated before the + said service is started.</para></note> + + <warning><para>If you are removing all dhparams from this list, you + have to leave <option>security.dhparams.enable</option> for at + least one activation in order to have them be cleaned up. This also + means if you rollback to a version without any dhparams the + existing ones won't be cleaned up. Of course this only applies if + <option>security.dhparams.stateful</option> is + <literal>true</literal>.</para></warning> + + <note><title>For module implementers:</title><para>It's recommended + to not set a specific bit size here, so that users can easily + override this by setting + <option>security.dhparams.defaultBitSize</option>.</para></note> + ''; + }; + + stateful = mkOption { + type = types.bool; + default = true; + description = '' + Whether generation of Diffie-Hellman parameters should be stateful or + not. If this is enabled, PEM-encoded files for Diffie-Hellman + parameters are placed in the directory specified by + <option>security.dhparams.path</option>. Otherwise the files are + created within the Nix store. + + <note><para>If this is <literal>false</literal> the resulting store + path will be non-deterministic and will be rebuilt every time the + <package>openssl</package> package changes.</para></note> + ''; + }; + + defaultBitSize = mkOption { + type = bitType; + default = 2048; + description = '' + This allows to override the default bit size for all of the + Diffie-Hellman parameters set in + <option>security.dhparams.params</option>. + ''; }; path = mkOption { - description = - '' - Path to the directory in which Diffie-Hellman parameters will be - stored. - ''; type = types.str; default = "/var/lib/dhparams"; - }; - - enable = mkOption { - description = - '' - Whether to generate new DH params and clean up old DH params. - ''; - default = false; - type = types.bool; + description = '' + Path to the directory in which Diffie-Hellman parameters will be + stored. This only is relevant if + <option>security.dhparams.stateful</option> is + <literal>true</literal>. + ''; }; }; }; - config = mkIf cfg.enable { + config = lib.mkIf (cfg.enable && cfg.stateful) { systemd.services = { dhparams-init = { - description = "Cleanup old Diffie-Hellman parameters"; - wantedBy = [ "multi-user.target" ]; # Clean up even when no DH params is set + description = "Clean Up Old Diffie-Hellman Parameters"; + + # Clean up even when no DH params is set + wantedBy = [ "multi-user.target" ]; + + serviceConfig.RemainAfterExit = true; serviceConfig.Type = "oneshot"; - script = - # Create directory - '' - if [ ! -d ${cfg.path} ]; then - mkdir -p ${cfg.path} - fi - '' + + + script = '' + if [ ! -d ${cfg.path} ]; then + mkdir -p ${cfg.path} + fi + # Remove old dhparams - '' - for file in ${cfg.path}/*; do - if [ ! -f "$file" ]; then - continue - fi - '' + concatStrings (mapAttrsToList (name: value: - '' - if [ "$file" == "${cfg.path}/${name}.pem" ] && \ - ${pkgs.openssl}/bin/openssl dhparam -in "$file" -text | head -n 1 | grep "(${toString value} bit)" > /dev/null; then + for file in ${cfg.path}/*; do + if [ ! -f "$file" ]; then + continue + fi + ${lib.concatStrings (lib.mapAttrsToList (name: { bits, path, ... }: '' + if [ "$file" = ${lib.escapeShellArg path} ] && \ + ${pkgs.openssl}/bin/openssl dhparam -in "$file" -text \ + | head -n 1 | grep "(${toString bits} bit)" > /dev/null; then continue fi - '' - ) cfg.params) + - '' - rm $file - done - - # TODO: Ideally this would be removing the *former* cfg.path, though this - # does not seem really important as changes to it are quite unlikely - rmdir --ignore-fail-on-non-empty ${cfg.path} - ''; + '') cfg.params)} + rm $file + done + + # TODO: Ideally this would be removing the *former* cfg.path, though + # this does not seem really important as changes to it are quite + # unlikely + rmdir --ignore-fail-on-non-empty ${cfg.path} + ''; }; - } // - mapAttrs' (name: value: nameValuePair "dhparams-gen-${name}" { - description = "Generate Diffie-Hellman parameters for ${name} if they don't exist yet"; - after = [ "dhparams-init.service" ]; - before = [ "${name}.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig.Type = "oneshot"; - script = - '' - mkdir -p ${cfg.path} - if [ ! -f ${cfg.path}/${name}.pem ]; then - ${pkgs.openssl}/bin/openssl dhparam -out ${cfg.path}/${name}.pem ${toString value} - fi - ''; - }) cfg.params; + } // lib.mapAttrs' (name: { bits, path, ... }: lib.nameValuePair "dhparams-gen-${name}" { + description = "Generate Diffie-Hellman Parameters for ${name}"; + after = [ "dhparams-init.service" ]; + before = [ "${name}.service" ]; + wantedBy = [ "multi-user.target" ]; + unitConfig.ConditionPathExists = "!${path}"; + serviceConfig.Type = "oneshot"; + script = '' + mkdir -p ${lib.escapeShellArg cfg.path} + ${pkgs.openssl}/bin/openssl dhparam -out ${lib.escapeShellArg path} \ + ${toString bits} + ''; + }) cfg.params; }; } diff --git a/nixos/release.nix b/nixos/release.nix index 55b4f19b8688..ae70b535a5e2 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -269,6 +269,7 @@ in rec { tests.containers-macvlans = callTest tests/containers-macvlans.nix {}; tests.couchdb = callTest tests/couchdb.nix {}; tests.deluge = callTest tests/deluge.nix {}; + tests.dhparams = callTest tests/dhparams.nix {}; tests.docker = callTestOnMatchingSystems ["x86_64-linux"] tests/docker.nix {}; tests.docker-tools = callTestOnMatchingSystems ["x86_64-linux"] tests/docker-tools.nix {}; tests.docker-tools-overlay = callTestOnMatchingSystems ["x86_64-linux"] tests/docker-tools-overlay.nix {}; diff --git a/nixos/tests/dhparams.nix b/nixos/tests/dhparams.nix new file mode 100644 index 000000000000..d11dfeec5d0c --- /dev/null +++ b/nixos/tests/dhparams.nix @@ -0,0 +1,144 @@ +let + common = { pkgs, ... }: { + security.dhparams.enable = true; + environment.systemPackages = [ pkgs.openssl ]; + }; + +in import ./make-test.nix { + name = "dhparams"; + + nodes.generation1 = { pkgs, config, ... }: { + imports = [ common ]; + security.dhparams.params = { + # Use low values here because we don't want the test to run for ages. + foo.bits = 16; + # Also use the old format to make sure the type is coerced in the right + # way. + bar = 17; + }; + + systemd.services.foo = { + description = "Check systemd Ordering"; + wantedBy = [ "multi-user.target" ]; + unitConfig = { + # This is to make sure that the dhparams generation of foo occurs + # before this service so we need this service to start as early as + # possible to provoke a race condition. + DefaultDependencies = false; + + # We check later whether the service has been started or not. + ConditionPathExists = config.security.dhparams.params.foo.path; + }; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + # The reason we only provide an ExecStop here is to ensure that we don't + # accidentally trigger an error because a file system is not yet ready + # during very early startup (we might not even have the Nix store + # available, for example if future changes in NixOS use systemd mount + # units to do early file system initialisation). + serviceConfig.ExecStop = "${pkgs.coreutils}/bin/true"; + }; + }; + + nodes.generation2 = { + imports = [ common ]; + security.dhparams.params.foo.bits = 18; + }; + + nodes.generation3 = common; + + nodes.generation4 = { + imports = [ common ]; + security.dhparams.stateful = false; + security.dhparams.params.foo2.bits = 18; + security.dhparams.params.bar2.bits = 19; + }; + + nodes.generation5 = { + imports = [ common ]; + security.dhparams.defaultBitSize = 30; + security.dhparams.params.foo3 = {}; + security.dhparams.params.bar3 = {}; + }; + + testScript = { nodes, ... }: let + getParamPath = gen: name: let + node = "generation${toString gen}"; + in nodes.${node}.config.security.dhparams.params.${name}.path; + + assertParamBits = gen: name: bits: let + path = getParamPath gen name; + in '' + $machine->nest('check bit size of ${path}', sub { + my $out = $machine->succeed('openssl dhparam -in ${path} -text'); + $out =~ /^\s*DH Parameters:\s+\((\d+)\s+bit\)\s*$/m; + die "bit size should be ${toString bits} but it is $1 instead." + if $1 != ${toString bits}; + }); + ''; + + switchToGeneration = gen: let + node = "generation${toString gen}"; + inherit (nodes.${node}.config.system.build) toplevel; + switchCmd = "${toplevel}/bin/switch-to-configuration test"; + in '' + $machine->nest('switch to generation ${toString gen}', sub { + $machine->succeed('${switchCmd}'); + $main::machine = ''$${node}; + }); + ''; + + in '' + my $machine = $generation1; + + $machine->waitForUnit('multi-user.target'); + + subtest "verify startup order", sub { + $machine->succeed('systemctl is-active foo.service'); + }; + + subtest "check bit sizes of dhparam files", sub { + ${assertParamBits 1 "foo" 16} + ${assertParamBits 1 "bar" 17} + }; + + ${switchToGeneration 2} + + subtest "check whether bit size has changed", sub { + ${assertParamBits 2 "foo" 18} + }; + + subtest "ensure that dhparams file for 'bar' was deleted", sub { + $machine->fail('test -e ${getParamPath 1 "bar"}'); + }; + + ${switchToGeneration 3} + + subtest "ensure that 'security.dhparams.path' has been deleted", sub { + $machine->fail( + 'test -e ${nodes.generation3.config.security.dhparams.path}' + ); + }; + + ${switchToGeneration 4} + + subtest "check bit sizes dhparam files", sub { + ${assertParamBits 4 "foo2" 18} + ${assertParamBits 4 "bar2" 19} + }; + + subtest "check whether dhparam files are in the Nix store", sub { + $machine->succeed( + 'expr match ${getParamPath 4 "foo2"} ${builtins.storeDir}', + 'expr match ${getParamPath 4 "bar2"} ${builtins.storeDir}', + ); + }; + + ${switchToGeneration 5} + + subtest "check whether defaultBitSize works as intended", sub { + ${assertParamBits 5 "foo3" 30} + ${assertParamBits 5 "bar3" 30} + }; + ''; +} |