boot.initrd.secrets
boot.initrd.secrets
Synopsis
boot.initrd.secrets = {
"/etc/ssh/ssh_host_ed25519_key" = /etc/secrets/initrd/ssh_host_ed25519_key;
"/etc/cryptroot.key" = /etc/secrets/initrd/cryptroot.key;
};Description
boot.initrd.secrets copies files into the initrd so they are available before the root filesystem is mounted. Typical contents include LUKS keyfiles, SSH host keys for initrd SSH, and VPN configurations.
The option type is attrsOf (nullOr path). Attribute names are destination paths inside the initrd. Values are source paths on the host, or null to use the attribute name as both source and destination:
boot.initrd.secrets."/etc/ssh/ssh_host_ed25519_key" = null;
# copies /etc/ssh/ssh_host_ed25519_key to /etc/ssh/ssh_host_ed25519_key inside the initrd
The option is defined in nixos/modules/system/boot/stage-1.nix.
Implementation
There are two code paths. Which one runs depends on the value of boot.loader.supportsInitrdSecrets.
Append path (supportsInitrdSecrets = true)
GRUB, systemd-boot, and Limine all set supportsInitrdSecrets = true.
In this path, the initrd is built as a normal Nix derivation without any secrets. At bootloader install time (nixos-rebuild switch), the generated script append-initrd-secrets constructs a second CPIO archive containing the secrets and appends it to the initrd file on the boot partition.
The Linux kernel treats concatenated CPIO archives as a single initramfs — it unpacks them sequentially. This is a documented property of the initramfs format.
Secrets handled this way never enter the Nix store. They reside on the boot partition only.
The script is built in stage-1.nix and stored at config.system.build.initialRamdiskSecretAppender. Bootloader installers call it: GRUB via install-grub.pl, systemd-boot via systemd-boot-builder.py. The path to the script is also recorded in bootspec.json under the initrdSecrets key for use by external boot managers.
Failure to create secrets for the current generation is fatal. Failure for older generations produces a warning and continues — this is expected after removing or renaming a secret source file.
Inline path (supportsInitrdSecrets = false)
When the bootloader does not support appending, secrets are copied into the initrd derivation at build time:
mkdir -p $(dirname "$out/secrets/${dest}")
cp -Lr ${source'} "$out/secrets/${dest}"
This places secrets in the Nix store. The store is world-readable. An assertion enforces that all values are unquoted Nix paths or store paths in this case, and the build emits:
Note that this will result in all secrets being stored world-readable in the Nix store!
The path type on the option (rather than str) exists to support this fallback.
append-initrd-secrets
The script performs the following steps:
- Exit immediately if
boot.initrd.secretsis empty. - Create a temporary directory.
- For each secret, copy the source file to
$tmp/.initrd-secrets/$dest, preserving attributes (cp -a). - Set all timestamps to the Unix epoch (
touch -amt 197001010000) for reproducibility. - Sort the file list (
sort -z) for deterministic ordering. - Generate a CPIO newc archive owned by root:root (
cpio -H newc -R +0:+0 --reproducible). - Compress with the same compressor used for the main initrd.
- Append (
>>) to the target initrd file.
Simplified:
tmp=$(mktemp -d ${TMPDIR:-/tmp}/initrd-secrets.XXXXXXXXXX)
mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
cp -a ${source} "$tmp/.initrd-secrets/${dest}"
(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) \
| zstd >> "$1"
The compressor and its arguments are taken from config.boot.initrd.compressor and config.boot.initrd.compressorArgs.
systemd initrd integration
When boot.initrd.systemd.enable = true, secrets are not placed at their final paths directly. They are staged under /.initrd-secrets/ inside the initrd and copied into place by the initrd-nixos-copy-secrets service.
This service is defined in nixos/modules/system/boot/systemd/initrd-secrets.nix:
boot.initrd.systemd.services.initrd-nixos-copy-secrets = {
description = "Copy secrets into place";
wantedBy = [ "sysinit.target" ];
before = [ "cryptsetup-pre.target" "shutdown.target" ];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
script = ''
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
mkdir -p "$(dirname "/$secret")"
cp "/.initrd-secrets/$secret" "/$secret"
done
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
The indirection exists because the initrd mounts a tmpfs on /run. Secrets targeting paths under /run would be shadowed by this mount. Staging them in /.initrd-secrets/ and copying after mount setup avoids this.
The service runs before cryptsetup-pre.target, so secrets are available before LUKS devices attempt to unlock.
Consumers
Several NixOS modules set boot.initrd.secrets internally.
boot.initrd.network.ssh
boot.initrd.network.ssh = {
enable = true;
hostKeys = [ /etc/secrets/initrd/ssh_host_ed25519_key ];
};
Generates:
boot.initrd.secrets."/etc/ssh/ssh_host_ed25519_key" =
/etc/secrets/initrd/ssh_host_ed25519_key;
The module warns against reusing regular host keys. With the append path, the initrd SSH key resides on the unencrypted boot partition.
boot.initrd.luks.devices.*.keyFile
boot.initrd.secrets."/etc/cryptroot.key" = /etc/secrets/initrd/cryptroot.key;
boot.initrd.luks.devices.cryptroot = {
device = "/dev/disk/by-uuid/...";
keyFile = "/etc/cryptroot.key";
};boot.initrd.network.openvpn
boot.initrd.network.openvpn = {
enable = true;
configuration = /etc/secrets/initrd/vpn.ovpn;
};
Generates:
boot.initrd.secrets."/etc/initrd.ovpn" = /etc/secrets/initrd/vpn.ovpn;Security
Append path. Secrets are on the boot partition, owned by root, not world-readable. They are not encrypted. The security posture is equivalent to the rest of an unencrypted /boot — kernel and initrd are equally exposed. Attacks against the boot partition (replacement, modification) are addressed by Secure Boot and measured boot (TPM), not by this module.
Inline path. Secrets are in /nix/store and world-readable. Do not use this path for sensitive material.
Garbage collection. Old NixOS generations retain their initrd files with appended secrets. When a secret source file is removed, nixos-rebuild switch warns but continues for old generations. The old initrd (with the old secret) remains on /boot until the generation is deleted with nix-collect-garbage -d or nix-env --delete-generations.
Files
| File | Role |
|---|---|
nixos/modules/system/boot/stage-1.nix | Option definition, append-initrd-secrets script, inline fallback, assertion |
nixos/modules/system/boot/systemd/initrd-secrets.nix | systemd initrd copy service |
nixos/modules/system/boot/loader/grub/grub.nix | GRUB sets supportsInitrdSecrets = true |
nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix | systemd-boot sets supportsInitrdSecrets = true |
nixos/modules/system/boot/loader/grub/install-grub.pl | Calls append-initrd-secrets per generation |
nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py | Calls append-initrd-secrets per generation |
nixos/modules/system/boot/bootspec.nix | Records initrdSecrets path in bootspec.json |
Comments