Skip to content

Commit 2a03b6e

Browse files
authored
Merge pull request #329 from bob-rohan/328_base64_encoded_sops_encrypted_secrets
SOPS: Decrypt Kubernetes secrets generated by kustomize
2 parents 774da2d + a77ea03 commit 2a03b6e

File tree

6 files changed

+164
-33
lines changed

6 files changed

+164
-33
lines changed

controllers/kustomization_controller_sops_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers
1818

1919
import (
2020
"context"
21+
"encoding/base64"
2122
"fmt"
2223
"io/ioutil"
2324
"os"
@@ -95,10 +96,19 @@ var _ = Describe("KustomizationReconciler", func() {
9596
Expect(err).ToNot(HaveOccurred())
9697
ageKey, err := ioutil.ReadFile("testdata/sops/age.txt")
9798
Expect(err).ToNot(HaveOccurred())
99+
dayKey, err := ioutil.ReadFile("testdata/sops/day.txt.encrypted")
100+
Expect(err).ToNot(HaveOccurred())
101+
98102
sopsSecretKey := types.NamespacedName{
99103
Name: "sops-" + randStringRunes(5),
100104
Namespace: namespace.Name,
101105
}
106+
107+
sopsEncodedSecretKey := types.NamespacedName{
108+
Name: "sops-encoded-" + randStringRunes(5),
109+
Namespace: namespace.Name,
110+
}
111+
102112
sopsSecret := &corev1.Secret{
103113
ObjectMeta: metav1.ObjectMeta{
104114
Name: sopsSecretKey.Name,
@@ -109,7 +119,20 @@ var _ = Describe("KustomizationReconciler", func() {
109119
"age.agekey": string(ageKey),
110120
},
111121
}
122+
123+
sopsEncodedSecret := &corev1.Secret{
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: sopsEncodedSecretKey.Name,
126+
Namespace: sopsEncodedSecretKey.Namespace,
127+
},
128+
StringData: map[string]string{
129+
// base64.StdEncoding.EncodeToString replicates kustomize.secretGenerator
130+
"day.dayKey": base64.StdEncoding.EncodeToString(dayKey),
131+
},
132+
}
133+
112134
Expect(k8sClient.Create(context.Background(), sopsSecret)).To(Succeed())
135+
Expect(k8sClient.Create(context.Background(), sopsEncodedSecret)).To(Succeed())
113136

114137
kustomizationKey := types.NamespacedName{
115138
Name: "sops-" + randStringRunes(5),
@@ -158,6 +181,10 @@ var _ = Describe("KustomizationReconciler", func() {
158181
var ageSecret corev1.Secret
159182
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-age", Namespace: namespace.Name}, &ageSecret)).To(Succeed())
160183
Expect(ageSecret.Data["secret"]).To(Equal([]byte(`my-sops-age-secret`)))
184+
185+
var daySecret corev1.Secret
186+
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-day", Namespace: namespace.Name}, &daySecret)).To(Succeed())
187+
Expect(string(daySecret.Data["secret"])).To(Equal("day=Tuesday\n"))
161188
})
162189
})
163190
})

controllers/kustomization_decryptor.go

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controllers
1919
import (
2020
"bytes"
2121
"context"
22+
"encoding/base64"
2223
"fmt"
2324
"io/ioutil"
2425
"os"
@@ -75,47 +76,103 @@ func (kd *KustomizeDecryptor) Decrypt(res *resource.Resource) (*resource.Resourc
7576
return nil, err
7677
}
7778

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

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

87-
key, err := tree.Metadata.GetDataKeyWithKeyServices(
88-
[]keyservice.KeyServiceClient{
89-
intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.ageIdentities)),
90-
},
91-
)
92-
if err != nil {
93-
if userErr, ok := err.(sops.UserError); ok {
94-
err = fmt.Errorf(userErr.UserError())
84+
tree, err := store.LoadEncryptedFile(out)
85+
if err != nil {
86+
return nil, fmt.Errorf("LoadEncryptedFile: %w", err)
9587
}
96-
return nil, fmt.Errorf("GetDataKey: %w", err)
97-
}
9888

99-
cipher := aes.NewCipher()
100-
if _, err := tree.Decrypt(key, cipher); err != nil {
101-
return nil, fmt.Errorf("AES decrypt: %w", err)
102-
}
89+
key, err := tree.Metadata.GetDataKeyWithKeyServices(
90+
[]keyservice.KeyServiceClient{
91+
intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.ageIdentities)),
92+
},
93+
)
94+
if err != nil {
95+
if userErr, ok := err.(sops.UserError); ok {
96+
err = fmt.Errorf(userErr.UserError())
97+
}
98+
return nil, fmt.Errorf("GetDataKey: %w", err)
99+
}
103100

