Skip to content

Commit

Permalink
Decrypt base64 encoded SOPS encrypted secrets
Browse files Browse the repository at this point in the history
Signed-off-by: Bob Rohan <bob.rohan@hodge.co.uk>
  • Loading branch information
bob.rohan committed Apr 28, 2021
1 parent 1ba5d2b commit a77ea03
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 33 deletions.
27 changes: 27 additions & 0 deletions controllers/kustomization_controller_sops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -95,10 +96,19 @@ var _ = Describe("KustomizationReconciler", func() {
Expect(err).ToNot(HaveOccurred())
ageKey, err := ioutil.ReadFile("testdata/sops/age.txt")
Expect(err).ToNot(HaveOccurred())
dayKey, err := ioutil.ReadFile("testdata/sops/day.txt.encrypted")
Expect(err).ToNot(HaveOccurred())

sopsSecretKey := types.NamespacedName{
Name: "sops-" + randStringRunes(5),
Namespace: namespace.Name,
}

sopsEncodedSecretKey := types.NamespacedName{
Name: "sops-encoded-" + randStringRunes(5),
Namespace: namespace.Name,
}

sopsSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sopsSecretKey.Name,
Expand All @@ -109,7 +119,20 @@ var _ = Describe("KustomizationReconciler", func() {
"age.agekey": string(ageKey),
},
}

sopsEncodedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sopsEncodedSecretKey.Name,
Namespace: sopsEncodedSecretKey.Namespace,
},
StringData: map[string]string{
// base64.StdEncoding.EncodeToString replicates kustomize.secretGenerator
"day.dayKey": base64.StdEncoding.EncodeToString(dayKey),
},
}

Expect(k8sClient.Create(context.Background(), sopsSecret)).To(Succeed())
Expect(k8sClient.Create(context.Background(), sopsEncodedSecret)).To(Succeed())

kustomizationKey := types.NamespacedName{
Name: "sops-" + randStringRunes(5),
Expand Down Expand Up @@ -158,6 +181,10 @@ var _ = Describe("KustomizationReconciler", func() {
var ageSecret corev1.Secret
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-age", Namespace: namespace.Name}, &ageSecret)).To(Succeed())
Expect(ageSecret.Data["secret"]).To(Equal([]byte(`my-sops-age-secret`)))

var daySecret corev1.Secret
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-day", Namespace: namespace.Name}, &daySecret)).To(Succeed())
Expect(string(daySecret.Data["secret"])).To(Equal("day=Tuesday\n"))
})
})
})
123 changes: 90 additions & 33 deletions controllers/kustomization_decryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package controllers
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -75,47 +76,103 @@ func (kd *KustomizeDecryptor) Decrypt(res *resource.Resource) (*resource.Resourc
return nil, err
}

if kd.kustomization.Spec.Decryption != nil && kd.kustomization.Spec.Decryption.Provider == DecryptionProviderSOPS &&
bytes.Contains(out, []byte("sops:")) && bytes.Contains(out, []byte("mac: ENC[")) {
store := common.StoreForFormat(formats.Yaml)
if kd.kustomization.Spec.Decryption != nil && kd.kustomization.Spec.Decryption.Provider == DecryptionProviderSOPS {

tree, err := store.LoadEncryptedFile(out)
if err != nil {
return nil, fmt.Errorf("LoadEncryptedFile: %w", err)
}
if bytes.Contains(out, []byte("sops:")) && bytes.Contains(out, []byte("mac: ENC[")) {
store := common.StoreForFormat(formats.Yaml)

key, err := tree.Metadata.GetDataKeyWithKeyServices(
[]keyservice.KeyServiceClient{
intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.ageIdentities)),
},
)
if err != nil {
if userErr, ok := err.(sops.UserError); ok {
err = fmt.Errorf(userErr.UserError())
tree, err := store.LoadEncryptedFile(out)
if err != nil {
return nil, fmt.Errorf("LoadEncryptedFile: %w", err)
}
return nil, fmt.Errorf("GetDataKey: %w", err)
}

cipher := aes.NewCipher()
if _, err := tree.Decrypt(key, cipher); err != nil {
return nil, fmt.Errorf("AES decrypt: %w", err)
}
key, err := tree.Metadata.GetDataKeyWithKeyServices(
[]keyservice.KeyServiceClient{
intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.ageIdentities)),
},
)
if err != nil {
if userErr, ok := err.(sops.UserError); ok {
err = fmt.Errorf(userErr.UserError())
}
return nil, fmt.Errorf("GetDataKey: %w", err)
}

