Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initrd verify stage 2 #273593

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,7 @@
./system/boot/initrd-network.nix
./system/boot/initrd-openvpn.nix
./system/boot/initrd-ssh.nix
./system/boot/initrd-verify.nix
./system/boot/kernel.nix
./system/boot/kexec.nix
./system/boot/loader/efi.nix
Expand Down
46 changes: 46 additions & 0 deletions nixos/modules/system/boot/initrd-verify.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{ lib, config, ... }: let
cfg = config.boot.initrd.verify;
nix = config.nix.package;
in {
options.boot.initrd.verify = {
enable = lib.mkEnableOption "verifying the stage 2 closure during initrd";
trustedPublicKeys = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = lib.mdDoc "Keys to verify the closure against.";
};
sigsNeeded = lib.mkOption {
type = lib.types.int;
description = lib.mdDoc "Number of signatures required.";
default = 1;
};
signing = {
enable = lib.mkEnableOption "signing the system profiles when configuring the boot loader";
keyFile = lib.mkOption {
type = lib.types.path;
description = lib.mdDoc "Path to the signing key.";
example = "/run/keys/signing-key";
};
};
};

config = lib.mkIf cfg.enable {
boot.initrd = {
systemd.storePaths = [ "${nix}/bin/nix" ];
systemd.services.verify-store = {
requiredBy = [ "initrd-nixos-activation.service" ];
before = [ "initrd-nixos-activation.service" ];
requires = [ "initrd-fs.target" ];
after = [ "initrd-fs.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${nix}/bin/nix --experimental-features nix-command verify -r --store /sysroot --trusted-public-keys \"${lib.concatStringsSep " " cfg.trustedPublicKeys}\" ${if cfg.sigsNeeded == 0 then "--no-trust" else "--sigs-needed " + toString cfg.sigsNeeded} \${NIXOS_SYSTEM_CLOSURE}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ExecStart = "${nix}/bin/nix --experimental-features nix-command verify -r --store /sysroot --trusted-public-keys \"${lib.concatStringsSep " " cfg.trustedPublicKeys}\" ${if cfg.sigsNeeded == 0 then "--no-trust" else "--sigs-needed " + toString cfg.sigsNeeded} \${NIXOS_SYSTEM_CLOSURE}";
ExecStart = "${nix}/bin/nix --extra-experimental-features nix-command verify -r --store /sysroot --trusted-public-keys \"${lib.concatStringsSep " " cfg.trustedPublicKeys}\" ${if cfg.sigsNeeded == 0 then "--no-trust" else "--sigs-needed " + toString cfg.sigsNeeded} \${NIXOS_SYSTEM_CLOSURE}";

I am not sure if it would be relevant if ca-derivations or similar are used in the closures.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? Content-address derivations have the hash in their path measure their contents, but it's not checked at boot- or access-time.

AFAIK, nix store verify still checks that the contents of CA derivations match their NAR hash, per the description:

For each path, it checks that

  • its contents match the NAR hash recorded in the Nix database; and
  • it is trusted, that is, it is signed by at least one trusted signing key, is content-addressed, or is built locally ("ultimately trusted").

That does lead to an interesting question, though: could an adversary manipulate the Nix db to change a CA derivation's expected hash ?

};
};
};

boot.loader.systemd-boot.extraInstallCommands = lib.mkIf cfg.signing.enable ''
echo Signing system generations
nix --experimental-features nix-command store sign -k ${toString cfg.signing.keyFile} -r /nix/var/nix/profiles/system*
'';
};
}
47 changes: 27 additions & 20 deletions nixos/modules/system/boot/systemd/initrd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ let
postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -sf '${v}' $out/bin/'${n}'") cfg.extraBin);
};

envGenerator = pkgs.writeShellScriptBin "nixos-environment-generator" ''
# Figure out what closure to boot
closure=
for o in $(< /proc/cmdline); do
case $o in
init=*)
IFS== read -r -a initParam <<< "$o"
closure="$(dirname "''${initParam[1]}")"
;;
esac
done

# Sanity check
if [ -z "''${closure:-}" ]; then
echo 'No init= parameter on the kernel command line' >&2
exit 1
fi

echo NIXOS_SYSTEM_CLOSURE=$closure
'';

