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:

  1. Exit immediately if boot.initrd.secrets is empty.
  2. Create a temporary directory.
  3. For each secret, copy the source file to $tmp/.initrd-secrets/$dest, preserving attributes (cp -a).
  4. Set all timestamps to the Unix epoch (touch -amt 197001010000) for reproducibility.
  5. Sort the file list (sort -z) for deterministic ordering.
  6. Generate a CPIO newc archive owned by root:root (cpio -H newc -R +0:+0 --reproducible).
  7. Compress with the same compressor used for the main initrd.
  8. 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

FileRole
nixos/modules/system/boot/stage-1.nixOption definition, append-initrd-secrets script, inline fallback, assertion
nixos/modules/system/boot/systemd/initrd-secrets.nixsystemd initrd copy service
nixos/modules/system/boot/loader/grub/grub.nixGRUB sets supportsInitrdSecrets = true
nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nixsystemd-boot sets supportsInitrdSecrets = true
nixos/modules/system/boot/loader/grub/install-grub.plCalls append-initrd-secrets per generation
nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.pyCalls append-initrd-secrets per generation
nixos/modules/system/boot/bootspec.nixRecords initrdSecrets path in bootspec.json

Comments