104-
data, err := store.EmitPlainFile(tree.Branches)
105-
if err != nil {
106-
return nil, fmt.Errorf("EmitPlainFile: %w", err)
107-
}
101+
cipher := aes.NewCipher()
102+
if _, err := tree.Decrypt(key, cipher); err != nil {
103+
return nil, fmt.Errorf("AES decrypt: %w", err)
104+
}
108105

109-
jsonData, err := yaml.YAMLToJSON(data)
110-
if err != nil {
111-
return nil, fmt.Errorf("YAMLToJSON: %w", err)
112-
}
106+
data, err := store.EmitPlainFile(tree.Branches)
107+
if err != nil {
108+
return nil, fmt.Errorf("EmitPlainFile: %w", err)
109+
}
110+
111+
jsonData, err := yaml.YAMLToJSON(data)
112+
if err != nil {
113+
return nil, fmt.Errorf("YAMLToJSON: %w", err)
114+
}
115+
116+
err = res.UnmarshalJSON(jsonData)
117+
if err != nil {
118+
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
119+
}
120+
return res, nil
121+
122+
} else if res.GetKind() == "Secret" {
123+
124+
dataMap := res.GetDataMap()
125+
126+
for key, value := range dataMap {
127+
128+
data, err := base64.StdEncoding.DecodeString(value)
129+
if err != nil {
130+
fmt.Println("Base64 Decode: %w", err)
131+
}
132+
133+
if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {
134+
135+
store := common.StoreForFormat(formats.Yaml)
136+
137+
tree, err := store.LoadEncryptedFile(data)
138+
if err != nil {
139+
return nil, fmt.Errorf("LoadEncryptedFile: %w", err)
140+
}
141+
142+
metadataKey, err := tree.Metadata.GetDataKeyWithKeyServices(
143+
[]keyservice.KeyServiceClient{
144+
intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.ageIdentities)),
145+
},
146+
)
147+
148+
if err != nil {
149+
if userErr, ok := err.(sops.UserError); ok {
150+
err = fmt.Errorf(userErr.UserError())
151+
}
152+
return nil, fmt.Errorf("GetDataKey: %w", err)
153+
}
154+
155+
cipher := aes.NewCipher()
156+
if _, err := tree.Decrypt(metadataKey, cipher); err != nil {
157+
return nil, fmt.Errorf("AES decrypt: %w", err)
158+
}
159+
160+
binaryStore := common.StoreForFormat(formats.Binary)
161+
162+
out, err := binaryStore.EmitPlainFile(tree.Branches)
163+
if err != nil {
164+
return nil, fmt.Errorf("EmitPlainFile: %w", err)
165+
}
166+
167+
dataMap[key] = base64.StdEncoding.EncodeToString(out)
168+
}
169+
}
170+
171+
res.SetDataMap(dataMap)
172+
173+
return res, nil
113174

114-
err = res.UnmarshalJSON(jsonData)
115-
if err != nil {
116-
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
117175
}
118-
return res, nil
119176
}
120177
return nil, nil
121178
}

