1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-11 07:10:39 +02:00

feat: implement external rekeying via nix run ".#rekey"

This commit is contained in:
oddlama 2023-01-28 20:11:07 +01:00
parent 9d1253c03b
commit 2081ce6585
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
8 changed files with 105 additions and 77 deletions

View file

@ -0,0 +1,24 @@
pkgs: config:
with pkgs.lib;
pkgs.stdenv.mkDerivation rec {
pname = "host-secrets";
version = "1.0";
description = "Rekeyed secrets for this host.";
srcs = mapAttrsToList (_: x: x.file) config.rekey.secrets;
sourcePath = ".";
# Required as input to have the derivation rebuild if this changes
hostPubkey = let
pubkey = config.rekey.hostPubkey;
in
if isPath pubkey
then readFile pubkey
else pubkey;
dontMakeSourcesWritable = true;
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = ''cp -r "/tmp/nix-rekey/${builtins.hashString "sha1" hostPubkey}/." "$out"'';
}

60
apps/rekey.nix Normal file
View file

@ -0,0 +1,60 @@
{
self,
nixpkgs,
...
}: system:
with nixpkgs.lib; let
pkgs = self.pkgs.${system};
toPubkey = pubkey:
if isPath pubkey
then readFile pubkey
else pubkey;
rekeyCommandsForHost = hostName: hostAttrs: let
hostPubkeyStr = toPubkey hostAttrs.config.rekey.hostPubkey;
secretDir = "/tmp/nix-rekey/${builtins.hashString "sha1" hostPubkeyStr}";
rekeyCommand = secretName: secretAttrs: let
masterIdentityArgs = concatMapStrings (x: ''-i "${x}" '') hostAttrs.config.rekey.masterIdentityPaths;
secretOut = "${secretDir}/${secretName}.age";
in ''
echo "Rekeying ${secretName} for host ${hostName}"
${pkgs.rage}/bin/rage ${masterIdentityArgs} -d ${secretAttrs.file} \
| ${pkgs.rage}/bin/rage -r "${hostPubkeyStr}" -o "${secretOut}" -e \
|| { \
echo "Failed to rekey secret ${secretName} for ${hostName}!" ; \
echo "This is a dummy replacement value. The actual secret could not be rekeyed." \
| ${pkgs.rage}/bin/rage -r "${hostPubkeyStr}" -o "${secretOut}" -e ; \
}
'';
in ''
mkdir -p "${secretDir}"
# Enable selected age plugins for this host
export PATH="$PATH${concatMapStrings (x: ":${x}/bin") hostAttrs.config.rekey.agePlugins}"
${concatStringsSep "\n" (mapAttrsToList rekeyCommand hostAttrs.config.rekey.secrets)}
'';
rekeyScript = pkgs.writeShellScript "rekey" ''
set -euo pipefail
${concatStringsSep "\n" (mapAttrsToList rekeyCommandsForHost self.nixosConfigurations)}
nix run --extra-sandbox-paths /tmp "${../.}#rekey-save-outputs";
'';
rekeySaveOutputsScript = let
copyHostSecrets = hostName: hostAttrs: let
drv = import ./rekey-output-derivation.nix pkgs hostAttrs.config;
in ''echo "Stored rekeyed secrets for ${hostAttrs.config.networking.hostName} in ${drv}"'';
in
pkgs.writeShellScript "rekey-save-outputs" ''
set -euo pipefail
${concatStringsSep "\n" (mapAttrsToList copyHostSecrets self.nixosConfigurations)}
'';
in {
rekey = {
type = "app";
program = "${rekeyScript}";
};
rekey-save-outputs = {
type = "app";
program = "${rekeySaveOutputsScript}";
};
}

View file