data, err := store.EmitPlainFile(tree.Branches)
if err != nil {
return nil, fmt.Errorf("EmitPlainFile: %w", err)
}
cipher := aes.NewCipher()
if _, err := tree.Decrypt(key, cipher); err != nil {
return nil, fmt.Errorf("AES decrypt: %w", err)
}

jsonData, err := yaml.YAMLToJSON(data)
if err != nil {
return nil, fmt.Errorf("YAMLToJSON: %w", err)
}
data, err := store.EmitPlainFile(tree.Branches)
if err != nil {
return nil, fmt.Errorf("EmitPlainFile: %w", err)
}

jsonData, err := yaml.YAMLToJSON(data)
if err != nil {
return nil, fmt.Errorf("YAMLToJSON: %w", err)
}

err = res.UnmarshalJSON(jsonData)
if err != nil {
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
}
return res, nil

} else if res.GetKind() == "Secret" {

dataMap := res.GetDataMap()

for key, value := range dataMap {

data, err := base64.StdEncoding.DecodeString(value)
if err != nil {
fmt.Println("Base64 Decode: %w", err)
}

if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {

store := common.StoreForFormat(formats.Yaml)

tree, err := store.LoadEncryptedFile(data)
if err != nil {
return nil, fmt.Errorf("LoadEncryptedFile: %w", err)
}

metadataKey, err := tree.Metadata.GetDataKeyWithKeyServices(
[]keyservice.KeyServiceClient{
intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.ageIdentities)),
},
)

if err != nil {
if userErr, ok := err.(sops.UserError); ok {
err = fmt.Errorf(userErr.UserError())
}
return nil, fmt.Errorf("GetDataKey: %w", err)
}

cipher := aes.NewCipher()
if _, err := tree.Decrypt(metadataKey, cipher); err != nil {
return nil, fmt.Errorf("AES decrypt: %w", err)
}

binaryStore := common.StoreForFormat(formats.Binary)

out, err := binaryStore.EmitPlainFile(tree.Branches)
if err != nil {
return nil, fmt.Errorf("EmitPlainFile: %w", err)
}

dataMap[key] = base64.StdEncoding.EncodeToString(out)
}
}

res.SetDataMap(dataMap)

return res, nil

