Skip to content

Commit

Permalink
sopsFiles
Browse files Browse the repository at this point in the history
Allow multiple sopsFiles for easier secret sharing across multiple
configurations
  • Loading branch information
vdbe committed Oct 8, 2023
1 parent d7380c3 commit f983dd8
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 10 deletions.
42 changes: 41 additions & 1 deletion modules/sops/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -77,13 +80,27 @@ 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;
description = ''
Hash of the sops file, useful in <xref linkend="opt-systemd.services._name_.restartTriggers" />.
'';
};
sopsFilesHash = mkOption {
type = types.listOf types.str;
readOnly = true;
description = ''
Hash of the sops file, useful in <xref linkend="opt-systemd.services._name_.restartTriggers" />.
'';
};
restartUnits = mkOption {
type = types.listOf types.str;
default = [ ];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
} {
Expand All @@ -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");
Expand Down
47 changes: 38 additions & 9 deletions pkgs/sops-install-secrets/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -257,27 +258,31 @@ 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 {
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.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 {
Expand All @@ -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
}

Expand Down

0 comments on commit f983dd8

Please sign in to comment.