diff --git a/default.nix b/default.nix index 2b99e88..877ed64 100644 --- a/default.nix +++ b/default.nix @@ -1,6 +1,4 @@ { pkgs ? import {} }: -rec { - age-nix = pkgs.writeScriptBin "age-nix" '' - exit 0 - ''; +{ + agenix = pkgs.callPackage ./pkgs/agenix.nix {}; } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8263057 --- /dev/null +++ b/flake.lock @@ -0,0 +1,25 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1599148892, + "narHash": "sha256-V76c6DlI0ZZffvbBpxGlpVSpXxZ14QpFHwAvEEujIsY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7ff50a7f7b9a701228f870813fe58f01950f870b", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index b25719a..b076214 100644 --- a/flake.nix +++ b/flake.nix @@ -12,7 +12,7 @@ forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); in { nixosModules.age = import ./modules/age.nix; - # packages = forAllSystems (system: nixpkgs.legacyPackages.${system}.callPackage ./default.nix {}); -# defaultPackage = forAllSystems (system: self.packages.${system}.age-nix); # + packages = forAllSystems (system: nixpkgs.legacyPackages.${system}.callPackage ./default.nix {}); + defaultPackage = forAllSystems (system: self.packages.${system}.agenix); }; } diff --git a/modules/age.nix b/modules/age.nix index c9b5afb..383a85a 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -13,7 +13,6 @@ let chmod ${secretType.mode} "$TMP_FILE" chown ${secretType.owner}:${secretType.group} "$TMP_FILE" mv -f "$TMP_FILE" '${secretType.path}' - ''; installAllSecrets = builtins.concatStringsSep "\n" (map installSecret (builtins.attrValues cfg.secrets)); @@ -27,15 +26,12 @@ let ''; }; file = mkOption { - type = types.either types.str types.path; + type = types.path; description = '' Age file the secret is loaded from. ''; }; - path = assert assertMsg (builtins.pathExists config.file) '' - Cannot find path '${config.file}' set in 'age.secrets."${config._module.args.name}".file' - ''; - mkOption { + path = mkOption { type = types.str; default = "/run/secrets/${config.name}"; description = '' @@ -81,20 +77,15 @@ in { map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys) else []; description = '' - Path to SSH keys to be used as identities in age file decryption. + Path to SSH keys to be used as identities in age decryption. ''; }; }; config = mkIf (cfg.secrets != {}) { assertions = [{ assertion = cfg.sshKeyPaths != []; - message = "Either age.sshKeyPaths must be set."; - }] ++ map (name: let - inherit (cfg.secrets.${name}) file; - in { - assertion = builtins.isPath file; - message = "${file} is not in the nix store. Either add it to the nix store."; - }) (builtins.attrNames cfg.secrets); + message = "age.sshKeyPaths must be set."; + }]; system.activationScripts.setup-secrets = stringAfter [ "users" "groups" ] installAllSecrets; }; diff --git a/pkgs/agenix.nix b/pkgs/agenix.nix new file mode 100644 index 0000000..5c34237 --- /dev/null +++ b/pkgs/agenix.nix @@ -0,0 +1,122 @@ +{writeShellScriptBin, runtimeShell, age, yq-go} : +writeShellScriptBin "agenix" '' +set -euo pipefail +PACKAGE="agenix" + +function show_help () { + echo "$PACKAGE - edit and rekey age secret files" + echo " " + echo "$PACKAGE -e FILE" + echo "$PACKAGE -r" + echo ' ' + echo 'options:' + echo '-h, --help show help' + echo '-e, --edit FILE edits FILE using $EDITOR' + echo '-r, --rekey re-encrypts all secrets with specified recipients' + echo ' ' + echo 'FILE an age-encrypted file' + echo ' ' + echo 'EDITOR environment variable of editor to use when editing FILE' + echo ' ' + echo 'RULES environment variable with path to YAML file specifying recipient public keys.' + echo "Defaults to 'secrets.yaml'" +} + +test $# -eq 0 && (show_help && exit 1) + +REKEY=0 + +while test $# -gt 0; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -e|--edit) + shift + if test $# -gt 0; then + export FILE=$1 + else + echo "no file specified" + exit 1 + fi + shift + ;; + -r|--rekey) + shift + REKEY=1 + ;; + *) + show_help + exit 1 + ;; + esac +done + +RULES=''${RULES:-secrets.yaml} + +function cleanup { + if [ ! -z ''${CLEARTEXT_DIR+x} ] + then + rm -rf "$CLEARTEXT_DIR" + fi + if [ ! -z ''${REENCRYPTED_DIR+x} ] + then + rm -rf "$REENCRYPTED_DIR" + fi +} +trap "cleanup" 0 2 3 15 + +function edit { + FILE=$1 + KEYS=$(${yq-go}/bin/yq r "$RULES" "secrets.(name==$FILE).public_keys.**") + if [ -z "$KEYS" ] + then + >&2 echo "There is no rule for $FILE in $RULES." + exit 1 + fi + + CLEARTEXT_DIR=$(mktemp -d) + CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")" + + if [ -f "$FILE" ] + then + DECRYPT=(--decrypt) + while IFS= read -r key + do + DECRYPT+=(--identity "$key") + done <<<$(find ~/.ssh -maxdepth 1 -type f -not -name "*pub" -not -name "config" -not -name "authorized_keys" -not -name "known_hosts") + DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE") + ${age}/bin/age "''${DECRYPT[@]}" + fi + + $EDITOR "$CLEARTEXT_FILE" + + ENCRYPT=() + while IFS= read -r key + do + ENCRYPT+=(--recipient "$key") + done <<< "$KEYS" + + REENCRYPTED_DIR=$(mktemp -d) + REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")" + + ENCRYPT+=(-o "$REENCRYPTED_FILE") + + cat "$CLEARTEXT_FILE" | ${age}/bin/age "''${ENCRYPT[@]}" + + mv -f "$REENCRYPTED_FILE" "$1" +} + +function rekey { + echo "rekeying..." + FILES=$(${yq-go}/bin/yq r "$RULES" "secrets.*.name") + for FILE in $FILES + do + EDITOR=: edit $FILE + done +} + +[ $REKEY -eq 1 ] && rekey && exit 0 +edit $FILE && exit 0 +'' diff --git a/pkgs/agenix.sh b/pkgs/agenix.sh new file mode 100644 index 0000000..5714afe --- /dev/null +++ b/pkgs/agenix.sh @@ -0,0 +1,49 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash -p age yq-go moreutils + +while test $# -gt 0; do + case "$1" in + -h|--help) + echo "$package - attempt to capture frames" + echo " " + echo "$package [options] application [arguments]" + echo " " + echo "options:" + echo "-h, --help show brief help" + echo "-a, --action=ACTION specify an action to use" + echo "-o, --output-dir=DIR specify a directory to store output in" + exit 0 + ;; + -a) + shift + if test $# -gt 0; then + export PROCESS=$1 + else + echo "no process specified" + exit 1 + fi + shift + ;; + --action*) + export PROCESS=`echo $1 | sed -e 's/^[^=]*=//g'` + shift + ;; + -o) + shift + if test $# -gt 0; then + export OUTPUT=$1 + else + echo "no output dir specified" + exit 1 + fi + shift + ;; + --output-dir*) + export OUTPUT=`echo $1 | sed -e 's/^[^=]*=//g'` + shift + ;; + *) + break + ;; + esac +done