nixos/initrd: add secretPaths and extraSecretsHook

This commit is contained in:
cyclopentane
2026-02-23 17:42:14 +01:00
parent ddbb171b5f
commit 72e4df576e
7 changed files with 173 additions and 44 deletions
@@ -86,7 +86,7 @@ in
description = ''
Append an additional file's contents to `/etc/iscsid.conf`. Use a non-store path
and store passwords in this file. Note: the file specified here must be available
in the initrd, see: `boot.initrd.secrets`.
in the initrd, see: `boot.initrd.secretPaths`.
'';
default = null;
type = nullOr str;
+4 -2
View File
@@ -14,7 +14,9 @@ let
children = lib.mapAttrs (
childName: childConfig: childConfig.configuration.system.build.toplevel
) config.specialisation;
hasAtLeastOneInitrdSecret = lib.length (lib.attrNames config.boot.initrd.secrets) > 0;
hasInitrdSecrets =
(lib.length (lib.attrNames config.boot.initrd.secretPaths) > 0)
|| (config.boot.initrd.extraSecretsHook != "");
schemas = {
v1 = rec {
filename = "boot.json";
@@ -33,7 +35,7 @@ let
// lib.optionalAttrs config.boot.initrd.enable {
initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
}
// lib.optionalAttrs hasAtLeastOneInitrdSecret {
// lib.optionalAttrs hasInitrdSecrets {
initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
};
}
+1 -1
View File
@@ -414,7 +414,7 @@ in
ln -s ${initrdPath} $out/initrd
${optionalString (config.boot.initrd.secrets != { }) ''
${optionalString (config.boot.initrd.secretPaths != { }) ''
ln -s ${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets $out
''}
@@ -482,7 +482,7 @@ sub addEntry {
die "failed to create initrd secrets $!\n";
} else {
say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation";
say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`";
say STDERR "note: this is normal after having modified or removed an entry in `boot.initrd.secretPaths`";
}
}
# Check whether any secrets were actually added
@@ -202,7 +202,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
print("warning: failed to create initrd secrets "
f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
print("note: this is normal after having removed "
"or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
"or modified an entry in `boot.initrd.secretPaths`", file=sys.stderr)
entry_file = BOOT_MOUNT_POINT / "loader/entries" / generation_conf_filename(profile, generation, specialisation)
tmp_path = entry_file.with_suffix(".tmp")
kernel_params = "init=%s " % bootspec.init
+148 -30
View File
@@ -148,23 +148,22 @@ let
# Copy secrets if needed.
#
# TODO: move out to a separate script; see #85000.
${optionalString (!config.boot.loader.supportsInitrdSecrets) (
concatStringsSep "\n" (
mapAttrsToList (
dest: source:
let
source' = if source == null then dest else source;
in
''
mkdir -p $(dirname "$out/secrets/${dest}")
# Some programs (e.g. ssh) doesn't like secrets to be
# symlinks, so we use `cp -L` here to match the
# behaviour when secrets are natively supported.
cp -Lr ${source'} "$out/secrets/${dest}"
''
) config.boot.initrd.secrets
)
)}
${optionalString (!config.boot.loader.supportsInitrdSecrets) ''
${concatStringsSep "\n" (
mapAttrsToList (_: scfg: ''
mkdir -p $(dirname "$out/secrets${scfg.path}")
# Some programs (e.g. ssh) doesn't like secrets to be
# symlinks, so we use `cp -L` here to match the
# behaviour when secrets are natively supported.
# The assertion further up in this file (stage-1.nix)
# checks that all secretPaths are Nix store paths set via
# boot.initrd.secretPaths.*.source if the bootloader doesn't
# support initrd secrets.
cp -Lr ${scfg.source} "$out/secrets${scfg.path}"
'') config.boot.initrd.secretPaths
)}
${config.boot.initrd.extraSecretsHook}
''}
${config.boot.initrd.extraUtilsCommands}
@@ -436,7 +435,9 @@ let
exit 0
fi
${lib.optionalString (config.boot.initrd.secrets == { }) "exit 0"}
${lib.optionalString (
config.boot.initrd.secretPaths == { } && config.boot.initrd.extraSecretsHook == ""
) "exit 0"}
export PATH=${pkgs.coreutils}/bin:${pkgs.cpio}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
@@ -451,17 +452,25 @@ let
${lib.concatStringsSep "\n" (
mapAttrsToList (
dest: source:
_: scfg:
let
source' = if source == null then dest else toString source;
prefix = lib.optionalString scfg.intermediateSecretsDir "/.initrd-secrets";
in
''
mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
mkdir -p $(dirname "$tmp${prefix}${scfg.path}")
(
export out="$tmp${prefix}${scfg.path}"
${scfg.generateSecretCommand}
)
''
) config.boot.initrd.secrets
) config.boot.initrd.secretPaths
)}
(
cd "$tmp"
${config.boot.initrd.extraSecretsHook}
)
# mindepth 1 so that we don't change the mode of /
(cd "$tmp" && find . -mindepth 1 | xargs touch -amt 197001010000 && find . -mindepth 1 -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
@@ -668,6 +677,104 @@ in
'';
};
boot.initrd.secretPaths = mkOption {
default = { };
type = types.attrsOf (
types.submodule (
{ config, name, ... }:
{
options = {
path = mkOption {
type = types.path;
default = name;
description = ''
The path the secret should be placed at in the initrd. Defaults
to the attribute name.
'';
};
intermediateSecretsDir = mkOption {
type = types.bool;
default = true;
description = ''
By default, the secrets will be copied over to the
`/.initrd-secrets` dir at initrd generation time, and then copied
over to their final location at boot time. This is because initrd secrets
that are supposed to be placed in `/run` would be overridden by
the tmpfs mount over `/run` otherwise.
Set this option to `false` to skip this intermediate step and
place the secret at its final location straightaway.
'';
};
source = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
The absolute path on the filesystem to copy the secret from.
'';
example = "/var/lib/secrets/initrd/ssh_host_ed25519_key";
};
generateSecretCommand = mkOption {
type = types.path;
description = ''
The command to run to generate the secret. It should write
the secret to `$out`.
This is useful if you have a more advanced secrets provisioning
mechanism.
'';
example = ''
pkgs.writeShellScript "generate-secret" '''
''${lib.getExe pkgs.age} -d -i /etc/ssh/ssh_host_ed25519_key -o "$out" ''${./secret.age}
'''
'';
};
};
config = {
generateSecretCommand = lib.mkIf (config.source != null) (
pkgs.writeShellScript "copy-secret" ''
cp -Lr ${config.source} "$out"
''
);
};
}
)
);
description = ''
Secret paths to append to the initrd. The attribute name is the
path the secret should have inside the initrd.
Note that `nixos-rebuild switch` will generate the initrd
also for past generations, so if secrets are moved or deleted
you will also have to garbage collect the generations that
use those secrets.
'';
example = {
"/etc/ssh/ssh_host_ed25519_key".source = "/var/lib/secrets/initrd/ssh_host_ed25519_key";
};
};
boot.initrd.extraSecretsHook = mkOption {
default = "";
type = types.lines;
description = ''
Extra commands to be executed after the initrd secrets generation phase.
This script should place files into the current workdir. These files
will then be copied over to the initrd to the corresponding absolute
paths, e.g. `etc/ssh/ssh_host_ed25519_key` will be copied over to
`/etc/ssh/ssh_host_ed25519_key`.
'';
example = ''
# Generate a new SSH host key for every generation.
ssh-keygen -f etc/ssh/ssh_host_ed25519_key
'';
};
boot.initrd.supportedFilesystems = mkOption {
default = { };
inherit (options.boot.supportedFilesystems) example type description;
@@ -746,15 +853,18 @@ in
assertion =
!config.boot.loader.supportsInitrdSecrets
-> all (
source: builtins.isPath source || (builtins.isString source && hasPrefix builtins.storeDir source)
) (attrValues config.boot.initrd.secrets);
scfg:
builtins.isPath scfg.source
|| (builtins.isString scfg.source && hasPrefix builtins.storeDir scfg.source)
) (attrValues config.boot.initrd.secretPaths);
message = ''
boot.initrd.secrets values must be unquoted paths when
using a bootloader that doesn't natively support initrd
secrets, e.g.:
When using a bootloader that doesn't natively support initrd secrets,
all `boot.initrd.secretPaths` values must be defined via
`boot.initrd.secretsPaths.*.source`, and the `source` values must be
unquoted paths, e.g.
boot.initrd.secrets = {
"/etc/secret" = /path/to/secret;
boot.initrd.secretPaths = {
"/etc/secret".source = /path/to/secret;
};
Note that this will result in all secrets being stored
@@ -763,6 +873,14 @@ in
}
];
# Backwards compatibility to the legacy `boot.initrd.secrets` option.
boot.initrd.secretPaths = lib.mapAttrs' (dest: source: {
# The legacy boot.initrd.secrets option didn't type-check the attr
# names, so we need to optionally prepend a slash.
name = "${lib.optionalString (!lib.hasPrefix "/" dest) "/"}${dest}";
value.source = if dest != null then source else dest;
}) config.boot.initrd.secrets;
system.build = mkMerge [
{
inherit
@@ -10,14 +10,21 @@
# Copy secrets into the initrd if they cannot be appended
boot.initrd.systemd.contents = lib.mkIf (!config.boot.loader.supportsInitrdSecrets) (
lib.mapAttrs' (
dest: source:
lib.nameValuePair "/.initrd-secrets/${dest}" { source = if source == null then dest else source; }
) config.boot.initrd.secrets
_: scfg:
let
prefix = lib.optionalString scfg.intermediateSecretsDir "/.initrd-secrets";
in
lib.nameValuePair "${prefix}${scfg.path}" { inherit (scfg) source; }
) config.boot.initrd.secretPaths
);
# Copy secrets to their respective locations
boot.initrd.systemd.services.initrd-nixos-copy-secrets =
lib.mkIf (config.boot.initrd.secrets != { })
lib.mkIf
(
(builtins.any (x: x.intermediateSecretsDir) (builtins.attrValues config.boot.initrd.secretPaths))
|| config.boot.initrd.extraSecretsHook != ""
)
{
description = "Copy secrets into place";
# Run as early as possible
@@ -34,10 +41,12 @@
# drop this service, we'd mount the /run tmpfs over the secret, making it
# invisible in stage 2.
script = ''
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
mkdir -p "$(dirname "/$secret")"
cp "/.initrd-secrets/$secret" "/$secret"
done
if [ -d /.initrd-secrets ]; then
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
mkdir -p "$(dirname "/$secret")"
cp "/.initrd-secrets/$secret" "/$secret"
done
fi
'';
serviceConfig = {