From a77ea03ec6a8affe5f8571ffe89d13847c2ac217 Mon Sep 17 00:00:00 2001 From: "bob.rohan" Date: Wed, 21 Apr 2021 15:23:34 +0100 Subject: [PATCH] Decrypt base64 encoded SOPS encrypted secrets Signed-off-by: Bob Rohan --- .../kustomization_controller_sops_test.go | 27 ++++ controllers/kustomization_decryptor.go | 123 +++++++++++++----- controllers/testdata/sops/day.txt | 1 + controllers/testdata/sops/day.txt.encrypted | 20 +++ controllers/testdata/sops/secret.day.yaml | 7 + docs/spec/v1beta1/kustomization.md | 19 +++ 6 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 controllers/testdata/sops/day.txt create mode 100644 controllers/testdata/sops/day.txt.encrypted create mode 100644 controllers/testdata/sops/secret.day.yaml diff --git a/controllers/kustomization_controller_sops_test.go b/controllers/kustomization_controller_sops_test.go index 699f7fda..79f28c2c 100644 --- a/controllers/kustomization_controller_sops_test.go +++ b/controllers/kustomization_controller_sops_test.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "encoding/base64" "fmt" "io/ioutil" "os" @@ -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, @@ -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), @@ -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")) }) }) }) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index d519b6d2..96ef07ba 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -19,6 +19,7 @@ package controllers import ( "bytes" "context" + "encoding/base64" "fmt" "io/ioutil" "os" @@ -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 } diff --git a/controllers/testdata/sops/day.txt b/controllers/testdata/sops/day.txt new file mode 100644 index 00000000..9c7fc6a6 --- /dev/null +++ b/controllers/testdata/sops/day.txt @@ -0,0 +1 @@ +day=Tuesday diff --git a/controllers/testdata/sops/day.txt.encrypted b/controllers/testdata/sops/day.txt.encrypted new file mode 100644 index 00000000..87d91d3c --- /dev/null +++ b/controllers/testdata/sops/day.txt.encrypted @@ -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" + } +} \ No newline at end of file diff --git a/controllers/testdata/sops/secret.day.yaml b/controllers/testdata/sops/secret.day.yaml new file mode 100644 index 00000000..19d62784 --- /dev/null +++ b/controllers/testdata/sops/secret.day.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + secret: ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpCZDJuL1VCc21KN2NYYXVvLGl2OmR4c25ncWVDVitzVVVIM24rVTZ1N1F3WjEvRFptM3RhVlVOSjJRTFNlWGM9LHRhZzp3enhxUjA2MWRmbmVhQmlMNHRxaTN3PT0sdHlwZTpzdHJdIiwKCSJzb3BzIjogewoJCSJrbXMiOiBudWxsLAoJCSJnY3Bfa21zIjogbnVsbCwKCQkiYXp1cmVfa3YiOiBudWxsLAoJCSJoY192YXVsdCI6IG51bGwsCgkJImxhc3Rtb2RpZmllZCI6ICIyMDIxLTA0LTI3VDE5OjQ4OjIwWiIsCgkJIm1hYyI6ICJFTkNbQUVTMjU2X0dDTSxkYXRhOkUwQmFsdjRGcWRiQVNRSGNpa2oyaURTeTdCTjBVSUY3cHhlSFNwUUZ2dXF6akVtRWlDV2xJRFl0dmgxY2t4ZnZxNHpYS2xITkp6QitkM1RSaHNuaGtIZ2tSWFA1MldwUkNHZ1pwY3h5Q3FCTmhhL00wRGNGY1ZZZG14T2NVNEU4eFdsdFRuektzZll0bVkvTVludmVJT1htNkpmMUhPS2FVM1EwdzBGbG8wQT0saXY6QWgza0puZEI3UnE5RVV3ZUc0TUMzUmh5bXRYRXY0ellIc2M3M3NxQzlGdz0sdGFnOnpOS3ZHcW5WNG8yWTdhNUJTV28yZEE9PSx0eXBlOnN0cl0iLAoJCSJwZ3AiOiBbCgkJCXsKCQkJCSJjcmVhdGVkX2F0IjogIjIwMjEtMDQtMjdUMTk6NDg6MjBaIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tXG5cbmhRSU1BOTBTT0ppaGFBakxBUS8vZGo2NzlnSXpKdU1vc05wdkRZQVNUOVF4MjBGdmJOREsvTDhmY2xlTFd0NHpcbktWaHdlTnRRdXhZbXA1a2Z6VUpBWXh2Mk01a2NSWFo1QzVDWDJLSER2N1lxVWRRdVdMam9wTFRmR1RPcE56RlBcbjNyZE5sRXZKMC8zSXdteEhtYXVPbUpPazByRUQrOXZtTXNCL3pnaXIxWkd2d0tTNVM5dW9XSDN6bUlVdmJBR29cbmpCKzAxRkdqaXlYRUhSanFzbFlMZ0tXczhZaVR4anNPOVd3QlU0ZmRySzRCKytEaWFweVdFcDJYVWhFZWt6MjFcbjR3dXU5dEp5TmtOWXpmMXNXUWxMc1lPblMzRkkrNSsrMFg1SmREYWtFVmkzSnF1TmtLeDRuWms3ZHJqcktnM1hcbnJPWTc1YnIxdGkwTkxrQWFsaUwvbEJSR3JCTW5BeTViV0l4dkpoSmtqRGMyZTNBWmJNbXdJQ0FJZ05kMEcrNFBcbkJqWkhNWnZUQk1RN0VvWFhGeVg5K3JKKzFnUzF5UEJuaFNma3lrSmJSR1ljdnB1RVRFL1NyK0FSa0s0cHV5bFVcbk5sdU8xZmdOMEF1STVqVll2NzJ1MzJWZEw2N2ZYbjlPdjhFYmlkdVVKcWoxZXB3Mk53dDNZK2xrNERLbVBybVRcbjFRTzF3OC96UHo1SlR0U1R4ZXFJak4weXBTazFocE9XekNwOTE0QmgxckFscXFxakorc0Q4dkVseEk2N2JSWG5cblY1alBkZkQwQktLU0tqS0ZLeVhnUHdPdCtvd2xTTDROR0V6bmdTcmsyeDlTcHVDdWQweXpoeVpta2tHRm5JKzdcbmhpT2kzeGxmZnkvRWY4TDkvaWhDbmJQc1pTck50L0RPQlVGK0ZGQUlmZitpUElPRTBieGZCaHpMNWZOTS9ZdlNcblhBRS9pYk42NktLT2ZwYWlqWnRXSkdTY1RHVVlYMkt2WTAwN2h6Y1ErR3BaZUZOd3oyUlpEd3BkTzZ4N3JHelFcbkFHQUtjd2pTYXcramluVzQwWmZnOWQ5YmFMdWRYTDRXVU9FSUdTN2FpWjNFNjJTSFJGU2U0dmNpSVh6blxuPThRcmZcbi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS1cbiIsCgkJCQkiZnAiOiAiMzVDMUE2NENEN0ZDMEFCNkVCNjY3NTZCMjQ0NTQ2M0MzMjM0RUNFMSIKCQkJfQoJCV0sCgkJImVuY3J5cHRlZF9yZWdleCI6ICJeKGRhdGF8c3RyaW5nRGF0YSkkIiwKCQkidmVyc2lvbiI6ICIzLjYuMCIKCX0KfQ== +kind: Secret +metadata: + creationTimestamp: null + name: sops-day diff --git a/docs/spec/v1beta1/kustomization.md b/docs/spec/v1beta1/kustomization.md index 74c238ec..3cee2e52 100644 --- a/docs/spec/v1beta1/kustomization.md +++ b/docs/spec/v1beta1/kustomization.md @@ -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 < 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.