Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main // import "github.com/getsops/sops/v3/cmd/sops"
import (
"context"
encodingjson "encoding/json"
"errors"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -2516,6 +2517,16 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so
return []sops.KeyGroup{group}, nil
}

func hasInlineMasterKeyFlags(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") != ""
Comment on lines +2520 to +2527
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasInlineMasterKeyFlags duplicates the same inline-key presence check already implemented in keyGroups (the long c.String(...) == "" condition). This creates a maintenance risk where new master-key flags would need to be updated in multiple places. Consider reusing this helper in keyGroups (or deriving both checks from a shared list of flag names) to keep the logic in sync.

Copilot uses AI. Check for mistakes.
}

// loadConfig will look for an existing config file, either provided through the command line, or using findConfigFile
// Since a config file is not required, this function does not error when one is not found, and instead returns a nil config pointer
func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) {
Expand All @@ -2531,6 +2542,9 @@ func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*st
}
conf, err := config.LoadCreationRuleForFile(configPath, file, kmsEncryptionContext)
if err != nil {
if hasInlineMasterKeyFlags(c) && errors.Is(err, config.ErrNoMatchingCreationRules) {
return nil, nil
}
return nil, err
}
return conf, nil
Expand Down
87 changes: 87 additions & 0 deletions cmd/sops/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"flag"
"os"
"path/filepath"
"testing"

"github.com/getsops/sops/v3/config"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
)

const nonMatchingCreationRuleConfig = `creation_rules:
- path_regex: something-else/.*\.(json|yaml|yml|env|txt)$
age: age15sq7kls08hzq8djpn26dda0fna3ccnw038568gcul9amjjjdaedq4xg2rr
`

const matchingCreationRuleConfig = `creation_rules:
- path_regex: ""
age: age15sq7kls08hzq8djpn26dda0fna3ccnw038568gcul9amjjjdaedq4xg2rr
`

func newTestCLIContext(t *testing.T, configPath string, inlineFlags map[string]string) *cli.Context {
t.Helper()

app := cli.NewApp()

globalSet := flag.NewFlagSet("global", flag.ContinueOnError)
globalSet.String("config", "", "")
require.NoError(t, globalSet.Set("config", configPath))
globalCtx := cli.NewContext(app, globalSet, nil)

localSet := flag.NewFlagSet("local", flag.ContinueOnError)
for _, name := range []string{"kms", "pgp", "gcp-kms", "hckms", "azure-kv", "hc-vault-transit", "age"} {
localSet.String(name, "", "")
}
for name, value := range inlineFlags {
require.NoError(t, localSet.Set(name, value))
}

return cli.NewContext(app, localSet, globalCtx)
}

func writeConfigFile(t *testing.T, dir string, contents string) string {
t.Helper()

configPath := filepath.Join(dir, ".sops.yaml")
require.NoError(t, os.WriteFile(configPath, []byte(contents), 0o600))
return configPath
}

func TestLoadConfigIgnoresNonMatchingCreationRulesWhenInlineKeysAreProvided(t *testing.T) {
dir := t.TempDir()
configPath := writeConfigFile(t, dir, nonMatchingCreationRuleConfig)
ctx := newTestCLIContext(t, configPath, map[string]string{
"age": "age1xxfdafu5j4e5z7y5l6my6x07vjuh6unxersnwne4etpvykheq9gsj003fv",
})

conf, err := loadConfig(ctx, filepath.Join(dir, "secret.json"), nil)
require.NoError(t, err)
require.Nil(t, conf)
}

func TestLoadConfigReturnsNonMatchingCreationRuleErrorWithoutInlineKeys(t *testing.T) {
dir := t.TempDir()
configPath := writeConfigFile(t, dir, nonMatchingCreationRuleConfig)
ctx := newTestCLIContext(t, configPath, nil)

conf, err := loadConfig(ctx, filepath.Join(dir, "secret.json"), nil)
require.Nil(t, conf)
require.ErrorIs(t, err, config.ErrNoMatchingCreationRules)
}

func TestLoadConfigStillLoadsMatchingCreationRulesWithInlineKeys(t *testing.T) {
dir := t.TempDir()
configPath := writeConfigFile(t, dir, matchingCreationRuleConfig)
ctx := newTestCLIContext(t, configPath, map[string]string{
"age": "age1xxfdafu5j4e5z7y5l6my6x07vjuh6unxersnwne4etpvykheq9gsj003fv",
})

conf, err := loadConfig(ctx, filepath.Join(dir, "secret.json"), nil)
require.NoError(t, err)
require.NotNil(t, conf)
require.Len(t, conf.KeyGroups, 1)
require.Len(t, conf.KeyGroups[0], 1)
}
5 changes: 4 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -43,6 +44,8 @@ const (
alternateConfigName = ".sops.yml"
)

var ErrNoMatchingCreationRules = errors.New("error loading config: no matching creation rules found")

// ConfigFileResult contains the path to a config file and any warnings
type ConfigFileResult struct {
Path string
Expand Down Expand Up @@ -599,7 +602,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, ErrNoMatchingCreationRules
}

config, err := configFromRule(rule, kmsEncryptionContext)
Expand Down
Loading