From abcc9993677200e6d3d3348cc8d4ad23cbd848b4 Mon Sep 17 00:00:00 2001 From: vdbewout Date: Tue, 10 Oct 2023 21:54:43 +0200 Subject: [PATCH] deprecate sopsFile --- modules/sops/default.nix | 64 +++-------- pkgs/sops-install-secrets/main.go | 137 +++++++++++------------ pkgs/sops-install-secrets/main_test.go | 18 +-- pkgs/sops-install-secrets/nixos-test.nix | 16 +-- 4 files changed, 95 insertions(+), 140 deletions(-) diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 5fccafc..cee0f86 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -11,10 +11,7 @@ let secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; 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}"); + sopsFiles = lib.mkOptionDefault cfg.defaultSopsFiles; sopsFilesHash = mkOptionDefault (optionals cfg.validateSopsFiles (forEach config.sopsFiles (builtins.hashFile "sha256"))); }; options = { @@ -73,32 +70,18 @@ let Group of the file. ''; }; - sopsFile = mkOption { - type = types.path; - defaultText = "\${config.sops.defaultSopsFile}"; - description = '' - Sops file the secret is loaded from. - ''; - }; sopsFiles = mkOption { - type = types.listOf types.path; - defaultText = "\${config.sops.defaultSopsFile}"; + type = types.nonEmptyListOf types.path; + defaultText = "\${config.sops.defaultSopsFiles}"; description = '' - Sops file the secret is loaded from. - ''; - }; - sopsFileHash = mkOption { - type = types.str; - readOnly = true; - description = '' - Hash of the sops file, useful in . + Sops files the secret is loaded from. ''; }; sopsFilesHash = mkOption { - type = types.listOf types.str; + type = types.nonEmptyListOf types.str; readOnly = true; description = '' - Hash of the sops file, useful in . + Hash of the sops files, useful in . ''; }; restartUnits = mkOption { @@ -183,15 +166,8 @@ in { ''; }; - defaultSopsFile = mkOption { - type = types.path; - description = '' - Default sops file used for all secrets. - ''; - }; defaultSopsFiles = mkOption { - type = types.listOf types.path; - default = [ cfg.defaulSopsFile ]; + type = types.nonEmptyListOf types.path; description = '' Default sops file used for all secrets. ''; @@ -342,6 +318,7 @@ in { ./templates (mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ]) (mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ]) + (mkRemovedOptionModule [ "sops" "defaultSopsFile" ] ''use `sops.defaultSopsFiles` instead'') ]; config = mkMerge [ (mkIf (cfg.secrets != {}) { @@ -355,31 +332,20 @@ 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: [{ - assertion = builtins.pathExists secret.sopsFile; - message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${strings.escapeNixIdentifier name}.sopsFile"; - } { - assertion = - builtins.isPath secret.sopsFile || - (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 + concatLists (mapAttrsToList (name: secret: concatMap (sopsFile: [{ assertion = builtins.pathExists sopsFile; message = "Cannot find path '${sopsFile}' set in sops.secrets.${strings.escapeNixIdentifier name}.sopsFiles"; - } - { - assertion = + } { + 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))) + (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"; + }]) + 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 a9da42f..9c91acb 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -29,7 +29,6 @@ type secret struct { Path string `json:"path"` Owner string `json:"owner"` Group string `json:"group"` - SopsFile string `json:"sopsFile"` SopsFiles []string `json:"sopsFiles"` Format FormatType `json:"format"` Mode string `json:"mode"` @@ -258,69 +257,48 @@ func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, er return strVal, nil } -func decryptSecretInner(s *secret, sopsFile *string, sourceFiles map[string]plainData) error { - if sopsFile == nil { - sopsFile = &s.SopsFile - } +func decryptSecret(s *secret, sourceFiles map[string]plainData) error { + for i := len(s.SopsFiles) - 1; i >= 0; i-- { + sourceFile := sourceFiles[s.SopsFiles[i]] + if sourceFile.data == nil || sourceFile.binary == nil { + plain, err := decrypt.File(s.SopsFiles[i], string(s.Format)) + if err != nil { + return fmt.Errorf("Failed to decrypt '%s': %w", s.SopsFiles[i], err) + } - sourceFile := sourceFiles[*sopsFile] - if sourceFile.data == nil || sourceFile.binary == nil { - plain, err := decrypt.File(*sopsFile, string(s.Format)) - if err != nil { - return fmt.Errorf("Failed to decrypt '%s': %w", *sopsFile, err) + switch s.Format { + case Binary, Dotenv, Ini: + sourceFile.binary = plain + case Yaml: + if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { + return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFiles[i], err) + } + case Json: + if err := json.Unmarshal(plain, &sourceFile.data); err != nil { + return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFiles[i], err) + } + default: + return fmt.Errorf("Secret of type %s in %s is not supported", s.Format, s.SopsFiles[i]) + } } - switch s.Format { case Binary, Dotenv, Ini: - sourceFile.binary = plain - case Yaml: - if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("Cannot parse yaml of '%s': %w", *sopsFile, err) + s.value = sourceFile.binary + case Yaml, Json: + strVal, err := recurseSecretKey(sourceFile.data, s.Key) + if err != nil { + continue; } - case Json: - if err := json.Unmarshal(plain, &sourceFile.data); err != nil { - 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, *sopsFile) + s.value = []byte(strVal) } - } - switch s.Format { - case Binary, Dotenv, Ini: - s.value = sourceFile.binary - 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, *sopsFile, err) - } - s.value = []byte(strVal) - } - 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 - } - } + sourceFiles[s.SopsFiles[i]] = sourceFile + // Secret found + return nil } - return nil + + // Secret not found in any of the SopsFiles + return fmt.Errorf("secret %s in %v is not valid", s.Name, s.SopsFiles) } func decryptSecrets(secrets []secret) error { @@ -424,14 +402,14 @@ func lookupKeysGroup() (int, error) { return 0, fmt.Errorf("Can't find group 'keys' nor 'nogroup' (%w).", err2) } -func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { +func (app *appContext) loadSopsFile(s *secret, sopsFileIndex int) (*secretFile, error) { if app.checkMode == Manifest { return &secretFile{firstSecret: s}, nil } - cipherText, err := os.ReadFile(s.SopsFile) + cipherText, err := os.ReadFile(s.SopsFiles[sopsFileIndex]) if err != nil { - return nil, fmt.Errorf("Failed reading %s: %w", s.SopsFile, err) + return nil, fmt.Errorf("Failed reading %s: %w", SopsFile, err) } var keys map[string]interface{} @@ -439,17 +417,17 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { switch s.Format { case Binary: if err := json.Unmarshal(cipherText, &keys); err != nil { - return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFiles[sopsFileIndex], err) } return &secretFile{cipherText: cipherText, firstSecret: s}, nil case Yaml: if err := yaml.Unmarshal(cipherText, &keys); err != nil { - return nil, fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err) + return nil, fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFiles[sopsFileIndex], err) } case Dotenv: env, err := godotenv.Unmarshal(string(cipherText)) if err != nil { - return nil, fmt.Errorf("Cannot parse dotenv of '%s': %w", s.SopsFile, err) + return nil, fmt.Errorf("Cannot parse dotenv of '%s': %w", s.SopsFiles[sopsFileIndex], err) } keys = map[string]interface{}{} for k, v := range env { @@ -457,7 +435,7 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { } case Json: if err := json.Unmarshal(cipherText, &keys); err != nil { - return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFiles[sopsFileIndex], err) } } @@ -470,14 +448,14 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { if file.firstSecret.Format != s.Format { - return fmt.Errorf("secret %s defined the format of %s as %s, but it was specified as %s in %s before", - s.Name, s.SopsFile, s.Format, + return fmt.Errorf("secret %s defined the format of %v as %s, but it was specified as %s in %s before", + s.Name, s.SopsFiles, s.Format, file.firstSecret.Format, file.firstSecret.Name) } if app.checkMode != Manifest && (!(s.Format == Binary || s.Format == Dotenv || s.Format == Ini)) { _, err := recurseSecretKey(file.keys, 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: %v", s.Name, s.SopsFiles, err) } } return nil @@ -524,17 +502,28 @@ func (app *appContext) validateSecret(secret *secret) error { return fmt.Errorf("Unsupported format %s for secret %s", secret.Format, secret.Name) } - file, ok := app.secretFiles[secret.SopsFile] - if !ok { - maybeFile, err := app.loadSopsFile(secret) - if err != nil { - return err + files := []secretFile {}; + for index, sopsFile := range secret.SopsFiles { + file, ok := app.secretFiles[sopsFile] + if !ok { + maybeFile, err := app.loadSopsFile(secret, index) + if err != nil { + return err + } + app.secretFiles[sopsFile] = *maybeFile + files = append(files, *maybeFile) + file = *maybeFile } - app.secretFiles[secret.SopsFile] = *maybeFile - file = *maybeFile + files = append(files, file) } - return app.validateSopsFile(secret, &file) + for i := len(files) - 1; i >= 0; i-- { + err := app.validateSopsFile(secret, &files[i]) + if err != nil && i == 0 { + return err + } + } + return nil; } func (app *appContext) validateManifest() error { diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 7c4ef92..c440e7a 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -104,7 +104,7 @@ func testGPG(t *testing.T) { Key: "test_key", Owner: "nobody", Group: "nogroup", - SopsFile: path.Join(assets, "secrets.yaml"), + SopsFiles: []string{path.Join(assets, "secrets.yaml")}, Path: path.Join(testdir.path, "test-target"), Mode: "0400", RestartUnits: []string{"affected-service"}, @@ -118,14 +118,14 @@ func testGPG(t *testing.T) { jsonSecret.Owner = "root" jsonSecret.Format = "json" jsonSecret.Group = "root" - jsonSecret.SopsFile = path.Join(assets, "secrets.json") + jsonSecret.SopsFiles = []string{path.Join(assets, "secrets.json")} jsonSecret.Path = path.Join(testdir.secretsPath, "test2") jsonSecret.Mode = "0700" binarySecret = yamlSecret binarySecret.Name = "test3" binarySecret.Format = "binary" - binarySecret.SopsFile = path.Join(assets, "secrets.bin") + binarySecret.SopsFiles = []string{path.Join(assets, "secrets.bin")} binarySecret.Path = path.Join(testdir.secretsPath, "test3") dotenvSecret = yamlSecret @@ -133,7 +133,7 @@ func testGPG(t *testing.T) { dotenvSecret.Owner = "root" dotenvSecret.Group = "root" dotenvSecret.Format = "dotenv" - dotenvSecret.SopsFile = path.Join(assets, "secrets.env") + dotenvSecret.SopsFiles = []string{path.Join(assets, "secrets.env")} dotenvSecret.Path = path.Join(testdir.secretsPath, "test4") iniSecret = yamlSecret @@ -141,7 +141,7 @@ func testGPG(t *testing.T) { iniSecret.Owner = "root" iniSecret.Group = "root" iniSecret.Format = "ini" - iniSecret.SopsFile = path.Join(assets, "secrets.ini") + iniSecret.SopsFiles = []string{path.Join(assets, "secrets.ini")} iniSecret.Path = path.Join(testdir.secretsPath, "test5") manifest := manifest{ @@ -219,7 +219,7 @@ func testSSHKey(t *testing.T) { Key: "test_key", Owner: "nobody", Group: "nogroup", - SopsFile: path.Join(assets, "secrets.yaml"), + SopsFiles: []string{path.Join(assets, "secrets.yaml")}, Path: target, Mode: "0400", RestartUnits: []string{"affected-service"}, @@ -252,7 +252,7 @@ func TestAge(t *testing.T) { Key: "test_key", Owner: "nobody", Group: "nogroup", - SopsFile: path.Join(assets, "secrets.yaml"), + SopsFiles: []string{path.Join(assets, "secrets.yaml")}, Path: target, Mode: "0400", RestartUnits: []string{"affected-service"}, @@ -285,7 +285,7 @@ func TestAgeWithSSH(t *testing.T) { Key: "test_key", Owner: "nobody", Group: "nogroup", - SopsFile: path.Join(assets, "secrets.yaml"), + SopsFiles: []string{path.Join(assets, "secrets.yaml")}, Path: target, Mode: "0400", RestartUnits: []string{"affected-service"}, @@ -319,7 +319,7 @@ func TestValidateManifest(t *testing.T) { Key: "test_key", Owner: "nobody", Group: "nogroup", - SopsFile: path.Join(assets, "secrets.yaml"), + SopsFiles: []string{path.Join(assets, "secrets.yaml")}, Path: path.Join(testdir.path, "test-target"), Mode: "0400", RestartUnits: []string{}, diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 487074e..ca37ad7 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -10,7 +10,7 @@ bits = 4096; path = ./test-assets/ssh-key; }]; - sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.defaultSopsFiles = [ ./test-assets/secrets.yaml ]; sops.secrets.test_key = { }; }; @@ -29,7 +29,7 @@ imports = [ ../../modules/sops ]; sops = { age.keyFile = ./test-assets/age-keys.txt; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFiles = [ ./test-assets/secrets.yaml ]; secrets.test_key.neededForUsers = true; secrets."nested/test/file".owner = "example-user"; }; @@ -69,7 +69,7 @@ imports = [ ../../modules/sops ]; sops = { age.keyFile = ./test-assets/age-keys.txt; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFiles = [ ./test-assets/secrets.yaml ]; secrets.test_key = { }; keepGenerations = lib.mkDefault 0; }; @@ -110,7 +110,7 @@ imports = [ ../../modules/sops ]; sops = { age.keyFile = ./test-assets/age-keys.txt; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFiles = [ ./test-assets/secrets.yaml ]; secrets.test_key = { }; }; }; @@ -134,7 +134,7 @@ path = ./test-assets/ssh-ed25519-key; }]; sops = { - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFiles = [ ./test-assets/secrets.yaml ]; secrets.test_key = { }; # Generate a key and append it to make sure it appending doesn't break anything age = { @@ -164,7 +164,7 @@ }; sops.gnupg.home = "/run/gpghome"; - sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.defaultSopsFiles = [ ./test-assets/secrets.yaml ]; sops.secrets.test_key.owner = config.users.users.someuser.name; sops.secrets."nested/test/file".owner = config.users.users.someuser.name; sops.secrets.existing-file = { @@ -215,7 +215,7 @@ imports = [ ../../modules/sops ]; sops = { age.keyFile = ./test-assets/age-keys.txt; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFiles = [ ./test-assets/secrets.yaml ]; secrets.test_key = { }; }; @@ -274,7 +274,7 @@ sops = { age.keyFile = ./test-assets/age-keys.txt; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFiles = [ ./test-assets/secrets.yaml ]; secrets.test_key = { restartUnits = [ "restart-unit.service" "reload-unit.service" ]; reloadUnits = [ "reload-trigger.service" ];