From 5f6c7348605cd29404b6a0c9f41e9177a4e26c8d Mon Sep 17 00:00:00 2001 From: Pawansingh3889 Date: Sun, 26 Jul 2026 05:33:34 +0100 Subject: [PATCH] Don't require a matching creation rule when a key is given explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1790: `sops --age -e file` (or --kms/--pgp/etc.) fails with "error loading config: no matching creation rules found" whenever a .sops.yaml exists somewhere up the directory tree but none of its creation rules match the target file — even though the explicit key should be sufficient on its own. keyGroups() already handles this correctly: it sources keys straight from CLI flags without consulting the config at all when any are given. But three other call sites load the config unconditionally before keyGroups() ever runs, and treat any loading failure as fatal: - main()'s app.Action (~line 1949), for EncryptedRegex/UnencryptedSuffix/etc. - getEncryptConfig() (~line 2134), for the same settings - shamirThreshold() (~line 2576), for ShamirThreshold Every value these three read from config has a safe zero-value default, so none of them actually need the config to exist -- they only need to not crash when it doesn't apply. Export config.ErrNoMatchingCreationRules as a sentinel for that specific failure, and at each of the three sites: if the error is that sentinel and the user supplied at least one key flag directly (the same check keyGroups() already makes, now factored into hasExplicitKeyFlags), treat it as "no applicable config" instead of a hard error. Verified with a full encrypt/decrypt round-trip against the exact repro (a .sops.yaml with rules, none matching, plus an explicit --age): fails on stock sops 3.13.3, succeeds identically patched. go build ./... and go test ./config/... ./cmd/sops/... both pass unchanged. Signed-off-by: Pawansingh3889 --- cmd/sops/main.go | 41 ++++++++++++++++++++++++++++++++++++++--- config/config.go | 10 +++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index e590e1c82b..35abc0618f 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -3,6 +3,7 @@ package main // import "github.com/getsops/sops/v3/cmd/sops" import ( "context" encodingjson "encoding/json" + "errors" "fmt" "io" "net" @@ -1942,12 +1943,23 @@ func main() { // Load configuration here for backwards compatibility (error out in case of bad config files), // but only when not just decrypting (https://github.com/getsops/sops/issues/868) needsCreationRule := isEncryptMode || isRotateMode || isSetMode || isEditMode + // Captured before the `config` local below shadows the `config` package name. + errNoMatchingCreationRules := config.ErrNoMatchingCreationRules var config *config.Config if needsCreationRule { kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) config, err = loadConfig(c, fileNameOverride, kmsEncryptionContext) if err != nil { - return toExitError(err) + // A config file exists but none of its creation rules match this file. + // That's only fatal if we have no other way to get an encryption key: + // keyGroups() sources keys straight from CLI flags without consulting + // the config at all, so a non-matching config shouldn't block encrypt/ + // rotate/set/edit when the caller supplied a key explicitly. + if errors.Is(err, errNoMatchingCreationRules) && hasExplicitKeyFlags(c) { + config, err = nil, nil + } else { + return toExitError(err) + } } } @@ -2121,7 +2133,16 @@ func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store, if optionalConfig == nil { optionalConfig, err = loadConfig(c, fileName, nil) if err != nil { - return encryptConfig{}, toExitError(err) + // A config file exists but none of its creation rules match this file. + // That's only fatal if we have no other way to get an encryption key: + // keyGroups() below already sources keys straight from these same CLI + // flags without consulting the config at all, so a non-matching config + // shouldn't block encryption when the caller supplied a key explicitly. + if errors.Is(err, config.ErrNoMatchingCreationRules) && hasExplicitKeyFlags(c) { + optionalConfig, err = nil, nil + } else { + return encryptConfig{}, toExitError(err) + } } } if optionalConfig != nil { @@ -2425,6 +2446,14 @@ func parseTreePath(arg string) ([]interface{}, error) { return path, nil } +// hasExplicitKeyFlags reports whether the user supplied at least one master-key source +// directly on the command line, rather than relying on a config file to provide one. +func hasExplicitKeyFlags(c *cli.Context) bool { + return c.String("kms") != "" || c.String("pgp") != "" || c.String("gcp-kms") != "" || + c.String("hckms") != "" || c.String("azure-kv") != "" || c.String("hc-vault-transit") != "" || + c.String("age") != "" +} + func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]sops.KeyGroup, error) { var kmsKeys []keys.MasterKey var pgpKeys []keys.MasterKey @@ -2488,7 +2517,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so ageMasterKeys = append(ageMasterKeys, k) } } - if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("hckms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" { + if !hasExplicitKeyFlags(c) { conf := optionalConfig var err error if conf == nil { @@ -2544,6 +2573,12 @@ func shamirThreshold(c *cli.Context, file string, optionalConfig *config.Config) conf := optionalConfig if conf == nil { conf, err = loadConfig(c, file, nil) + if errors.Is(err, config.ErrNoMatchingCreationRules) && hasExplicitKeyFlags(c) { + // A non-matching config doesn't block encryption when a key was supplied + // explicitly (see the identical exemption in getEncryptConfig); the caller + // is not relying on the config for anything else this function returns. + conf, err = nil, nil + } } if conf == nil { // This takes care of the following two case: diff --git a/config/config.go b/config/config.go index 511df1bc15..7e5e17eff3 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ Package config provides a way to find and load SOPS configuration files package config //import "github.com/getsops/sops/v3/config" import ( + "errors" "fmt" "os" "path" @@ -567,6 +568,13 @@ func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptio return config, nil } +// ErrNoMatchingCreationRules is returned by parseCreationRuleForFile when a config file +// has creation rules but none of them match the given file path. Exported as a sentinel +// so callers that can source keys another way (e.g. explicit --age/--kms/... flags) can +// tell this apart from other config-loading failures and treat it as "no applicable +// config" instead of a hard error. +var ErrNoMatchingCreationRules = errors.New("no matching creation rules found") + func parseCreationRuleForFile(conf *configFile, confPath, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) { // If config file doesn't contain CreationRules (it's empty or only contains DestionationRules), assume it does not exist if conf.CreationRules == nil { @@ -599,7 +607,7 @@ func parseCreationRuleForFile(conf *configFile, confPath, filePath string, kmsEn } if rule == nil { - return nil, fmt.Errorf("error loading config: no matching creation rules found") + return nil, fmt.Errorf("error loading config: %w", ErrNoMatchingCreationRules) } config, err := configFromRule(rule, kmsEncryptionContext)