@ -55,7 +55,7 @@
homeConfigurations = import ./nix/home-manager.nix inputs; homeConfigurations = import ./nix/home-manager.nix inputs;
nixosConfigurations = import ./nix/nixos.nix inputs; nixosConfigurations = import ./nix/nixos.nix inputs;
} }
// flake-utils.lib.eachSystem ["aarch64-linux" "x86_64-linux"] (localSystem: { // flake-utils.lib.eachSystem ["aarch64-linux" "x86_64-linux"] (localSystem: rec {
checks = import ./nix/checks.nix inputs localSystem; checks = import ./nix/checks.nix inputs localSystem;
devShells.default = import ./nix/dev-shell.nix inputs localSystem; devShells.default = import ./nix/dev-shell.nix inputs localSystem;
@ -75,5 +75,7 @@
]; ];
config.allowUnfree = true; config.allowUnfree = true;
}; };
apps = {} // import ./apps/rekey.nix inputs localSystem;
}); });
} }

View file

@ -1,7 +1,6 @@
{ {
networking = { networking = {
hostId = "4313abca"; hostId = "4313abca";
hostName = "nom";
wireless.iwd.enable = true; wireless.iwd.enable = true;
}; };

View file

@ -1,7 +1,6 @@
{ {
networking = { networking = {
hostId = "49ce3b71"; hostId = "49ce3b71";
hostName = "ward";
wireless.iwd.enable = true; wireless.iwd.enable = true;
}; };

View file

@ -28,7 +28,6 @@ in {
security.sudo.enable = false; security.sudo.enable = false;
rekey.hostPubkey = ../../secrets/pubkeys + "/${config.networking.hostName}.pub"; rekey.hostPubkey = ../../secrets/pubkeys + "/${config.networking.hostName}.pub";
rekey.hostPrivkey = lib.head (map (e: e.path) (lib.filter (e: e.type == "ed25519") config.services.openssh.hostKeys));
rekey.masterIdentityPaths = [../../secrets/yk1-nix-rage.pub]; rekey.masterIdentityPaths = [../../secrets/yk1-nix-rage.pub];
rekey.agePlugins = with pkgs; [age-plugin-yubikey]; rekey.agePlugins = with pkgs; [age-plugin-yubikey];

View file

@ -6,70 +6,16 @@
... ...
}: }:
with lib; { with lib; {
# TODO add /run to sandbox only for THIS derivation
# TODO interactiveness how
# TODO yubikeySupport=true convenience option
# TODO rekeyed secrets should only strip store path, not do basename
config = let config = let
rekeyedSecrets = pkgs.stdenv.mkDerivation rec { drv = import ../../apps/rekey-output-derivation.nix pkgs config;
pname = "host-secrets";
version = "1.0.0";
description = "Rekeyed secrets for this host.";
allSecrets = mapAttrsToList (_: value: value.file) config.rekey.secrets;
hostPubkeyStr =
if isPath config.rekey.hostPubkey
then readFile config.rekey.hostPubkey
else config.rekey.hostPubkey;
dontMakeSourcesWritable = true;
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = let
masterIdentityArgs = concatMapStrings (x: ''-i "${x}" '') config.rekey.masterIdentityPaths;
rekeyCommand = secret: ''
echo "Rekeying ${secret}" >&2
${pkgs.rage}/bin/rage ${masterIdentityArgs} -d ${secret} \
| ${pkgs.rage}/bin/rage -r "${hostPubkeyStr}" -o "$out/${baseNameOf secret}" -e \
|| { echo 1 > $out/status ; echo "error while rekeying secret!" | ${pkgs.rage}/bin/rage -r "${hostPubkeyStr}" -o "$out/${baseNameOf secret}" -e ; }
'';
in ''
set -euo pipefail
mkdir "$out"
echo 0 > $out/status
# Enable selected age plugins
export PATH="$PATH${concatMapStrings (x: ":${x}/bin") config.rekey.agePlugins}"
${concatStringsSep "\n" (map rekeyCommand allSecrets)}
'';
};
rekeyedSecretPath = secret: "${rekeyedSecrets}/${baseNameOf secret}";
in in
mkIf (config.rekey.secrets != {}) { mkIf (config.rekey.secrets != {}) {
environment.systemPackages = with pkgs; [rage]; # Produce a rekeyed age secret for each of the secrets defined in our secrets
age.secrets = mapAttrs (secretName:
# This polkit rule allows the nixbld users to access the pcsc-lite daemon, flip mergeAttrs {
# which is necessary to rekey the secrets. file = "${drv}/${secretName}";
security.polkit.extraConfig = mkIf (elem pkgs.age-plugin-yubikey config.rekey.agePlugins) '' })
polkit.addRule(function(action, subject) { config.rekey.secrets;
if ((action.id == "org.debian.pcsc-lite.access_pcsc" || action.id == "org.debian.pcsc-lite.access_card") &&
subject.user.match(/^nixbld\d+$/))
return polkit.Result.YES;
});
'';
age.secrets =
# Produce a rekeyed age secret for each of the secrets defined in our secrets
mapAttrs (_:
mapAttrs (name: value:
if name == "file"
then rekeyedSecretPath value
else value))
config.rekey.secrets;
assertions = [ assertions = [
{ {
@ -88,7 +34,12 @@ with lib; {
warnings = let warnings = let
hasGoodSuffix = x: strings.hasSuffix ".age" x || strings.hasSuffix ".pub" x; hasGoodSuffix = x: strings.hasSuffix ".age" x || strings.hasSuffix ".pub" x;
in in
optional (toInt (readFile "${rekeyedSecrets}/status") == 1) "Failed to rekey secrets! Run nix log ${rekeyedSecrets}.drv for more information." # drv.drvPath doesn't force evaluation, which allows the warning to be displayed
# before the error occurs
optional (!pathExists (removeSuffix ".drv" drv.drvPath)) ''
The secrets have not yet been rekeyed!
Be sure to run `nix run ".#rekey"` after changing your secrets!
''
++ optional (!all hasGoodSuffix config.rekey.masterIdentityPaths) ++ optional (!all hasGoodSuffix config.rekey.masterIdentityPaths)
'' ''
It seems like at least one of your rekey.masterIdentityPaths contains an It seems like at least one of your rekey.masterIdentityPaths contains an
@ -115,15 +66,6 @@ with lib; {
#example = "age159tavn5rcfnq30zge2jfq4yx60uksz8udndp0g3njzhrns67ca5qq3n0tj"; #example = "age159tavn5rcfnq30zge2jfq4yx60uksz8udndp0g3njzhrns67ca5qq3n0tj";
example = /etc/ssh/ssh_host_ed25519_key.pub; example = /etc/ssh/ssh_host_ed25519_key.pub;
}; };
rekey.hostPrivkey = mkOption {
# Str to prevent privkeys from entering the nix store
type = types.str;
description = ''
The age identity (private key) that should be used to decrypt the secrets on the target machine.
This corresponds to age.identityPaths and must match the pubkey set in rekey.hostPubkey.
'';
example = head (map (e: e.path) (filter (e: e.type == "ed25519") config.services.openssh.hostKeys));
};
rekey.masterIdentityPaths = mkOption { rekey.masterIdentityPaths = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
description = '' description = ''

View file

@ -20,10 +20,13 @@
}; };
}; };
genConfiguration = hostname: {hostPlatform, ...}: genConfiguration = hostName: {hostPlatform, ...}:
lib.nixosSystem { lib.nixosSystem {
modules = [ modules = [
(../hosts + "/${hostname}") (../hosts + "/${hostName}")
# Set hostName to same value as key in nixosConfigurations
{networking.hostName = hostName;}
# Use correct pkgs definition
{ {
nixpkgs.pkgs = self.pkgs.${hostPlatform}; nixpkgs.pkgs = self.pkgs.${hostPlatform};
# FIXME: This shouldn't be needed, but is for some reason # FIXME: This shouldn't be needed, but is for some reason