Skip to content

Commit 7e21b91

Browse files
authored
Merge commit from fork
map[] in error output exposes secret data in last-applied-annotation & patch error Invalid secrets with stringData exposes the secret values in diff. Attempt a normalization to prevent it. Refactor stringData to data conversion to eliminate code duplication Signed-off-by: Siddhesh Ghadi <[email protected]>
1 parent d78929e commit 7e21b91

File tree

4 files changed

+162
-9
lines changed

4 files changed

+162
-9
lines changed

pkg/diff/diff.go

+27-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package diff
77
import (
88
"bytes"
99
"context"
10+
"encoding/base64"
1011
"encoding/json"
1112
"errors"
1213
"fmt"
@@ -843,6 +844,32 @@ func NormalizeSecret(un *unstructured.Unstructured, opts ...Option) {
843844
if gvk.Group != "" || gvk.Kind != "Secret" {
844845
return
845846
}
847+
848+
// move stringData to data section
849+
if stringData, found, err := unstructured.NestedMap(un.Object, "stringData"); found && err == nil {
850+
var data map[string]interface{}
851+
data, found, _ = unstructured.NestedMap(un.Object, "data")
852+
if !found {
853+
data = make(map[string]interface{})
854+
}
855+
856+
// base64 encode string values and add non-string values as is.
857+
// This ensures that the apply fails if the secret is invalid.
858+
for k, v := range stringData {
859+
strVal, ok := v.(string)
860+
if ok {
861+
data[k] = base64.StdEncoding.EncodeToString([]byte(strVal))
862+
} else {
863+
data[k] = v
864+
}
865+
}
866+
867+
err := unstructured.SetNestedField(un.Object, data, "data")
868+
if err == nil {
869+
delete(un.Object, "stringData")
870+
}
871+
}
872+
846873
o := applyOptions(opts)
847874
var secret corev1.Secret
848875
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &secret)
@@ -856,15 +883,6 @@ func NormalizeSecret(un *unstructured.Unstructured, opts ...Option) {
856883
secret.Data[k] = []byte("")
857884
}
858885
}
859-
if len(secret.StringData) > 0 {
860-
if secret.Data == nil {
861-
secret.Data = make(map[string][]byte)
862-
}
863-
for k, v := range secret.StringData {
864-
secret.Data[k] = []byte(v)
865-
}
866-
delete(un.Object, "stringData")
867-
}
868886
newObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&secret)
869887
if err != nil {
870888
o.log.Error(err, "object unable to convert from secret")

pkg/diff/diff_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,126 @@ func TestHideSecretDataDifferentKeysDifferentValues(t *testing.T) {
10441044
assert.Equal(t, map[string]interface{}{"key2": replacement2, "key3": replacement1}, secretData(live))
10451045
}
10461046

1047+
func TestHideStringDataInInvalidSecret(t *testing.T) {
1048+
liveUn := &unstructured.Unstructured{
1049+
Object: map[string]interface{}{
1050+
"apiVersion": "v1",
1051+
"kind": "Secret",
1052+
"metadata": map[string]interface{}{
1053+
"name": "test-secret",
1054+
},
1055+
"type": "Opaque",
1056+
"data": map[string]interface{}{
1057+
"key1": "a2V5MQ==",
1058+
"key2": "a2V5MQ==",
1059+
},
1060+
},
1061+
}
1062+
targetUn := &unstructured.Unstructured{
1063+
Object: map[string]interface{}{
1064+
"apiVersion": "v1",
1065+
"kind": "Secret",
1066+
"metadata": map[string]interface{}{
1067+
"name": "test-secret",
1068+
},
1069+
"type": "Opaque",
1070+
"data": map[string]interface{}{
1071+
"key1": "a2V5MQ==",
1072+
"key2": "a2V5Mg==",
1073+
"key3": false,
1074+
},
1075+
"stringData": map[string]interface{}{
1076+
"key4": "key4",
1077+
"key5": 5,
1078+
},
1079+
},
1080+
}
1081+
1082+
liveUn = remarshal(liveUn, applyOptions(diffOptionsForTest()))
1083+
targetUn = remarshal(targetUn, applyOptions(diffOptionsForTest()))
1084+
1085+
target, live, err := HideSecretData(targetUn, liveUn, nil)
1086+
require.NoError(t, err)
1087+
1088+
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement2}, secretData(live))
1089+
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1, "key3": replacement1, "key4": replacement1, "key5": replacement1}, secretData(target))
1090+
}
1091+
1092+
// stringData in secrets should be normalized even if it is invalid
1093+
func TestNormalizeSecret(t *testing.T) {
1094+
var tests = []struct {
1095+
testname string
1096+
data map[string]interface{}
1097+
stringData map[string]interface{}
1098+
}{
1099+
{
1100+
testname: "Valid secret",
1101+
data: map[string]interface{}{
1102+
"key1": "key1",
1103+
},
1104+
stringData: map[string]interface{}{
1105+
"key2": "a2V5Mg==",
1106+
},
1107+
},
1108+
{
1109+
testname: "Invalid secret",
1110+
data: map[string]interface{}{
1111+
"key1": "key1",
1112+
"key2": 2,
1113+
},
1114+
stringData: map[string]interface{}{
1115+
"key3": "key3",
1116+
"key4": nil,
1117+
},
1118+
},
1119+
{
1120+
testname: "Invalid secret with stringData only",
1121+
data: nil,
1122+
stringData: map[string]interface{}{
1123+
"key3": "key3",
1124+
"key4": true,
1125+
},
1126+
},
1127+
}
1128+
1129+
for _, tt := range tests {
1130+
t.Run(tt.testname, func(t *testing.T) {
1131+
un := &unstructured.Unstructured{
1132+
Object: map[string]interface{}{
1133+
"apiVersion": "v1",
1134+
"kind": "Secret",
1135+
"metadata": map[string]interface{}{
1136+
"name": "test-secret",
1137+
},
1138+
"type": "Opaque",
1139+
"data": tt.data,
1140+
"stringData": tt.stringData,
1141+
},
1142+
}
1143+
un = remarshal(un, applyOptions(diffOptionsForTest()))
1144+
1145+
NormalizeSecret(un)
1146+
1147+
_, found, _ := unstructured.NestedMap(un.Object, "stringData")
1148+
assert.False(t, found)
1149+
1150+
data, found, _ := unstructured.NestedMap(un.Object, "data")
1151+
assert.True(t, found)
1152+
1153+
// check all secret keys are found under data in normalized secret
1154+
for _, obj := range []map[string]interface{}{tt.data, tt.stringData} {
1155+
if obj == nil {
1156+
continue
1157+
}
1158+
for k := range obj {
1159+
_, ok := data[k]
1160+
assert.True(t, ok)
1161+
}
1162+
}
1163+
})
1164+
}
1165+
}
1166+
10471167
func TestHideSecretAnnotations(t *testing.T) {
10481168
tests := []struct {
10491169
name string

pkg/utils/kube/kube.go

+3
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,15 @@ var (
205205
// See ApplyOpts::Run()
206206
// cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
207207
kubectlApplyPatchErrOutRegexp = regexp.MustCompile(`(?s)^error when applying patch:.*\nfor: "\S+": `)
208+
209+
kubectlErrOutMapRegexp = regexp.MustCompile(`map\[.*\]`)
208210
)
209211

210212
// cleanKubectlOutput makes the error output of kubectl a little better to read
211213
func cleanKubectlOutput(s string) string {
212214
s = strings.TrimSpace(s)
213215
s = kubectlErrOutRegexp.ReplaceAllString(s, "")
216+
s = kubectlErrOutMapRegexp.ReplaceAllString(s, "")
214217
s = kubectlApplyPatchErrOutRegexp.ReplaceAllString(s, "")
215218
s = strings.Replace(s, "; if you choose to ignore these errors, turn validation off with --validate=false", "", -1)
216219
return s

pkg/utils/kube/kube_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ Object: &{map["apiVersion":"v1" "kind":"Service" "metadata":map["annotations":ma
7474
for: "/var/folders/_m/991sn1ds7g39lnbhp6wvqp9d_j5476/T/224503547": Service "my-service" is invalid: spec.clusterIP: Invalid value: "10.96.0.44": field is immutable`
7575
assert.Equal(t, cleanKubectlOutput(s), `Service "my-service" is invalid: spec.clusterIP: Invalid value: "10.96.0.44": field is immutable`)
7676
}
77+
{
78+
s := `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/745145319": "" is invalid: patch: Invalid value: "map[data:map[email:aaaaa password:<nil> username:<nil>] metadata:map[annotations:map[kubectl.kubernetes.io/last-applied-configuration:{\"apiVersion\":\"v1\",\"data\":{\"email\":\"aaaaa\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test\"},\"name\":\"my-secret\",\"namespace\":\"default\"},\"stringData\":{\"id\":1,\"password\":0,\"username\":\"username\"},\"type\":\"Opaque\"}\n]] stringData:map[id:1 password:0 username:username]]": error decoding from json: illegal base64 data at input byte 4`
79+
assert.Equal(t, cleanKubectlOutput(s), `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/745145319": "" is invalid: patch: Invalid value: "": error decoding from json: illegal base64 data at input byte 4`)
80+
}
81+
{
82+
s := `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/2250018703": "" is invalid: patch: Invalid value: "map[data:<nil> metadata:map[annotations:map[kubectl.kubernetes.io/last-applied-configuration:{\"apiVersion\":\"v1\",\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test\"},\"name\":\"my-secret\",\"namespace\":\"default\"},\"stringData\":{\"id\":1,\"password\":0,\"username\":\"username\"},\"type\":\"Opaque\"}\n]]]": cannot convert int64 to string`
83+
assert.Equal(t, cleanKubectlOutput(s), `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/2250018703": "" is invalid: patch: Invalid value: "": cannot convert int64 to string`)
84+
}
85+
{
86+
s := `Secret in version "v1" cannot be handled as a Secret: json: cannot unmarshal bool into Go struct field Secret.data of type []uint8`
87+
assert.Equal(t, cleanKubectlOutput(s), `Secret in version "v1" cannot be handled as a Secret: json: cannot unmarshal bool into Go struct field Secret.data of type []uint8`)
88+
}
7789
}
7890

7991
func TestInClusterKubeConfig(t *testing.T) {

0 commit comments

Comments
 (0)