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

SOPS: Decrypt Kubernetes secrets generated by kustomize #329

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
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