err = res.UnmarshalJSON(jsonData)
if err != nil {
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
}
return res, nil
}
return nil, nil
}
Expand Down
1 change: 1 addition & 0 deletions controllers/testdata/sops/day.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
day=Tuesday
20 changes: 20 additions & 0 deletions controllers/testdata/sops/day.txt.encrypted
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"data": "ENC[AES256_GCM,data:YWPHPTVOCWivqZu0,iv:tLqbJD/KN2BchlAz1mnf4FtMY+SP5hiBYJP6dHy8gtc=,tag:Aj9T0Q7y9baA84EfEt8MfQ==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"lastmodified": "2021-04-27T20:27:20Z",
"mac": "ENC[AES256_GCM,data:1OqDvIaUpOKFa1vsa6nc+GHIvsxwQ3JhJsDTp+Yl2r8y0+n0VUbCm9FyqVvq8ur3Y3NyZfX+7FL6HxgTN0RnSMdwK1X16ioGWBk4CM3K7W8tyY7gmhddsuJqSDZdV7Hr2s7FB6LZJAHWO9vTn9zXM75Ef0B5yuOgzp29LmIhCK4=,iv:8ozNZ7IgDub2vICSzHWcAdx7/sVEoe8YayXYrAkN0BM=,tag:UwE0b6eTpA9uir+4Mwed7g==,type:str]",
"pgp": [
{
"created_at": "2021-04-27T20:27:20Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMA90SOJihaAjLAQ//cd4d6zghXW7uJ8rk0PoWiCVy5BeYwnInJT4uqJ5uUY62\nFLlsM4ZJB2SSBHGcXdwkWqTXeLLmD8aEuAe0lfutcOYyMZVWeYY+wybyJ5TgBMAo\nvEJoY67felWRb4h0BzkHIG/ZLiuDTV020GJNH2tGgE/mXVPhYosQ+EmA5EF45vfj\nqx2LjZjsCg28FK2qkXnHHjOV/12OnGpR0y6t9GijBUtttyjYaXUpNUSUiHHMjXyL\nQnKlRPt9N2QF6oUQVEwr9plNYKTfmeqUwWh6wFAaWF/104oSOwXFA8ID5wF6de1j\ntnzVf+1Ld5WNmXGmrz/6ugWfcU/3147EuPodjTyQIFMTxA6V7Z7BORjhuxFpR/jS\noZJF/SS70fg9J7sdizWKFNkqS9pPasdNHcGuXU+KGkD2ya54WyUDE86gMq0xtEf3\nMmQJRnjHuriD5EvnKmDJ+QE9nU0ld0kyfVUueHQHCtuuw7yZGi8vlyyjOq4nqCGV\nZ4TJcmpt7pKoxEAnp2tImnos7DbEoQMl7RIYgrhxS7Nej9naYeadFz/G84uwjfm0\nBr5J3A+xtG37HXQWqtd7EXmy/I94okNVXeAZuuQFt/So78jJ4H9uQK1snukPNBhr\nG8aM8SfdrTbp4KZQpm2RJwNdhbHzHoz2M2Dc6Eo14FceW0R0jYDaKTwKeNIgH6jS\nXgGdX+eJRyC1yhp6HAXOaaR9MvXJ8xCi6clWRpI9h3wxnrZtg+pERFeHhp2Ldlww\nRTjw4g3Cp9GQJB/0aTkVVOPmZ4/jpCyUS6hiV3cEE4veuDYZ20evpgO4sld6Ve8=\n=1o9a\n-----END PGP MESSAGE-----\n",
"fp": "35C1A64CD7FC0AB6EB66756B2445463C3234ECE1"
}
],
"encrypted_regex": "^(data|stringData)$",
"version": "3.6.0"
}
}
7 changes: 7 additions & 0 deletions controllers/testdata/sops/secret.day.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
data:
secret: ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpCZDJuL1VCc21KN2NYYXVvLGl2OmR4c25ncWVDVitzVVVIM24rVTZ1N1F3WjEvRFptM3RhVlVOSjJRTFNlWGM9LHRhZzp3enhxUjA2MWRmbmVhQmlMNHRxaTN3PT0sdHlwZTpzdHJdIiwKCSJzb3BzIjogewoJCSJrbXMiOiBudWxsLAoJCSJnY3Bfa21zIjogbnVsbCwKCQkiYXp1cmVfa3YiOiBudWxsLAoJCSJoY192YXVsdCI6IG51bGwsCgkJImxhc3Rtb2RpZmllZCI6ICIyMDIxLTA0LTI3VDE5OjQ4OjIwWiIsCgkJIm1hYyI6ICJFTkNbQUVTMjU2X0dDTSxkYXRhOkUwQmFsdjRGcWRiQVNRSGNpa2oyaURTeTdCTjBVSUY3cHhlSFNwUUZ2dXF6akVtRWlDV2xJRFl0dmgxY2t4ZnZxNHpYS2xITkp6QitkM1RSaHNuaGtIZ2tSWFA1MldwUkNHZ1pwY3h5Q3FCTmhhL00wRGNGY1ZZZG14T2NVNEU4eFdsdFRuektzZll0bVkvTVludmVJT1htNkpmMUhPS2FVM1EwdzBGbG8wQT0saXY6QWgza0puZEI3UnE5RVV3ZUc0TUMzUmh5bXRYRXY0ellIc2M3M3NxQzlGdz0sdGFnOnpOS3ZHcW5WNG8yWTdhNUJTV28yZEE9PSx0eXBlOnN0cl0iLAoJCSJwZ3AiOiBbCgkJCXsKCQkJCSJjcmVhdGVkX2F0IjogIjIwMjEtMDQtMjdUMTk6NDg6MjBaIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tXG5cbmhRSU1BOTBTT0ppaGFBakxBUS8vZGo2NzlnSXpKdU1vc05wdkRZQVNUOVF4MjBGdmJOREsvTDhmY2xlTFd0NHpcbktWaHdlTnRRdXhZbXA1a2Z6VUpBWXh2Mk01a2NSWFo1QzVDWDJLSER2N1lxVWRRdVdMam9wTFRmR1RPcE56RlBcbjNyZE5sRXZKMC8zSXdteEhtYXVPbUpPazByRUQrOXZtTXNCL3pnaXIxWkd2d0tTNVM5dW9XSDN6bUlVdmJBR29cbmpCKzAxRkdqaXlYRUhSanFzbFlMZ0tXczhZaVR4anNPOVd3QlU0ZmRySzRCKytEaWFweVdFcDJYVWhFZWt6MjFcbjR3dXU5dEp5TmtOWXpmMXNXUWxMc1lPblMzRkkrNSsrMFg1SmREYWtFVmkzSnF1TmtLeDRuWms3ZHJqcktnM1hcbnJPWTc1YnIxdGkwTkxrQWFsaUwvbEJSR3JCTW5BeTViV0l4dkpoSmtqRGMyZTNBWmJNbXdJQ0FJZ05kMEcrNFBcbkJqWkhNWnZUQk1RN0VvWFhGeVg5K3JKKzFnUzF5UEJuaFNma3lrSmJSR1ljdnB1RVRFL1NyK0FSa0s0cHV5bFVcbk5sdU8xZmdOMEF1STVqVll2NzJ1MzJWZEw2N2ZYbjlPdjhFYmlkdVVKcWoxZXB3Mk53dDNZK2xrNERLbVBybVRcbjFRTzF3OC96UHo1SlR0U1R4ZXFJak4weXBTazFocE9XekNwOTE0QmgxckFscXFxakorc0Q4dkVseEk2N2JSWG5cblY1alBkZkQwQktLU0tqS0ZLeVhnUHdPdCtvd2xTTDROR0V6bmdTcmsyeDlTcHVDdWQweXpoeVpta2tHRm5JKzdcbmhpT2kzeGxmZnkvRWY4TDkvaWhDbmJQc1pTck50L0RPQlVGK0ZGQUlmZitpUElPRTBieGZCaHpMNWZOTS9ZdlNcblhBRS9pYk42NktLT2ZwYWlqWnRXSkdTY1RHVVlYMkt2WTAwN2h6Y1ErR3BaZUZOd3oyUlpEd3BkTzZ4N3JHelFcbkFHQUtjd2pTYXcramluVzQwWmZnOWQ5YmFMdWRYTDRXVU9FSUdTN2FpWjNFNjJTSFJGU2U0dmNpSVh6blxuPThRcmZcbi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS1cbiIsCgkJCQkiZnAiOiAiMzVDMUE2NENEN0ZDMEFCNkVCNjY3NTZCMjQ0NTQ2M0MzMjM0RUNFMSIKCQkJfQoJCV0sCgkJImVuY3J5cHRlZF9yZWdleCI6ICJeKGRhdGF8c3RyaW5nRGF0YSkkIiwKCQkidmVyc2lvbiI6ICIzLjYuMCIKCX0KfQ==
kind: Secret
metadata:
creationTimestamp: null
name: sops-day
19 changes: 19 additions & 0 deletions docs/spec/v1beta1/kustomization.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,25 @@ spec:
name: sops-age
```

### Kustomize secretGenerator

`sops` encrypted data can be stored as a base64 encoded Secret, which enables use of kustomize secretGenerator as follows.

```console
$ echo "day=Tuesday" | sops -e /dev/stdin > day.txt.encrypted
$ cat <<EOF > kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
secretGenerator:
- name: day-secret
files:
- ./day.txt.encrypted
EOF
```

Commit and push `day.txt.encrypted` and `kustomization.yaml` to Git.

## Status

When the controller completes a Kustomization apply, reports the result in the `status` sub-resource.
Expand Down

0 comments on commit a77ea03

Please sign in to comment.