diff --git a/README.rst b/README.rst index bdd521295..de4a9f270 100644 --- a/README.rst +++ b/README.rst @@ -434,14 +434,25 @@ creation_rules: - filename_regex: .*keygroups.* key_groups: # First key group - - pgp: fingerprint1,fingerprint2 - kms: arn1,arn2 + - pgp: + - fingerprint1 + - fingerprint2 + kms: + - arn: arn1 + role: role1 + context: + foo: bar + - arn: arn2 # Second key group - - pgp: fingerprint3,fingerprint4 - kms: arn3,arn4 + - pgp: + - fingerprint3 + - fingerprint4 + kms: + - arn: arn3 + - arn: arn4 # Third key group - - pgp: fingerprint5,fingerprint6 - kms: arn5,arn6 + - pgp: + - fingerprint5 ``` Given this configuration, we can create a new encrypted file like we normally @@ -456,6 +467,38 @@ For example: sops --shamir-secret-sharing-threshold 2 example.json ``` +Alternatively, you can configure the Shamir threshold for each creation rule in the `.sops.yaml` config +with `shamir_threshold`: + +```yaml +creation_rules: + - filename_regex: .*keygroups.* + shamir_threshold: 2 + key_groups: + # First key group + - pgp: + - fingerprint1 + - fingerprint2 + kms: + - arn: arn1 + role: role1 + context: + foo: bar + - arn: arn2 + # Second key group + - pgp: + - fingerprint3 + - fingerprint4 + kms: + - arn: arn3 + - arn: arn4 + # Third key group + - pgp: + - fingerprint5 +``` + +And then run `sops example.json`. + This will require 2 master keys from different key groups in order to decrypt the file. You can then decrypt the file the same way as with any other SOPS file: diff --git a/cmd/sops/main.go b/cmd/sops/main.go index c79c0992d..47a8c740f 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -10,7 +10,6 @@ import ( "go.mozilla.org/sops" "fmt" - "io/ioutil" "os" "strings" "time" @@ -24,13 +23,13 @@ import ( "go.mozilla.org/sops/cmd/sops/codes" "go.mozilla.org/sops/cmd/sops/subcommand/groups" keyservicecmd "go.mozilla.org/sops/cmd/sops/subcommand/keyservice" + "go.mozilla.org/sops/config" "go.mozilla.org/sops/keys" "go.mozilla.org/sops/keyservice" "go.mozilla.org/sops/kms" "go.mozilla.org/sops/pgp" "go.mozilla.org/sops/stores/json" yamlstores "go.mozilla.org/sops/stores/yaml" - "go.mozilla.org/sops/config" "gopkg.in/urfave/cli.v1" ) @@ -306,6 +305,10 @@ func main() { if err != nil { return err } + shamirThreshold, err := shamirThreshold(c, fileName) + if err != nil { + return err + } output, err = encrypt(encryptOpts{ OutputStore: outputStore, InputStore: inputStore, @@ -314,7 +317,7 @@ func main() { UnencryptedSuffix: c.String("unencrypted-suffix"), KeyServices: svcs, KeyGroups: keyGroups, - GroupThreshold: c.Int("shamir-secret-sharing-threshold"), + GroupThreshold: shamirThreshold, }) if err != nil { return err @@ -412,11 +415,15 @@ func main() { if err != nil { return err } + shamirThreshold, err := shamirThreshold(c, fileName) + if err != nil { + return err + } output, err = editExample(editExampleOpts{ editOpts: opts, UnencryptedSuffix: c.String("unencrypted-suffix"), KeyGroups: keyGroups, - GroupThreshold: c.Int("shamir-secret-sharing-threshold"), + GroupThreshold: shamirThreshold, }) } } @@ -548,25 +555,45 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { pgpKeys = append(pgpKeys, k) } } - var err error if c.String("kms") == "" && c.String("pgp") == "" { - var confBytes []byte + var err error + var configPath string if c.String("config") != "" { - confBytes, err = ioutil.ReadFile(c.String("config")) + } else { + configPath, err = config.FindConfigFile(".") if err != nil { - return nil, cli.NewExitError(fmt.Sprintf("Error loading config file: %s", err), codes.ErrorReadingConfig) + return nil, fmt.Errorf("config file not found and no keys provided through command line options") } } - groups, err := config.KeyGroupsForFile(file, confBytes, kmsEncryptionContext) + conf, err := config.LoadForFile(configPath, file, kmsEncryptionContext) if err != nil { return nil, err } - log.Printf("Proceeding with key groups: %#v", groups) - return groups, err + return conf.KeyGroups, err } return []sops.KeyGroup{append(kmsKeys, pgpKeys...)}, nil } +func shamirThreshold(c *cli.Context, file string) (int, error) { + if c.Int("shamir-secret-sharing-threshold") != 0 { + return c.Int("shamir-secret-sharing-threshold"), nil + } + var err error + var configPath string + if c.String("config") != "" { + } else { + configPath, err = config.FindConfigFile(".") + if err != nil { + return 0, fmt.Errorf("config file not found and no keys provided through command line options") + } + } + conf, err := config.LoadForFile(configPath, file, nil) + if err != nil { + return 0, err + } + return conf.ShamirThreshold, err +} + func jsonValueToTreeInsertableValue(jsonValue string) (interface{}, error) { var valueToInsert interface{} err := encodingjson.Unmarshal([]byte(jsonValue), &valueToInsert) diff --git a/config/config.go b/config/config.go index 9dd518ee9..472a73cfd 100644 --- a/config/config.go +++ b/config/config.go @@ -57,14 +57,16 @@ type keyGroup struct { type kmsKey struct { Arn string `yaml:"arn"` + Role string `yaml:"role,omitempty"` Context map[string]*string `yaml:"context"` } type creationRule struct { - FilenameRegex string `yaml:"filename_regex"` - KMS string - PGP string - KeyGroups []keyGroup `yaml:"key_groups"` + FilenameRegex string `yaml:"filename_regex"` + KMS string + PGP string + KeyGroups []keyGroup `yaml:"key_groups"` + ShamirThreshold int `yaml:"shamir_threshold"` } // Load loads a sops config file into a temporary struct @@ -76,52 +78,61 @@ func (f *configFile) load(bytes []byte) error { return nil } -// KeyGroupsForFile returns the key groups that should be use for a given file, based on the file's path and the -// configuration -func KeyGroupsForFile(filepath string, confBytes []byte, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) { - var err error - if confBytes == nil { - var confPath string - confPath, err = FindConfigFile(".") - if err != nil { - return nil, err - } - confBytes, err = ioutil.ReadFile(confPath) - } - if err != nil { - return nil, fmt.Errorf("Could not read config file: %s", err) - } +// Config is the configuration for a given SOPS file +type Config struct { + KeyGroups []sops.KeyGroup + ShamirThreshold int +} + +func loadForFileFromBytes(confBytes []byte, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) { conf := configFile{} - err = conf.load(confBytes) + err := conf.load(confBytes) if err != nil { - return nil, fmt.Errorf("Error loading config: %s", err) + return nil, fmt.Errorf("error loading config: %s", err) + } + var rule *creationRule + + for _, r := range conf.CreationRules { + if match, _ := regexp.MatchString(r.FilenameRegex, filePath); match { + rule = &r + break + } } var groups []sops.KeyGroup - for _, rule := range conf.CreationRules { - if match, _ := regexp.MatchString(rule.FilenameRegex, filepath); match { - if len(rule.KeyGroups) > 0 { - for _, group := range rule.KeyGroups { - var keyGroup sops.KeyGroup - for _, k := range group.PGP { - keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k)) - } - for _, k := range group.KMS { - keyGroup = append(keyGroup, kms.NewMasterKeyFromArn(k.Arn, k.Context)) - } - groups = append(groups, keyGroup) - } - } else { - var keyGroup sops.KeyGroup - for _, k := range pgp.MasterKeysFromFingerprintString(rule.PGP) { - keyGroup = append(keyGroup, k) - } - for _, k := range kms.MasterKeysFromArnString(rule.KMS, kmsEncryptionContext) { - keyGroup = append(keyGroup, k) - } - groups = append(groups, keyGroup) + if len(rule.KeyGroups) > 0 { + for _, group := range rule.KeyGroups { + var keyGroup sops.KeyGroup + for _, k := range group.PGP { + keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k)) } - return groups, nil + for _, k := range group.KMS { + keyGroup = append(keyGroup, kms.NewMasterKey(k.Arn, k.Role, k.Context)) + } + groups = append(groups, keyGroup) } + } else { + var keyGroup sops.KeyGroup + for _, k := range pgp.MasterKeysFromFingerprintString(rule.PGP) { + keyGroup = append(keyGroup, k) + } + for _, k := range kms.MasterKeysFromArnString(rule.KMS, kmsEncryptionContext) { + keyGroup = append(keyGroup, k) + } + groups = append(groups, keyGroup) } - return nil, nil + return &Config{ + KeyGroups: groups, + ShamirThreshold: rule.ShamirThreshold, + }, nil +} + +// LoadForFile load the configuration for a given SOPS file from the config file at confPath. A kmsEncryptionContext +// should be provided for configurations that do not contain key groups, as there's no way to specify context inside +// a SOPS config file outside of key groups. +func LoadForFile(confPath string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) { + confBytes, err := ioutil.ReadFile(confPath) + if err != nil { + return nil, fmt.Errorf("could not read config file: %s", err) + } + return loadForFileFromBytes(confBytes, filePath, kmsEncryptionContext) } diff --git a/config/config_test.go b/config/config_test.go index 7b0a88a06..567c7362a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -122,21 +122,21 @@ func TestLoadConfigFileWithGroups(t *testing.T) { } func TestKeyGroupsForFile(t *testing.T) { - groups, err := KeyGroupsForFile("foobar2000", sampleConfig, nil) + conf, err := loadForFileFromBytes(sampleConfig, "foobar2000", nil) assert.Equal(t, nil, err) - assert.Equal(t, "2", groups[0][0].ToString()) - assert.Equal(t, "1", groups[0][1].ToString()) - groups, err = KeyGroupsForFile("whatever", sampleConfig, nil) + assert.Equal(t, "2", conf.KeyGroups[0][0].ToString()) + assert.Equal(t, "1", conf.KeyGroups[0][1].ToString()) + conf, err = loadForFileFromBytes(sampleConfig, "whatever", nil) assert.Equal(t, nil, err) - assert.Equal(t, "bar", groups[0][0].ToString()) - assert.Equal(t, "foo", groups[0][1].ToString()) + assert.Equal(t, "bar", conf.KeyGroups[0][0].ToString()) + assert.Equal(t, "foo", conf.KeyGroups[0][1].ToString()) } func TestKeyGroupsForFileWithGroups(t *testing.T) { - groups, err := KeyGroupsForFile("whatever", sampleConfigWithGroups, nil) + conf, err := loadForFileFromBytes(sampleConfigWithGroups, "whatever", nil) assert.Equal(t, nil, err) - assert.Equal(t, "bar", groups[0][0].ToString()) - assert.Equal(t, "foo", groups[0][1].ToString()) - assert.Equal(t, "qux", groups[1][0].ToString()) - assert.Equal(t, "baz", groups[1][1].ToString()) + assert.Equal(t, "bar", conf.KeyGroups[0][0].ToString()) + assert.Equal(t, "foo", conf.KeyGroups[0][1].ToString()) + assert.Equal(t, "qux", conf.KeyGroups[1][0].ToString()) + assert.Equal(t, "baz", conf.KeyGroups[1][1].ToString()) } diff --git a/kms/keysource.go b/kms/keysource.go index 70d2b8092..1aa462b07 100644 --- a/kms/keysource.go +++ b/kms/keysource.go @@ -111,6 +111,16 @@ func (key *MasterKey) ToString() string { return key.Arn } +// NewMasterKey creates a new MasterKey from an ARN, role and context, setting the creation date to the current date +func NewMasterKey(arn string, role string, context map[string]*string) *MasterKey { + return &MasterKey{ + Arn: arn, + Role: role, + EncryptionContext: context, + CreationDate: time.Now().UTC(), + } +} + // NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that ARN func NewMasterKeyFromArn(arn string, context map[string]*string) *MasterKey { k := &MasterKey{}