Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added encrypted-regex option #509

Merged
merged 5 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
15 changes: 13 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1193,10 +1193,21 @@ The unencrypted suffix can be set to a different value using the
Conversely, you can opt in to only encrypt some values in a YAML or JSON file,
by adding a chosen suffix to those keys and passing it to the ``--encrypted-suffix`` option.

A third method is to use the ``--encrypted-regex`` which will only encrypt values under
keys that match the supplied regular expression. For example, this command:

.. code:: bash

$ sops --encrypt --encrypted-regex '&(data|stringData)' k8s-secrets.yaml

will encrypt the values under the ``data`` and ``stringData`` keys in a YAML file
containing kubernetes secrets. It will not encrypt other values that help you to
navigate the file, like ``metadata`` which contains the secrets' names.

You can also specify these options in the ``.sops.yaml`` config file.

Note: these two options ``--unencrypted-suffix`` and ``--encrypted-suffix`` are mutually exclusive and
cannot both be used in the same file.
Note: these three options ``--unencrypted-suffix``, ``--encrypted-suffix``, and ``--encrypted-regex`` are
mutually exclusive and cannot both be used in the same file.
jpriebe marked this conversation as resolved.
Show resolved Hide resolved

Encryption Protocol
-------------------
Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type editExampleOpts struct {
editOpts
UnencryptedSuffix string
EncryptedSuffix string
EncryptedRegex string
KeyGroups []sops.KeyGroup
GroupThreshold int
}
Expand Down Expand Up @@ -65,6 +66,7 @@ func editExample(opts editExampleOpts) ([]byte, error) {
KeyGroups: opts.KeyGroups,
UnencryptedSuffix: opts.UnencryptedSuffix,
EncryptedSuffix: opts.EncryptedSuffix,
EncryptedRegex: opts.EncryptedRegex,
Version: version.Version,
ShamirThreshold: opts.GroupThreshold,
},
Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type encryptOpts struct {
KeyServices []keyservice.KeyServiceClient
UnencryptedSuffix string
EncryptedSuffix string
EncryptedRegex string
KeyGroups []sops.KeyGroup
GroupThreshold int
}
Expand Down Expand Up @@ -76,6 +77,7 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) {
KeyGroups: opts.KeyGroups,
UnencryptedSuffix: opts.UnencryptedSuffix,
EncryptedSuffix: opts.EncryptedSuffix,
EncryptedRegex: opts.EncryptedRegex,
Version: version.Version,
ShamirThreshold: opts.GroupThreshold,
},
Expand Down
31 changes: 27 additions & 4 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@ func main() {
Name: "encrypted-suffix",
Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
},
cli.StringFlag{
Name: "encrypted-regex",
Usage: "set the encrypted key suffix. When specified, only keys matching the regex will be encrypted.",
},
cli.StringFlag{
Name: "config",
Usage: "path to sops' config file. If set, sops will not search for the config file recursively.",
Expand Down Expand Up @@ -491,6 +495,7 @@ func main() {

unencryptedSuffix := c.String("unencrypted-suffix")
encryptedSuffix := c.String("encrypted-suffix")
encryptedRegex := c.String("encrypted-regex")
conf, err := loadConfig(c, fileName, nil)
if err != nil {
return toExitError(err)
Expand All @@ -503,12 +508,28 @@ func main() {
if encryptedSuffix == "" {
encryptedSuffix = conf.EncryptedSuffix
}
if encryptedRegex == "" {
encryptedRegex = conf.EncryptedRegex
}
}

cryptRuleCount := 0
if unencryptedSuffix != "" {
cryptRuleCount++
}
if unencryptedSuffix != "" && encryptedSuffix != "" {
return common.NewExitError("Error: cannot use both encrypted_suffix and unencrypted_suffix in the same file", codes.ErrorConflictingParameters)
if encryptedSuffix != "" {
cryptRuleCount++
}
// only supply the default UnencryptedSuffix when EncryptedSuffix is not provided
if unencryptedSuffix == "" && encryptedSuffix == "" {
if encryptedRegex != "" {
cryptRuleCount++
}

if cryptRuleCount > 1 {
return common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex in the same file", codes.ErrorConflictingParameters)
}

// only supply the default UnencryptedSuffix when EncryptedSuffix and EncryptedRegex are not provided
if cryptRuleCount == 0 {
unencryptedSuffix = sops.DefaultUnencryptedSuffix
}

Expand All @@ -535,6 +556,7 @@ func main() {
Cipher: aes.NewCipher(),
UnencryptedSuffix: unencryptedSuffix,
EncryptedSuffix: encryptedSuffix,
EncryptedRegex: encryptedRegex,
KeyServices: svcs,
KeyGroups: groups,
GroupThreshold: threshold,
Expand Down Expand Up @@ -656,6 +678,7 @@ func main() {
editOpts: opts,
UnencryptedSuffix: unencryptedSuffix,
EncryptedSuffix: encryptedSuffix,
EncryptedRegex: encryptedRegex,
KeyGroups: groups,
GroupThreshold: threshold,
})
Expand Down
18 changes: 16 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ type creationRule struct {
ShamirThreshold int `yaml:"shamir_threshold"`
UnencryptedSuffix string `yaml:"unencrypted_suffix"`
EncryptedSuffix string `yaml:"encrypted_suffix"`
EncryptedRegex string `yaml:"encrypted_regex"`
}

// Load loads a sops config file into a temporary struct
Expand All @@ -128,6 +129,7 @@ type Config struct {
ShamirThreshold int
UnencryptedSuffix string
EncryptedSuffix string
EncryptedRegex string
Destination publish.Destination
}

Expand Down Expand Up @@ -187,8 +189,19 @@ func loadConfigFile(confPath string) (*configFile, error) {
}

func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) (*Config, error) {
if rule.UnencryptedSuffix != "" && rule.EncryptedSuffix != "" {
return nil, fmt.Errorf("error loading config: cannot use both encrypted_suffix and unencrypted_suffix for the same rule")
cryptRuleCount := 0
if rule.UnencryptedSuffix != "" {
cryptRuleCount++
}
if rule.EncryptedSuffix != "" {
cryptRuleCount++
}
if rule.EncryptedRegex != "" {
cryptRuleCount++
}

if cryptRuleCount > 1 {
return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex for the same rule")
}

groups, err := getKeyGroupsFromCreationRule(rule, kmsEncryptionContext)
Expand All @@ -201,6 +214,7 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
ShamirThreshold: rule.ShamirThreshold,
UnencryptedSuffix: rule.UnencryptedSuffix,
EncryptedSuffix: rule.EncryptedSuffix,
EncryptedRegex: rule.EncryptedRegex,
}, nil
}

Expand Down
14 changes: 14 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ creation_rules:
version: fooversion
`)

var sampleConfigWithRegexParameters = []byte(`
creation_rules:
- path_regex: barbar*
kms: "1"
pgp: "2"
encrypted_regex: "^enc:"
`)

var sampleConfigWithInvalidParameters = []byte(`
creation_rules:
- path_regex: foobar*
Expand Down Expand Up @@ -273,6 +281,12 @@ func TestLoadConfigFileWithEncryptedSuffix(t *testing.T) {
assert.Equal(t, "_enc", conf.EncryptedSuffix)
}

func TestLoadConfigFileWithEncryptedRegex(t *testing.T) {
conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "barbar", nil)
assert.Equal(t, nil, err)
assert.Equal(t, "^enc:", conf.EncryptedRegex)
}

func TestLoadConfigFileWithInvalidParameters(t *testing.T) {
_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "foobar", nil)
assert.NotNil(t, err)
Expand Down
34 changes: 30 additions & 4 deletions sops.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"crypto/sha512"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -285,9 +286,10 @@ func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(

// Encrypt walks over the tree and encrypts all values with the provided cipher,
// except those whose key ends with the UnencryptedSuffix specified on the
// Metadata struct, or those not ending with EncryptedSuffix, if EncryptedSuffix
// is provided (by default it is not). If encryption is successful, it returns
// the MAC for the encrypted tree.
// Metadata struct, those not ending with EncryptedSuffix, if EncryptedSuffix
// is provided (by default it is not), or those not matching EncryptedRegex,
// if EncryptedRegex is provided (by default it is not). If encryption is
// successful, it returns the MAC for the encrypted tree.
func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
audit.SubmitEvent(audit.EncryptEvent{
File: tree.FilePath,
Expand Down Expand Up @@ -321,6 +323,16 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
}
}
}
if tree.Metadata.EncryptedRegex != "" {
encrypted = false
for _, p := range path {
matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p))
if matched {
encrypted = true
break
}
}
}
if encrypted {
var err error
pathString := strings.Join(path, ":") + ":"
Expand All @@ -343,7 +355,10 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
return fmt.Sprintf("%X", hash.Sum(nil)), nil
}

// Decrypt walks over the tree and decrypts all values with the provided cipher, except those whose key ends with the UnencryptedSuffix specified on the Metadata struct or those not ending with EncryptedSuffix, if EncryptedSuffix is provided (by default it is not).
// Decrypt walks over the tree and decrypts all values with the provided cipher,
// except those whose key ends with the UnencryptedSuffix specified on the Metadata struct,
// those not ending with EncryptedSuffix, if EncryptedSuffix is provided (by default it is not),
// or those not matching EncryptedRegex, if EncryptedRegex is provided (by default it is not).
// If decryption is successful, it returns the MAC for the decrypted tree.
func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
log.Debug("Decrypting tree")
Expand Down Expand Up @@ -371,6 +386,16 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
}
}
}
if tree.Metadata.EncryptedRegex != "" {
encrypted = false
for _, p := range path {
matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p))
if matched {
encrypted = true
break
}
}
}
var v interface{}
if encrypted {
var err error
Expand Down Expand Up @@ -441,6 +466,7 @@ type Metadata struct {
LastModified time.Time
UnencryptedSuffix string
EncryptedSuffix string
EncryptedRegex string
MessageAuthenticationCode string
Version string
KeyGroups []KeyGroup
Expand Down
52 changes: 52 additions & 0 deletions sops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,58 @@ func TestEncryptedSuffix(t *testing.T) {
}
}

func TestEncryptedRegex(t *testing.T) {
branches := TreeBranches{
TreeBranch{
TreeItem{
Key: "enc:foo",
Value: "bar",
},
TreeItem{
Key: "bar",
Value: TreeBranch{
TreeItem{
Key: "foo",
Value: "bar",
},
},
},
},
}
tree := Tree{Branches: branches, Metadata: Metadata{EncryptedRegex: "^enc:"}}
expected := TreeBranch{
TreeItem{
Key: "enc:foo",
Value: "rab",
},
TreeItem{
Key: "bar",
Value: TreeBranch{
TreeItem{
Key: "foo",
Value: "bar",
},
},
},
}
cipher := reverseCipher{}
_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the tree failed: %s", err)
}
if !reflect.DeepEqual(tree.Branches[0], expected) {
t.Errorf("Trees don't match: \ngot \t\t%+v,\n expected \t\t%+v", tree.Branches[0], expected)
}
_, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Decrypting the tree failed: %s", err)
}
expected[0].Value = "bar"
if !reflect.DeepEqual(tree.Branches[0], expected) {
t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected)
}
}