initialRamdisk = pkgs.makeInitrdNG {
name = "initrd-${kernel-name}";
inherit (config.boot.initrd) compressor compressorArgs prepend;
Expand Down Expand Up @@ -398,6 +419,9 @@ in {
ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
'';

# Make the system closure available as an environment variable.
"/etc/systemd/system-environment-generators/nixos-environment-generator".source = "${envGenerator}/bin/nixos-environment-generator";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"/etc/systemd/system-environment-generators/nixos-environment-generator".source = "${envGenerator}/bin/nixos-environment-generator";
"/etc/systemd/system-environment-generators/nixos-environment-generator".source = lib.getExe envGenerator;

TIL that meta.mainProgram is prefilled for writeShellScriptBin


"/lib/modules".source = "${modulesClosure}/lib/modules";
"/lib/firmware".source = "${modulesClosure}/lib/firmware";

Expand Down Expand Up @@ -490,28 +514,11 @@ in {
set -uo pipefail
export PATH="/bin:${cfg.package.util-linux}/bin"

# Figure out what closure to boot
closure=
for o in $(< /proc/cmdline); do
case $o in
init=*)
IFS== read -r -a initParam <<< "$o"
closure="$(dirname "''${initParam[1]}")"
;;
esac
done

# Sanity check
if [ -z "''${closure:-}" ]; then
echo 'No init= parameter on the kernel command line' >&2
exit 1
fi

# If we are not booting a NixOS closure (e.g. init=/bin/sh),
# we don't know what root to prepare so we don't do anything
if ! [ -x "/sysroot$(readlink "/sysroot$closure/prepare-root" || echo "$closure/prepare-root")" ]; then
if ! [ -x "/sysroot$(readlink "/sysroot$NIXOS_SYSTEM_CLOSURE/prepare-root" || echo "$NIXOS_SYSTEM_CLOSURE/prepare-root")" ]; then
echo "NEW_INIT=''${initParam[1]}" > /etc/switch-root.conf
echo "$closure does not look like a NixOS installation - not activating"
echo "$NIXOS_SYSTEM_CLOSURE does not look like a NixOS installation - not activating"
exit 0
fi
echo 'NEW_INIT=' > /etc/switch-root.conf
Expand All @@ -524,7 +531,7 @@ in {

# Initialize the system
export IN_NIXOS_SYSTEMD_STAGE1=true
exec chroot /sysroot $closure/prepare-root
exec chroot /sysroot $NIXOS_SYSTEM_CLOSURE/prepare-root
'';
};

Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ in {
initrdNetwork = handleTest ./initrd-network.nix {};
initrd-secrets = handleTest ./initrd-secrets.nix {};
initrd-secrets-changing = handleTest ./initrd-secrets-changing.nix {};
initrd-verify = handleTest ./initrd-verify.nix {};
input-remapper = handleTest ./input-remapper.nix {};
inspircd = handleTest ./inspircd.nix {};
installer = handleTest ./installer.nix {};
Expand Down
50 changes: 50 additions & 0 deletions nixos/tests/initrd-verify.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import ./make-test-python.nix ({ ... }: {
name = "initrd-verify";

nodes.machine = {
boot.initrd.systemd.enable = true;
boot.initrd.verify = {
enable = true;
trustedPublicKeys = [(builtins.readFile ./initrd-verify.pub)];
signing = {
enable = true;
keyFile = "${./initrd-verify.secret}";
};
};

# We want to detect failure in stage 1 ourselves.
testing.initrdBackdoor = true;
boot.initrd.systemd.services.panic-on-fail.enable = false;

virtualisation.useBootLoader = true;
virtualisation.useEFIBoot = true;
boot.loader.timeout = 0;
boot.loader.systemd-boot.enable = true;
};

testScript = ''
machine.switch_root()
machine.wait_for_unit("multi-user.target")
machine.succeed(
"mount -o remount,rw /nix/store",
# Invalidate a dependency (systemd) to verify recursion
"echo invalidate > /run/current-system/systemd/invalidated",
"sync",
)
machine.shutdown()
machine.start()
def check_failed(_) -> bool:
info = machine.get_unit_info("verify-store.service")
state = info["ActiveState"]
if state == "failed":
return True
else:
status, jobs = machine.systemctl("list-jobs --full 2>&1")
if "No jobs" in jobs:
raise Exception('verify-store.service never failed as it should have.')
else:
return False
with machine.nested("Waiting for verification to fail"):
retry(check_failed)
'';
})
1 change: 1 addition & 0 deletions nixos/tests/initrd-verify.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
not-secret:mS/WQyNhrsO6hMN/3IwbSoIda/DCaKkD1lJpC+II2eY=
1 change: 1 addition & 0 deletions nixos/tests/initrd-verify.secret
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
not-secret:0f9ZmCOeRdEa5HitWkOOfQDJQcrif06V+apZpSSHJ/WZL9ZDI2Guw7qEw3/cjBtKgh1r8MJoqQPWUmkL4gjZ5g==
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix the editorconfig check we probably need to add a line sort of like

insert_final_newline = unset

Loading