Mask dockerconfigjson secret types and support StringData secrets

If implemented, flux diff kustomization will managed correctly sops
managed dockerconfigjson secrets.
Sops encrypted secret with stringData maps are supported too.

Signed-off-by: Soule BA <soule@weave.works>
pull/2392/head
Soule BA 3 years ago
parent cf3f729f98
commit 2e9fd33ce5
No known key found for this signature in database
GPG Key ID: 4D40965192802994

@ -79,6 +79,18 @@ func TestDiffKustomization(t *testing.T) {
objectFile: "./testdata/diff-kustomization/value-sops-secret.yaml", objectFile: "./testdata/diff-kustomization/value-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden"), assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden"),
}, },
{
name: "diff with a sops dockerconfigjson secret object",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo",
objectFile: "./testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden"),
},
{
name: "diff with a sops stringdata secret object",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo",
objectFile: "./testdata/diff-kustomization/stringdata-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-stringdata-sops-secret.golden"),
},
} }
tmpl := map[string]string{ tmpl := map[string]string{

@ -153,12 +153,18 @@ func NewRootFlags() rootFlags {
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
logger.Failuref("%v", err)
if err, ok := err.(*RequestError); ok { if err, ok := err.(*RequestError); ok {
if err.StatusCode == 1 {
logger.Warningf("%v", err)
} else {
logger.Failuref("%v", err)
}
os.Exit(err.StatusCode) os.Exit(err.StatusCode)
} }
logger.Failuref("%v", err)
os.Exit(1) os.Exit(1)
} }
} }

@ -123,6 +123,31 @@ spec:
type: ClusterIP type: ClusterIP
--- ---
apiVersion: v1 apiVersion: v1
data:
.dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ==
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: docker-secret
namespace: default
type: kubernetes.io/dockerconfigjson
---
apiVersion: v1
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: secret-basic-auth-stringdata
namespace: default
stringData:
password: KipTT1BTKio=
username: KipTT1BTKio=
type: kubernetes.io/basic-auth
---
apiVersion: v1
data: data:
token: KipTT1BTKio= token: KipTT1BTKio=
kind: Secret kind: Secret