controllers/testdata/sops/day.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
day=Tuesday
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"data": "ENC[AES256_GCM,data:YWPHPTVOCWivqZu0,iv:tLqbJD/KN2BchlAz1mnf4FtMY+SP5hiBYJP6dHy8gtc=,tag:Aj9T0Q7y9baA84EfEt8MfQ==,type:str]",
3+
"sops": {
4+
"kms": null,
5+
"gcp_kms": null,
6+
"azure_kv": null,
7+
"hc_vault": null,
8+
"lastmodified": "2021-04-27T20:27:20Z",
9+
"mac": "ENC[AES256_GCM,data:1OqDvIaUpOKFa1vsa6nc+GHIvsxwQ3JhJsDTp+Yl2r8y0+n0VUbCm9FyqVvq8ur3Y3NyZfX+7FL6HxgTN0RnSMdwK1X16ioGWBk4CM3K7W8tyY7gmhddsuJqSDZdV7Hr2s7FB6LZJAHWO9vTn9zXM75Ef0B5yuOgzp29LmIhCK4=,iv:8ozNZ7IgDub2vICSzHWcAdx7/sVEoe8YayXYrAkN0BM=,tag:UwE0b6eTpA9uir+4Mwed7g==,type:str]",
10+
"pgp": [
11+
{
12+
"created_at": "2021-04-27T20:27:20Z",
13+
"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",
14+
"fp": "35C1A64CD7FC0AB6EB66756B2445463C3234ECE1"
15+
}
16+
],
17+
"encrypted_regex": "^(data|stringData)$",
18+
"version": "3.6.0"
19+
}
20+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
data:
3+
secret: ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpCZDJuL1VCc21KN2NYYXVvLGl2OmR4c25ncWVDVitzVVVIM24rVTZ1N1F3WjEvRFptM3RhVlVOSjJRTFNlWGM9LHRhZzp3enhxUjA2MWRmbmVhQmlMNHRxaTN3PT0sdHlwZTpzdHJdIiwKCSJzb3BzIjogewoJCSJrbXMiOiBudWxsLAoJCSJnY3Bfa21zIjogbnVsbCwKCQkiYXp1cmVfa3YiOiBudWxsLAoJCSJoY192YXVsdCI6IG51bGwsCgkJImxhc3Rtb2RpZmllZCI6ICIyMDIxLTA0LTI3VDE5OjQ4OjIwWiIsCgkJIm1hYyI6ICJFTkNbQUVTMjU2X0dDTSxkYXRhOkUwQmFsdjRGcWRiQVNRSGNpa2oyaURTeTdCTjBVSUY3cHhlSFNwUUZ2dXF6akVtRWlDV2xJRFl0dmgxY2t4ZnZxNHpYS2xITkp6QitkM1RSaHNuaGtIZ2tSWFA1MldwUkNHZ1pwY3h5Q3FCTmhhL00wRGNGY1ZZZG14T2NVNEU4eFdsdFRuektzZll0bVkvTVludmVJT1htNkpmMUhPS2FVM1EwdzBGbG8wQT0saXY6QWgza0puZEI3UnE5RVV3ZUc0TUMzUmh5bXRYRXY0ellIc2M3M3NxQzlGdz0sdGFnOnpOS3ZHcW5WNG8yWTdhNUJTV28yZEE9PSx0eXBlOnN0cl0iLAoJCSJwZ3AiOiBbCgkJCXsKCQkJCSJjcmVhdGVkX2F0IjogIjIwMjEtMDQtMjdUMTk6NDg6MjBaIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tXG5cbmhRSU1BOTBTT0ppaGFBakxBUS8vZGo2NzlnSXpKdU1vc05wdkRZQVNUOVF4MjBGdmJOREsvTDhmY2xlTFd0NHpcbktWaHdlTnRRdXhZbXA1a2Z6VUpBWXh2Mk01a2NSWFo1QzVDWDJLSER2N1lxVWRRdVdMam9wTFRmR1RPcE56RlBcbjNyZE5sRXZKMC8zSXdteEhtYXVPbUpPazByRUQrOXZtTXNCL3pnaXIxWkd2d0tTNVM5dW9XSDN6bUlVdmJBR29cbmpCKzAxRkdqaXlYRUhSanFzbFlMZ0tXczhZaVR4anNPOVd3QlU0ZmRySzRCKytEaWFweVdFcDJYVWhFZWt6MjFcbjR3dXU5dEp5TmtOWXpmMXNXUWxMc1lPblMzRkkrNSsrMFg1SmREYWtFVmkzSnF1TmtLeDRuWms3ZHJqcktnM1hcbnJPWTc1YnIxdGkwTkxrQWFsaUwvbEJSR3JCTW5BeTViV0l4dkpoSmtqRGMyZTNBWmJNbXdJQ0FJZ05kMEcrNFBcbkJqWkhNWnZUQk1RN0VvWFhGeVg5K3JKKzFnUzF5UEJuaFNma3lrSmJSR1ljdnB1RVRFL1NyK0FSa0s0cHV5bFVcbk5sdU8xZmdOMEF1STVqVll2NzJ1MzJWZEw2N2ZYbjlPdjhFYmlkdVVKcWoxZXB3Mk53dDNZK2xrNERLbVBybVRcbjFRTzF3OC96UHo1SlR0U1R4ZXFJak4weXBTazFocE9XekNwOTE0QmgxckFscXFxakorc0Q4dkVseEk2N2JSWG5cblY1alBkZkQwQktLU0tqS0ZLeVhnUHdPdCtvd2xTTDROR0V6bmdTcmsyeDlTcHVDdWQweXpoeVpta2tHRm5JKzdcbmhpT2kzeGxmZnkvRWY4TDkvaWhDbmJQc1pTck50L0RPQlVGK0ZGQUlmZitpUElPRTBieGZCaHpMNWZOTS9ZdlNcblhBRS9pYk42NktLT2ZwYWlqWnRXSkdTY1RHVVlYMkt2WTAwN2h6Y1ErR3BaZUZOd3oyUlpEd3BkTzZ4N3JHelFcbkFHQUtjd2pTYXcramluVzQwWmZnOWQ5YmFMdWRYTDRXVU9FSUdTN2FpWjNFNjJTSFJGU2U0dmNpSVh6blxuPThRcmZcbi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS1cbiIsCgkJCQkiZnAiOiAiMzVDMUE2NENEN0ZDMEFCNkVCNjY3NTZCMjQ0NTQ2M0MzMjM0RUNFMSIKCQkJfQoJCV0sCgkJImVuY3J5cHRlZF9yZWdleCI6ICJeKGRhdGF8c3RyaW5nRGF0YSkkIiwKCQkidmVyc2lvbiI6ICIzLjYuMCIKCX0KfQ==
4+
kind: Secret
5+
metadata:
6+
creationTimestamp: null
7+
name: sops-day

docs/spec/v1beta1/kustomization.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,25 @@ spec:
962962
name: sops-age
963963
```
964964

965+
### Kustomize secretGenerator
966+
967+
`sops` encrypted data can be stored as a base64 encoded Secret, which enables use of kustomize secretGenerator as follows.
968+
969+
```console
970+
$ echo "day=Tuesday" | sops -e /dev/stdin > day.txt.encrypted
971+
$ cat <<EOF > kustomization.yaml
972+
apiVersion: kustomize.config.k8s.io/v1beta1
973+
kind: Kustomization
974+
975+
secretGenerator:
976+
- name: day-secret
977+
files:
978+
- ./day.txt.encrypted
979+
EOF
980+
```
981+
982+
Commit and push `day.txt.encrypted` and `kustomization.yaml` to Git.
983+
965984
## Status
966985

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

0 commit comments

Comments
 (0)