diff options
| author | Seth Flynn <[email protected]> | 2025-02-07 01:57:46 -0500 |
|---|---|---|
| committer | Seth Flynn <[email protected]> | 2025-02-07 22:52:00 -0500 |
| commit | af5215082ef899f01180b6c350f22ce9243830d8 (patch) | |
| tree | 44a12f34e6e1f501ecba8a05658b4407aaa46756 | |
| parent | 7baba7111bffbf3dafc41c66d7cc138526294f53 (diff) | |
secrets: use module to evaluate
| -rw-r--r-- | secrets/agenix-configuration.nix | 25 | ||||
| -rw-r--r-- | secrets/eval-agenix.nix | 13 | ||||
| -rw-r--r-- | secrets/module.nix | 210 | ||||
| -rw-r--r-- | secrets/secrets.nix | 41 | ||||
| -rw-r--r-- | secrets/toSecrets.nix | 35 |
5 files changed, 251 insertions, 73 deletions
diff --git a/secrets/agenix-configuration.nix b/secrets/agenix-configuration.nix new file mode 100644 index 0000000..d093d4e --- /dev/null +++ b/secrets/agenix-configuration.nix @@ -0,0 +1,25 @@ +{ config, lib, ... }: + +{ + rootDirectory = ./.; + + recipients = { + # Catch-all + default = [ config.recipients.getchoo ]; + + # Users + getchoo = "age1zyqu6zkvl0rmlejhm5auzmtflfy4pa0fzwm0nzy737fqrymr7crsqrvnhs"; + + # Machines + atlas = "age18eu3ya4ucd2yzdrpkpg7wrymrxewt8j3zj2p2rqgcjeruacp0dgqryp39z"; + glados = "age1n7tyxx63wpgnmwkzn7dmkm62jxel840rk3ye3vsultrszsfrwuzsawdzhq"; + glados-wsl = "age1ffqfq3azqfwxwtxnfuzzs0y566a7ydgxce4sqxjqzw8yexc2v4yqfr55vr"; + }; + + secrets = lib.mapAttrsToList (hostname: pubkey: { + regex = "^${hostname}\/.*\.age$"; + recipients = { + ${hostname} = pubkey; + }; + }) { inherit (config.recipients) atlas glados glados-wsl; }; +} diff --git a/secrets/eval-agenix.nix b/secrets/eval-agenix.nix new file mode 100644 index 0000000..f02577f --- /dev/null +++ b/secrets/eval-agenix.nix @@ -0,0 +1,13 @@ +let + lib = import <nixpkgs/lib>; +in + +args: + +lib.evalModules ( + { + modules = args.modules ++ [ ./module.nix ]; + class = "agenix"; + } + // lib.removeAttrs args [ "modules" ] +) diff --git a/secrets/module.nix b/secrets/module.nix new file mode 100644 index 0000000..2244d15 --- /dev/null +++ b/secrets/module.nix @@ -0,0 +1,210 @@ +{ config, lib, ... }: + +let + inherit (lib) + attrValues + filter + flip + match + mkOption + recursiveUpdate + removeAttrs + removePrefix + types + ; + + inherit (lib.filesystem) listFilesRecursive; + + cfg = config; + + toRelativePath = filePath: removePrefix (toString cfg.rootDirectory + "/") (toString filePath); + + handleSecretRegex = + secret: + + let + secretRegexMatches = str: match secret.regex str != null; + matched = filter (filePath: secretRegexMatches (toRelativePath filePath)) secretFiles; + in + + map ( + path: + recursiveUpdate secret { + path = toRelativePath path; + } + ) matched; + + secretFiles = listFilesRecursive cfg.rootDirectory; + + failedAssertions = map (x: x.message) (filter (x: !x.assertion) cfg.assertions); + assertionsMessage = "\nFailed assertions:\n${lib.concatLines (map (x: "- " + x) failedAssertions)}"; + + agenixSecretSubmodule = { + freeformType = lib.types.attrsOf types.anything; + + options = { + publicKeys = mkOption { + type = types.listOf types.str; + defaultText = "config.recipients.default"; + description = "List of public keys a given secret is encrypted for."; + }; + }; + }; + + recipientsSubmodule = { + freeformType = types.attrsOf types.str; + + options = { + default = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Recipetents added to secrets by default."; + }; + }; + }; + + recipientsOptionSubmodule = { + options = { + recipients = mkOption { + type = types.submodule recipientsSubmodule; + default = { }; + description = "Recipetents that files will be encrypted for."; + }; + }; + }; + + secretSettingsSubmodule = + { config, ... }: + + { + imports = [ recipientsOptionSubmodule ]; + + options = { + recipients = mkOption { + # We only use this in the toplevel `config.recipients` + apply = flip removeAttrs [ "default" ]; + }; + + useDefault = lib.mkEnableOption "the default recipients" // { + default = true; + }; + + settings = mkOption { + type = types.submodule agenixSecretSubmodule; + default = { }; + description = '' + Settings for a given secret. + + Loosely documented in the agenix [tutorial](https://github.com/ryantm/agenix#tutorial). + ''; + }; + }; + + # Dogfood `settings` to apply global and per-secret recipients + # Use `mkForce` to override + config = lib.mkMerge [ + { + settings.publicKeys = attrValues config.recipients; + } + + (lib.mkIf config.useDefault { + settings.publicKeys = cfg.recipients.default; + }) + ]; + }; + + secretPathSubmodule = { + imports = [ secretSettingsSubmodule ]; + + options = { + path = mkOption { + type = types.nullOr types.str; + default = null; + description = "Relative path (to `config.rootDirectory`) of a secret"; + }; + + regex = mkOption { + type = types.nullOr types.str; + default = null; + description = "A regex for a given file path relative to `config.rootDirectory`."; + }; + }; + }; + + # TODO: Re-implement when can `types.either (types.submodule ...) (types.submodule ...)` works + # It would be good to avoid the `nullOr` above and assertions below + /* + secretRegexSubmodule = { + imports = [ secretSettingsSubmodule ]; + + options = { + regex = mkOption { + type = types.str; + description = "A regex for a given file path relative to `config.rootDirectory`."; + }; + }; + }; + */ + + buildSubmodule = { + options = { + rules = mkOption { + type = types.attrsOf (types.submodule agenixSecretSubmodule); + readOnly = true; + description = "Final rules passed to the agenix CLi."; + }; + }; + }; +in + +{ + imports = [ + recipientsOptionSubmodule + + <nixpkgs/nixos/modules/misc/assertions.nix> + ]; + + options = { + rootDirectory = mkOption { + type = types.path; + description = "Root directory containing agenix secrets."; + }; + + secrets = mkOption { + # TODO: Use `types.listOf (types.either ...)` + type = types.listOf (types.submodule secretPathSubmodule); + default = { }; + description = "Submodule describing agenix secrets."; + }; + + # Outputs + build = mkOption { + type = types.submodule buildSubmodule; + default = { }; + apply = build: if failedAssertions != [ ] then throw assertionsMessage else build; + internal = true; + }; + }; + + config = { + assertions = map (secret: { + assertion = secret.path != null || secret.regex != null; + message = "One of `path` or `regex` must be set"; + }) cfg.secrets; + + build = { + # TODO: Harvest all secrets + rules = lib.listToAttrs ( + lib.concatMap ( + secret: + + let + normalized = if secret.regex != null then handleSecretRegex secret else [ secret ]; + in + + map (secret': lib.nameValuePair secret'.path secret'.settings) normalized + ) cfg.secrets + ); + }; + }; +} diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 5c3ae1c..3cd930d 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -1,38 +1,3 @@ -let - toSecrets = import ./toSecrets.nix; - - owners = { - getchoo = "age1zyqu6zkvl0rmlejhm5auzmtflfy4pa0fzwm0nzy737fqrymr7crsqrvnhs"; - }; - - hosts = { - glados = { - owner = owners.getchoo; - pubkey = "age1n7tyxx63wpgnmwkzn7dmkm62jxel840rk3ye3vsultrszsfrwuzsawdzhq"; - files = [ - "sethPassword.age" - "macstadium.age" - ]; - }; - - glados-wsl = { - pubkey = "age1ffqfq3azqfwxwtxnfuzzs0y566a7ydgxce4sqxjqzw8yexc2v4yqfr55vr"; - owner = owners.getchoo; - inherit (hosts.glados) files; - }; - - atlas = { - pubkey = "age18eu3ya4ucd2yzdrpkpg7wrymrxewt8j3zj2p2rqgcjeruacp0dgqryp39z"; - owner = owners.getchoo; - files = [ - "userPassword.age" - "miniflux.age" - "nixpkgs-tracker-bot.age" - "tailscaleAuthKey.age" - "cloudflaredCreds.age" - "teawieBot.age" - ]; - }; - }; -in -toSecrets hosts +(import ./eval-agenix.nix { + modules = [ ./agenix-configuration.nix ]; +}).config.build.rules diff --git a/secrets/toSecrets.nix b/secrets/toSecrets.nix deleted file mode 100644 index 3ae33f1..0000000 --- a/secrets/toSecrets.nix +++ /dev/null @@ -1,35 +0,0 @@ -hosts: -let - # Find any public keys from a given system's attributes - findPubkeysIn = - host: - builtins.filter (item: item != null) [ - (host.pubkey or null) - (host.owner or null) - ]; - - # Memorize them for later - publicKeysFor = builtins.mapAttrs (_: findPubkeysIn) hosts; - - # Map secret files meant for `hostname` to an attribute set containing - # their relative path and public keys - # - # See https://github.com/ryantm/agenix/blob/de96bd907d5fbc3b14fc33ad37d1b9a3cb15edc6/README.md#tutorial - # as a reference to what this outputs - secretsFrom = - hostname: host: - builtins.listToAttrs ( - map (file: { - name = "${hostname}/${file}"; - value = { - publicKeys = publicKeysFor.${hostname}; - }; - - }) host.files - ); - - # Memorize them all - secretsFor = builtins.mapAttrs secretsFrom hosts; -in -# Now merge them all into one attribute set -builtins.foldl' (acc: secrets: acc // secrets) { } (builtins.attrValues secretsFor) |
