diff --git a/apps/rekey-output-derivation.nix b/apps/rekey-output-derivation.nix new file mode 100644 index 0000000..258a9ca --- /dev/null +++ b/apps/rekey-output-derivation.nix @@ -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"''; + } diff --git a/apps/rekey.nix b/apps/rekey.nix new file mode 100644 index 0000000..bc69e39 --- /dev/null +++ b/apps/rekey.nix @@ -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}"; + }; +} diff --git a/flake.nix b/flake.nix index 9228f4d..17ed887 100644 --- a/flake.nix +++ b/flake.nix @@ -55,7 +55,7 @@ homeConfigurations = import ./nix/home-manager.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; devShells.default = import ./nix/dev-shell.nix inputs localSystem; @@ -75,5 +75,7 @@ ]; config.allowUnfree = true; }; + + apps = {} // import ./apps/rekey.nix inputs localSystem; }); } diff --git a/hosts/nom/net.nix b/hosts/nom/net.nix index 225c142..556516f 100644 --- a/hosts/nom/net.nix +++ b/hosts/nom/net.nix @@ -1,7 +1,6 @@ { networking = { hostId = "4313abca"; - hostName = "nom"; wireless.iwd.enable = true; }; diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index 9da4728..7f82a24 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -1,7 +1,6 @@ { networking = { hostId = "49ce3b71"; - hostName = "ward"; wireless.iwd.enable = true; }; diff --git a/modules/core/default.nix b/modules/core/default.nix index de4f4a6..02ab213 100644 --- a/modules/core/default.nix +++ b/modules/core/default.nix @@ -28,7 +28,6 @@ in { security.sudo.enable = false; 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.agePlugins = with pkgs; [age-plugin-yubikey]; diff --git a/modules/core/rekey.nix b/modules/core/rekey.nix index 6106bef..c409ba3 100644 --- a/modules/core/rekey.nix +++ b/modules/core/rekey.nix @@ -6,70 +6,16 @@ ... }: 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 - rekeyedSecrets = pkgs.stdenv.mkDerivation rec { - 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}"; + drv = import ../../apps/rekey-output-derivation.nix pkgs config; in mkIf (config.rekey.secrets != {}) { - environment.systemPackages = with pkgs; [rage]; - - # This polkit rule allows the nixbld users to access the pcsc-lite daemon, - # which is necessary to rekey the secrets. - security.polkit.extraConfig = mkIf (elem pkgs.age-plugin-yubikey config.rekey.agePlugins) '' - polkit.addRule(function(action, subject) { - 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; + # Produce a rekeyed age secret for each of the secrets defined in our secrets + age.secrets = mapAttrs (secretName: + flip mergeAttrs { + file = "${drv}/${secretName}"; + }) + config.rekey.secrets; assertions = [ { @@ -88,7 +34,12 @@ with lib; { warnings = let hasGoodSuffix = x: strings.hasSuffix ".age" x || strings.hasSuffix ".pub" x; 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) '' It seems like at least one of your rekey.masterIdentityPaths contains an @@ -115,15 +66,6 @@ with lib; { #example = "age159tavn5rcfnq30zge2jfq4yx60uksz8udndp0g3njzhrns67ca5qq3n0tj"; 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 { type = types.listOf types.path; description = '' diff --git a/nix/nixos.nix b/nix/nixos.nix index 1823139..2561898 100644 --- a/nix/nixos.nix +++ b/nix/nixos.nix @@ -20,10 +20,13 @@ }; }; - genConfiguration = hostname: {hostPlatform, ...}: + genConfiguration = hostName: {hostPlatform, ...}: lib.nixosSystem { 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}; # FIXME: This shouldn't be needed, but is for some reason