diff --git a/.gitignore b/.gitignore index 845b2fdb32..3f4f721d4d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ **/obj/ **/node_modules/ **/.vs +**/.vscode **/.idea **/.ionide .pulumi diff --git a/CHANGELOG.md b/CHANGELOG.md index 7834227f29..94eb95437f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +## 3.24.0 (February 6, 2023) + +- Fix unencrypted secrets in the state `outputs` after `Secret.get` #2300 - Upgrade to latest helm and k8s client dependencies (https://github.com/pulumi/pulumi-kubernetes/pulls/2292) ## 3.23.1 (December 19, 2022) diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index f1706d420e..e30b543718 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -94,6 +94,7 @@ const ( lastAppliedConfigKey = "kubectl.kubernetes.io/last-applied-configuration" initialAPIVersionKey = "__initialApiVersion" fieldManagerKey = "__fieldManager" + secretKind = "Secret" ) type cancellationContext struct { @@ -2931,16 +2932,19 @@ func initialAPIVersion(state resource.PropertyMap, oldConfig *unstructured.Unstr func checkpointObject(inputs, live *unstructured.Unstructured, fromInputs resource.PropertyMap, initialAPIVersion, fieldManager string) resource.PropertyMap { + object := resource.NewPropertyMapFromMap(live.Object) inputsPM := resource.NewPropertyMapFromMap(inputs.Object) annotateSecrets(object, fromInputs) annotateSecrets(inputsPM, fromInputs) + isSecretKind := live.GetKind() == secretKind + // For secrets, if `stringData` is present in the inputs, the API server will have filled in `data` based on it. By // base64 encoding the secrets. We should mark any of the values which were secrets in the `stringData` object // as secrets in the `data` field as well. - if live.GetAPIVersion() == "v1" && live.GetKind() == "Secret" { + if live.GetAPIVersion() == "v1" && isSecretKind { stringData, hasStringData := fromInputs["stringData"] data, hasData := object["data"] @@ -2958,7 +2962,7 @@ func checkpointObject(inputs, live *unstructured.Unstructured, fromInputs resour // Ensure that the annotation we add for lastAppliedConfig is treated as a secret if any of the inputs were secret // (the value of this annotation is a string-ified JSON so marking the entire thing as a secret is really the best // that we can do). - if fromInputs.ContainsSecrets() { + if fromInputs.ContainsSecrets() || isSecretKind { if _, has := object["metadata"]; has && object["metadata"].IsObject() { metadata := object["metadata"].ObjectValue() if _, has := metadata["annotations"]; has && metadata["annotations"].IsObject() { @@ -3340,7 +3344,21 @@ func (pc *patchConverter) addPatchArrayToDiff( // and the order may not be preserved across an operation. This means we do end up encrypting the entire array // but that's better than accidentally leaking a value which just moved to a different location. func annotateSecrets(outs, ins resource.PropertyMap) { - if outs == nil || ins == nil { + if outs == nil { + return + } + + if kind, ok := outs["kind"]; ok && kind.StringValue() == secretKind { + if data, hasData := outs["data"]; hasData { + outs["data"] = resource.MakeSecret(data) + } + if stringData, hasStringData := outs["stringData"]; hasStringData { + outs["stringData"] = resource.MakeSecret(stringData) + } + return + } + + if ins == nil { return } diff --git a/provider/pkg/provider/provider_test.go b/provider/pkg/provider/provider_test.go index 2d74f31beb..deca26ccc2 100644 --- a/provider/pkg/provider/provider_test.go +++ b/provider/pkg/provider/provider_test.go @@ -75,6 +75,39 @@ func TestCheckpointObject(t *testing.T) { assert.Equal(t, objLive, obj.Mappable()) } +// #2300 - Read() on top-level k8s objects of kind "secret" led to unencrypted __input +func TestCheckpointSecretObject(t *testing.T) { + objInputSecret := map[string]interface{}{ + "kind": "Secret", + "data": map[string]interface{}{ + "password": "verysecret", + }, + } + objSecretLive := map[string]interface{}{ + initialAPIVersionKey: "", + fieldManagerKey: "", + "kind": "Secret", + "data": map[string]interface{}{ + "password": "verysecret", + }, + } + + // Questionable but correct pinning test as of the time of writing + assert.False(t, resource.NewPropertyMapFromMap(objInputSecret).ContainsSecrets()) + assert.False(t, resource.NewPropertyMapFromMap(objSecretLive).ContainsSecrets()) + + inputs := &unstructured.Unstructured{Object: objInputSecret} + live := &unstructured.Unstructured{Object: objSecretLive} + + obj := checkpointObject(inputs, live, nil, "", "") + assert.NotNil(t, obj) + + oldInputs := obj["__inputs"] + assert.True(t, oldInputs.IsObject()) + oldInputsVal := oldInputs.ObjectValue() + assert.True(t, oldInputsVal["data"].ContainsSecrets()) +} + func TestRoundtripCheckpointObject(t *testing.T) { old := resource.NewPropertyMapFromMap(objLive) old["__inputs"] = resource.NewObjectProperty(resource.NewPropertyMapFromMap(objInputs))