mirror of
https://github.com/getsops/sops.git
synced 2026-02-05 12:45:21 +01:00
Allow configuring shamir threshold from config file
This commit is contained in:
55
README.rst
55
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
101
config/config.go
101
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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user