From f983dd80a50bde431787b130267d87f721797496 Mon Sep 17 00:00:00 2001 From: vdbewout Date: Sun, 8 Oct 2023 09:21:02 +0200 Subject: [PATCH] sopsFiles Allow multiple sopsFiles for easier secret sharing across multiple configurations --- modules/sops/default.nix | 42 ++++++++++++++++++++++++++- pkgs/sops-install-secrets/main.go | 47 +++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/modules/sops/default.nix b/modules/sops/default.nix index a3f1c965..5fccafc0 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -12,7 +12,10 @@ let secretType = types.submodule ({ config, ... }: { config = { sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; + #sopsFiles = lib.mkOptionDefault cfg.defaultSopsFiles; + sopsFiles = lib.mkOptionDefault []; sopsFileHash = mkOptionDefault (optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); + sopsFilesHash = mkOptionDefault (optionals cfg.validateSopsFiles (forEach config.sopsFiles (builtins.hashFile "sha256"))); }; options = { name = mkOption { @@ -77,6 +80,13 @@ let Sops file the secret is loaded from. ''; }; + sopsFiles = mkOption { + type = types.listOf types.path; + defaultText = "\${config.sops.defaultSopsFile}"; + description = '' + Sops file the secret is loaded from. + ''; + }; sopsFileHash = mkOption { type = types.str; readOnly = true; @@ -84,6 +94,13 @@ let Hash of the sops file, useful in . ''; }; + sopsFilesHash = mkOption { + type = types.listOf types.str; + readOnly = true; + description = '' + Hash of the sops file, useful in . + ''; + }; restartUnits = mkOption { type = types.listOf types.str; default = [ ]; @@ -172,6 +189,13 @@ in { Default sops file used for all secrets. ''; }; + defaultSopsFiles = mkOption { + type = types.listOf types.path; + default = [ cfg.defaulSopsFile ]; + description = '' + Default sops file used for all secrets. + ''; + }; defaultSopsFormat = mkOption { type = types.str; @@ -331,7 +355,7 @@ in { assertion = (filterAttrs (_: v: v.owner != "root" || v.group != "root") secretsForUsers) == {}; message = "neededForUsers cannot be used for secrets that are not root-owned"; }] ++ optionals cfg.validateSopsFiles ( - concatLists (mapAttrsToList (name: secret: [{ + (concatLists (mapAttrsToList (name: secret: [{ assertion = builtins.pathExists secret.sopsFile; message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${strings.escapeNixIdentifier name}.sopsFile"; } { @@ -340,6 +364,22 @@ in { (builtins.isString secret.sopsFile && hasPrefix builtins.storeDir secret.sopsFile); message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; }]) cfg.secrets) + ) ++ (concatLists (mapAttrsToList + (name: secret: + concatMap + (sopsFile: [{ + assertion = builtins.pathExists sopsFile; + message = "Cannot find path '${sopsFile}' set in sops.secrets.${strings.escapeNixIdentifier name}.sopsFiles"; + } + { + assertion = + builtins.isPath sopsFile || + (builtins.isString sopsFile && hasPrefix builtins.storeDir sopsFile); + message = "'${sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; + }]) + (toList (if (secret.sopsFiles == null) then [ ] else secret.sopsFiles))) + cfg.secrets) + ) ); sops.environment.SOPS_GPG_EXEC = mkIf (cfg.gnupg.home != null) (mkDefault "${pkgs.gnupg}/bin/gpg"); diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index d02c32c1..2c50e10d 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -30,6 +30,7 @@ type secret struct { Owner string `json:"owner"` Group string `json:"group"` SopsFile string `json:"sopsFile"` + SopsFiles []string `json:"sopsFiles"` Format FormatType `json:"format"` Mode string `json:"mode"` RestartUnits []string `json:"restartUnits"` @@ -257,12 +258,16 @@ func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, er return strVal, nil } -func decryptSecret(s *secret, sourceFiles map[string]plainData) error { - sourceFile := sourceFiles[s.SopsFile] +func decryptSecretInner(s *secret, sopsFile *string, sourceFiles map[string]plainData) error { + if sopsFile == nil { + sopsFile = &s.SopsFile + } + + sourceFile := sourceFiles[*sopsFile] if sourceFile.data == nil || sourceFile.binary == nil { - plain, err := decrypt.File(s.SopsFile, string(s.Format)) + plain, err := decrypt.File(*sopsFile, string(s.Format)) if err != nil { - return fmt.Errorf("Failed to decrypt '%s': %w", s.SopsFile, err) + return fmt.Errorf("Failed to decrypt '%s': %w", *sopsFile, err) } switch s.Format { @@ -270,14 +275,14 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { sourceFile.binary = plain case Yaml: if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err) + return fmt.Errorf("Cannot parse yaml of '%s': %w", *sopsFile, err) } case Json: if err := json.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + return fmt.Errorf("Cannot parse json of '%s': %w", *sopsFile, err) } default: - return fmt.Errorf("Secret of type %s in %s is not supported", s.Format, s.SopsFile) + return fmt.Errorf("Secret of type %s in %s is not supported", s.Format, *sopsFile) } } switch s.Format { @@ -286,11 +291,35 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { case Yaml, Json: strVal, err := recurseSecretKey(sourceFile.data, s.Key) if err != nil { - return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) + return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, *sopsFile, err) } s.value = []byte(strVal) } - sourceFiles[s.SopsFile] = sourceFile + sourceFiles[*sopsFile] = sourceFile + + return nil +} + +func decryptSecret(s *secret, sourceFiles map[string]plainData) error { + if len(s.SopsFiles) == 0 { + if err := decryptSecretInner(s, &s.SopsFile, sourceFiles); err != nil { + return err + } + } else { + // Check SopsFiles in reverse order and use first matched secret + for ii := len(s.SopsFiles) - 1; ii >= 0; ii-- { + if err := decryptSecretInner(s, &s.SopsFiles[ii], sourceFiles); err != nil { + if ii == 0 { + // Secret not found in any of the SopsFiles + // TODO: print SopsFiles + return fmt.Errorf("secret %s not found in SopsFiles", s.Name) + } + } else { + // Found the secret + break + } + } + } return nil }