@ -0,0 +1,27 @@
apiVersion: v1
data:
.dockerconfigjson: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str]
kind: Secret
metadata:
name: docker-secret
type: kubernetes.io/dockerconfigjson
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eU1CTEJhVXZ4eEVYYkVV
OU90TEcrR2pYckttN0pBanJoSUZWSW1RQXlRCkUydFJ3V1NZUTBuVFF0aC9GUEcw
bUdhNjJWTkoyL1FUVi9Dc1dxUDBkM0UKLS0tIE1sQXkwcWdGaEFuY0RHQTVXM0J6
dWpJcThEbW15V3dXYXpPZklBdW1Hd1kKoIAdmGNPrEctV8h1w8KuvQ5S+BGmgqN9
MgpNmUhJjWhgcQpb5BRYpQesBOgU5TBGK7j58A6DMDKlSiYZsdQchQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2022-02-03T16:03:17Z"
mac: ENC[AES256_GCM,data:AHdYSawajwgAFwlmDN1IPNmT9vWaYKzyVIra2d6sPcjTbZ8/p+VRSRpVm4XZFFsaNnW5AUJaouwXnKYDTmJDXKlr/rQcu9kXqsssQgdzcXaA6l5uJlgsnml8ba7J3OK+iEKMax23mwQEx2EUskCd9ENOwFDkunP02sxqDNOz20k=,iv:8F5OamHt3fAVorf6p+SoIrWoqkcATSGWVoM0EK87S4M=,tag:E1mxXnc7wWkEX5BxhpLtng==,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.7.1

@ -4,6 +4,8 @@ resources:
- ./deployment.yaml - ./deployment.yaml
- ./hpa.yaml - ./hpa.yaml
- ./service.yaml - ./service.yaml
- ./dockerconfigjson-sops-secret.yaml
- ./stringdata-secret.yaml
secretGenerator: secretGenerator:
- files: - files:
- token=token.encrypted - token=token.encrypted

@ -0,0 +1,28 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth-stringdata
type: kubernetes.io/basic-auth
stringData:
username: ENC[AES256_GCM,data:uKiQR48=,iv:jh2lgyAVu7igJAgoJsnOGhjxFyvUAa9lvT21u3hhqpU=,tag:zXM2JEpk3ZEH7WfkcWXXkw==,type:str]
password: ENC[AES256_GCM,data:PyhZmNhy929JGQ==,iv:PBqPaJmSw21+kn4gIlg5VdjLNZyf613z5RUTCesBoVw=,tag:Hjc7DsuUrtsz7PYPdNkL3g==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJd0xxbDZhYjVoZzY4YWhK
d2NvMVgrSGRVUGhHRGg3R1FpVURnbmh1TDBzCjcwby85M3JaK09QVk0yZFNMb2NL
c2NQZW5hS1FhYlBHU0VoUzBVYzZYUUUKLS0tIEdaNEw2Y0VjVHpZc3pyYUtLVmJk
NmN3K2VLU0NiZ1d0VHBYbGlCM1lrNmMKeWz3yfFbMNE+ly21oLfc1XnDSPRmnlPP
wIs8lk/qrzVZ45C9GdWnnPeGZZiia46Yop9TxseUS8gCjJ6KCxJCAg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2022-02-06T12:51:07Z"
mac: ENC[AES256_GCM,data:jtdzwj19uxdxvnmXg1HkAkDA6XlKMJOYFy7uLI5t/t11LwGop5Yeo7a4nQEEELehRx9J7B6U6NiySxAxBxWx5uW5vI5c8+069VV6dkiCIefnYSzuoIhQafjlFl1/KvH7VEjIWfHYuXF09v9PEKXkxEHUYDpS3QqQ3ymHRRI08pU=, iv:xX3E7F+AM29Pm8G5oqxRfYu9E7tEBGIaHeCJYgrtFmc=,tag:MJPGusNvu05z939jg8PAwQ==,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.7.1

@ -1,4 +1,6 @@
► HorizontalPodAutoscaler/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created ► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created ► Secret/default/db-user-pass-bkbd782d2c created

@ -0,0 +1,6 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

@ -1,6 +1,8 @@
► Deployment/default/podinfo created ► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created ► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 drifted ► Secret/default/podinfo-token-77t89m9b67 drifted
data data

@ -1,6 +1,8 @@
► Deployment/default/podinfo created ► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created ► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c drifted ► Secret/default/db-user-pass-bkbd782d2c drifted

@ -7,5 +7,7 @@ spec.ports.http.port
- 9899 - 9899
+ 9898 + 9898
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created ► Secret/default/db-user-pass-bkbd782d2c created

@ -1,4 +1,6 @@
► Deployment/default/podinfo created ► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created ► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/db-user-pass-bkbd782d2c created ► Secret/default/db-user-pass-bkbd782d2c created

@ -0,0 +1,6 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

@ -0,0 +1,11 @@
apiVersion: v1
data:
.dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ==
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: docker-secret
namespace: default
type: kubernetes.io/dockerconfigjson

@ -1,5 +1,7 @@
► Deployment/default/podinfo created ► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created ► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created ► Secret/default/db-user-pass-bkbd782d2c created

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: secret-basic-auth-stringdata
namespace: default
stringData:
password: KipTT1BTKio=
username: KipTT1BTKio=
type: kubernetes.io/basic-auth

@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"sync" "sync"
"time" "time"
@ -43,6 +44,10 @@ const (
controllerName = "kustomize-controller" controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io" controllerGroup = "kustomize.toolkit.fluxcd.io"
mask = "**SOPS**" mask = "**SOPS**"
dockercfgSecretType = "kubernetes.io/dockerconfigjson"
typeField = "type"
dataField = "data"
stringDataField = "stringData"
) )
var defaultTimeout = 80 * time.Second var defaultTimeout = 80 * time.Second
@ -183,7 +188,7 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
} }
// make sure secrets are masked // make sure secrets are masked
err = trimSopsData(res) err = maskSopsData(res)
if err != nil { if err != nil {
return return
} }
@ -257,40 +262,131 @@ func (b *Builder) setOwnerLabels(res *resource.Resource) error {
return nil return nil
} }
func trimSopsData(res *resource.Resource) error { func maskSopsData(res *resource.Resource) error {
// sopsMess is the base64 encoded mask // sopsMess is the base64 encoded mask
sopsMess := base64.StdEncoding.EncodeToString([]byte(mask)) sopsMess := base64.StdEncoding.EncodeToString([]byte(mask))
if res.GetKind() == "Secret" { if res.GetKind() == "Secret" {
// get both data and stringdata maps as a secret can have both
dataMap := res.GetDataMap() dataMap := res.GetDataMap()
stringDataMap := getStringDataMap(res)
asYaml, err := res.AsYAML() asYaml, err := res.AsYAML()
if err != nil { if err != nil {
return fmt.Errorf("failed to decode secret %s data: %w", res.GetName(), err) return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
} }
//delete any sops data as we don't want to expose it // delete any sops data as we don't want to expose it
// assume that both data and stringdata are encrypted
if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) { if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) {
// delete the sops object
res.PipeE(yaml.FieldClearer{Name: "sops"}) res.PipeE(yaml.FieldClearer{Name: "sops"})
secretType, err := res.GetFieldValue(typeField)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
if v, ok := secretType.(string); ok && v == dockercfgSecretType {
// if the secret is a json docker config secret, we need to mask the data with a json object
err := maskDockerconfigjsonSopsData(dataMap)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
err = maskDockerconfigjsonSopsData(stringDataMap)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
} else {
for k := range dataMap { for k := range dataMap {
dataMap[k] = sopsMess dataMap[k] = sopsMess
} }
for k := range stringDataMap {
stringDataMap[k] = sopsMess
}
}
} else { } else {
err := maskBase64EncryptedSopsData(dataMap, sopsMess)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
err = maskSopsDataInStringDataSecret(stringDataMap, sopsMess)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
}
// set the data and stringdata maps
res.SetDataMap(dataMap)
if len(stringDataMap) > 0 {
err = res.SetMapField(yaml.NewMapRNode(&stringDataMap), stringDataField)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
}
}
return nil
}
func getStringDataMap(rn *resource.Resource) map[string]string {
n, err := rn.Pipe(yaml.Lookup(stringDataField))
if err != nil {
return nil
}
result := map[string]string{}
_ = n.VisitFields(func(node *yaml.MapNode) error {
result[yaml.GetValue(node.Key)] = yaml.GetValue(node.Value)
return nil
})
return result
}
func maskDockerconfigjsonSopsData(dataMap map[string]string) error {
sopsMess := struct {
Mask string `json:"mask"`
}{
Mask: mask,
}
maskJson, err := json.Marshal(sopsMess)
if err != nil {
return err
}
for k := range dataMap {
dataMap[k] = base64.StdEncoding.EncodeToString(maskJson)
}
return nil
}
func maskBase64EncryptedSopsData(dataMap map[string]string, mask string) error {
for k, v := range dataMap { for k, v := range dataMap {
data, err := base64.StdEncoding.DecodeString(v) data, err := base64.StdEncoding.DecodeString(v)
if err != nil { if err != nil {
if _, ok := err.(base64.CorruptInputError); ok { if _, ok := err.(base64.CorruptInputError); ok {
return fmt.Errorf("failed to decode secret %s data: %w", res.GetName(), err) return err
} }
} }
if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) { if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {
dataMap[k] = sopsMess dataMap[k] = mask
}
} }
} }
res.SetDataMap(dataMap) return nil
}
func maskSopsDataInStringDataSecret(stringDataMap map[string]string, mask string) error {
for k, v := range stringDataMap {
if bytes.Contains([]byte(v), []byte("sops")) && bytes.Contains([]byte(v), []byte("ENC[")) {
stringDataMap[k] = mask
}
} }
return nil return nil

