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" ];