type MockCipher struct{}

func (m MockCipher) Encrypt(value interface{}, key []byte, path string) (string, error) {
Expand Down
22 changes: 19 additions & 3 deletions stores/stores.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type Metadata struct {
PGPKeys []pgpkey `yaml:"pgp" json:"pgp"`
UnencryptedSuffix string `yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty"`
EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"`
EncryptedRegex string `yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"`
Version string `yaml:"version" json:"version"`
}

Expand Down Expand Up @@ -90,6 +91,7 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata {
m.LastModified = sopsMetadata.LastModified.Format(time.RFC3339)
m.UnencryptedSuffix = sopsMetadata.UnencryptedSuffix
m.EncryptedSuffix = sopsMetadata.EncryptedSuffix
m.EncryptedRegex = sopsMetadata.EncryptedRegex
m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode
m.Version = sopsMetadata.Version
m.ShamirThreshold = sopsMetadata.ShamirThreshold
Expand Down Expand Up @@ -183,10 +185,23 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
if err != nil {
return sops.Metadata{}, err
}
if m.UnencryptedSuffix != "" && m.EncryptedSuffix != "" {
return sops.Metadata{}, fmt.Errorf("Cannot use both encrypted_suffix and unencrypted_suffix in the same file")

cryptRuleCount := 0
if m.UnencryptedSuffix != "" {
cryptRuleCount++
}
if m.EncryptedSuffix != "" {
cryptRuleCount++
}
if m.EncryptedRegex != "" {
cryptRuleCount++
}
if m.UnencryptedSuffix == "" && m.EncryptedSuffix == "" {

if cryptRuleCount > 1 {
return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex in the same file")
}

if cryptRuleCount == 0 {
m.UnencryptedSuffix = sops.DefaultUnencryptedSuffix
}
return sops.Metadata{
Expand All @@ -196,6 +211,7 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
MessageAuthenticationCode: m.MessageAuthenticationCode,
UnencryptedSuffix: m.UnencryptedSuffix,
EncryptedSuffix: m.EncryptedSuffix,
EncryptedRegex: m.EncryptedRegex,
LastModified: lastModified,
}, nil
}
Expand Down