@ -97,7 +97,7 @@ type: kubernetes.io/basic-auth
name: "secret sops secret", name: "secret sops secret",
yamlStr: `apiVersion: v1 yamlStr: `apiVersion: v1
data: data:
.dockercfg: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str] .dockerconfigjson: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str]
kind: Secret kind: Secret
metadata: metadata:
name: secret name: secret
@ -125,7 +125,7 @@ sops:
`, `,
expected: `apiVersion: v1 expected: `apiVersion: v1
data: data:
.dockercfg: KipTT1BTKio= .dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ==
kind: Secret kind: Secret
metadata: metadata:
name: secret name: secret
@ -142,7 +142,7 @@ type: kubernetes.io/dockerconfigjson
} }
resource := &resource.Resource{RNode: *r} resource := &resource.Resource{RNode: *r}
err = trimSopsData(resource) err = maskSopsData(resource)
if err != nil { if err != nil {
t.Fatalf("unable to trim sops data: %v", err) t.Fatalf("unable to trim sops data: %v", err)
} }

@ -199,17 +199,31 @@ func diff(liveFile, mergedFile string, output io.Writer) error {
} }
func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) {
data := obj.Object["data"] // get both data and stringdata maps
for _, v := range data.(map[string]interface{}) { data := obj.Object[dataField]
stringData := obj.Object[stringDataField]
if m, ok := data.(map[string]interface{}); ok && m != nil {
applySopsDiff(m, liveObject, mergedObject, change)
}
if m, ok := stringData.(map[string]interface{}); ok && m != nil {
applySopsDiff(m, liveObject, mergedObject, change)
}
}
func applySopsDiff(data map[string]interface{}, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) {
for _, v := range data {
v, err := base64.StdEncoding.DecodeString(v.(string)) v, err := base64.StdEncoding.DecodeString(v.(string))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
if bytes.Contains(v, []byte(mask)) { if bytes.Contains(v, []byte(mask)) {
if liveObject != nil && mergedObject != nil { if liveObject != nil && mergedObject != nil {
change.Action = string(ssa.UnchangedAction) change.Action = string(ssa.UnchangedAction)
dataLive := liveObject.Object["data"].(map[string]interface{}) dataLive := liveObject.Object[dataField].(map[string]interface{})
dataMerged := mergedObject.Object["data"].(map[string]interface{}) dataMerged := mergedObject.Object[dataField].(map[string]interface{})
if cmp.Diff(keys(dataLive), keys(dataMerged)) != "" { if cmp.Diff(keys(dataLive), keys(dataMerged)) != "" {
change.Action = string(ssa.ConfiguredAction) change.Action = string(ssa.ConfiguredAction)
} }

Loading…
Cancel
Save