summaryrefslogtreecommitdiff
path: root/secrets/module.nix
diff options
context:
space:
mode:
authorSeth Flynn <[email protected]>2025-02-07 01:57:46 -0500
committerSeth Flynn <[email protected]>2025-02-07 22:52:00 -0500
commitaf5215082ef899f01180b6c350f22ce9243830d8 (patch)
tree44a12f34e6e1f501ecba8a05658b4407aaa46756 /secrets/module.nix
parent7baba7111bffbf3dafc41c66d7cc138526294f53 (diff)
secrets: use module to evaluate
Diffstat (limited to 'secrets/module.nix')
-rw-r--r--secrets/module.nix210
1 files changed, 210 insertions, 0 deletions
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
+ );
+ };
+ };
+}