forked from mirrors_public/oddlama_nix-config
feat: implement external rekeying via nix run ".#rekey"
This commit is contained in:
parent
9d1253c03b
commit
2081ce6585
8 changed files with 105 additions and 77 deletions
24
apps/rekey-output-derivation.nix
Normal file
24
apps/rekey-output-derivation.nix
Normal 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
60
apps/rekey.nix
Normal 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 "[1;31mFailed to rekey secret ${secretName} for ${hostName}![m" ; \
|
||||||
|
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}";
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
networking = {
|
networking = {
|
||||||
hostId = "4313abca";
|
hostId = "4313abca";
|
||||||
hostName = "nom";
|
|
||||||
wireless.iwd.enable = true;
|
wireless.iwd.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
networking = {
|
networking = {
|
||||||
hostId = "49ce3b71";
|
hostId = "49ce3b71";
|
||||||
hostName = "ward";
|
|
||||||
wireless.iwd.enable = true;
|
wireless.iwd.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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 = ''
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue