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

Tracks Secrets in __inputs and lastAppliedConfig #741

Merged
merged 2 commits into from
Aug 28, 2019
Merged

Conversation

ellismg
Copy link
Contributor

@ellismg ellismg commented Aug 23, 2019

In order to support tracking secretness that flows from inputs to
outputs when using providers that do not understand secrets directly,
the engine takes any input that is secret and if there is a
coresponding output with the same name, marks it as a secret. This
works in common cases, but does not work for Kubernetes for two key
reasons:

  1. The provider retains a copy of the inputs for a resource on an
    object called __inputs inside the state object. It uses this during
    Diff for reasons that are un-interesting to this PR.

  2. The provider JSON stringifies the inputs and stores them as an
    annotation on the object iself, as kubectl would.

These two decisions mean that if a secret value is used as an input to
a k8s resource, we will persist the plaintext value in the state
file, since the engine has no idea to look at __inputs or
lastAppliedConfig.

This change updates the provider to be able to handle secrets. The
engine will now pass any secret inputs as strongly typed secrets. The
provider will use this information to ensure that the relevent members
in the __inputs bag are marked as secrets as well as ensuring that
if there are any inputs that are secret, all of
lastAppliedConfig (which is a stringified JSON object) is marked as
a secret as well.

An integration test confirms this behavior by stringifying the state
and ensuring that our secret values do not end up in it (which will
catch cases where we may copy this data to other places as well).

Fixes #734

@ellismg
Copy link
Contributor Author

ellismg commented Aug 23, 2019

@pgavlin @lblackstone Worthwhile I think to get your eyes on this now, as I continue my validation. At a high level, this does the following:

  1. Opts the provider into accepting RPCs where the payloads from the engine have secrets annotated with correct metadata.
  2. Follows the same strategy of marking each "output" value as a secret if the same "input" value was a secret.
  3. Does the same sort of marking for the data on the __inputs field of the state.
  4. Marks the last applied config annotation as a secret if any of the inputs are secret (right now this is just a stringified JSON object that could contain secret data, because it copies in the all the inputs).

@lblackstone Interested in understanding if there are other places where we logically duplicate inputs somewhere else in the state.

We drop the secretness when we convert these PropertyMaps to Unstructure'd, so the core of the provider doesn't change much.

@ellismg
Copy link
Contributor Author

ellismg commented Aug 23, 2019

@lblackstone thinks that the travis failure here is due to a race since the helm chart does not namespace the config maps. I'll be adding some directed testing before merging.

@lblackstone
Copy link
Member

lblackstone commented Aug 23, 2019

Looks like you're on the right track, but I'm getting an error trying to add a secret to a ConfigMap:

import * as k8s from "@pulumi/kubernetes";

const pw = pulumi.secret("supers3kr3t");
const cm = new k8s.core.v1.ConfigMap("cm", {binaryData: {password: pw}});
ly failed: resource cm-l8kxa7qn/%!s(MISSING) was not successfully created by the Kubernetes API server : ConfigMap in version "v1" cannot be handled as a ConfigMap: v1.ConfigMap.ObjectMeta: v1.ObjectMeta.TypeMeta: Kind: BinaryData: decode base64: illegal base64 data at input byte 8, error found in #10 byte of ...|pers3kr3t"},"kind":"|..., bigger context ...|rsion":"v1","binaryData":{"password":"supers3kr3t"},"kind":"ConfigMap","metadata":{"annotations":{"k|...
import * as k8s from "@pulumi/kubernetes";

const pw = pulumi.secret("supers3kr3t");
const cm = new k8s.core.v1.ConfigMap("cm", {data: {password: pw}});

works because k8s base64 encodes it server-side when using data vs binaryData.

Here's what the ConfigMap looks like in k8s:

apiVersion: v1
data:
  password: supers3kr3t
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"password":"supers3kr3t"},"kind":"ConfigMap","metadata":{"annotations":{"pulumi.com/autonamed":"true"},"labels":{"app.kubernetes.io/managed-by":"pulumi"},"name":"cm-wuhxd54q"}}
    pulumi.com/autonamed: "true"
  creationTimestamp: "2019-08-23T23:18:20Z"
  labels:
    app.kubernetes.io/managed-by: pulumi
  name: cm-wuhxd54q
  namespace: default
  resourceVersion: "813035"
  selfLink: /api/v1/namespaces/default/configmaps/cm-wuhxd54q
  uid: 4ba3bb71-c5fc-11e9-b2bd-025000000001

and here's what the ConfigMap looks like in the Pulumi SaaS Console:

{
  "__inputs": {
    "apiVersion": "v1",
    "data": {
      "password": {
        "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
        "ciphertext": "AAABANZuLjRjIT4Ky6L9ETGKR1JG7G3zp/aH1QZFZqXuWfH2KSN8cXtKaWRK"
      }
    },
    "kind": "ConfigMap",
    "metadata": {
      "annotations": {
        "pulumi.com/autonamed": "true"
      },
      "labels": {
        "app.kubernetes.io/managed-by": "pulumi"
      },
      "name": "cm-wuhxd54q"
    }
  },
  "apiVersion": "v1",
  "data": {
    "password": {
      "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
      "ciphertext": "AAABAJDrt163iRrnQoG1MPQLb4Coyj22h9QDqEGoheIP+DiQULKpPq+dEPE2"
    }
  },
  "kind": "ConfigMap",
  "metadata": {
    "annotations": {
      "kubectl.kubernetes.io/last-applied-configuration": {
        "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
        "ciphertext": "AAABAAqYN+3FS9t51ROk1sz18AEP07eJIv3XjYZQ124TU67AzIw0wIHlxfN2fAuV8QOneh86J/mrw0Zylpj+0O8xy0Qlrk3CV9pGl8WvZH/7g3C5vLNvnwb0kawk3nMeWqjaPltve9rkk6jcnWFpNuZBVN+cxa1cYqeZU4z5uf+w++3le7baoBYTtmzfNn+a9PWd/BlYJ0J6JlCUaY1PS+xTyc6k6wh3fTyubUXjxFGMLvXNvYuCTgS3QzMZ5znIcf3kACvD1C/s/0OHh5pH1QcLuogtLgWQVkV88UpeGGfFVKyQ77WIcRPr+yShqKXBEJPcE/ErNW+97UjKPt1kfJRVEdcf++ta39PhyLqttw=="
      },
      "pulumi.com/autonamed": "true"
    },
    "creationTimestamp": "2019-08-23T23:18:20Z",
    "labels": {
      "app.kubernetes.io/managed-by": "pulumi"
    },
    "name": "cm-wuhxd54q",
    "namespace": "default",
    "resourceVersion": "813035",
    "selfLink": "/api/v1/namespaces/default/configmaps/cm-wuhxd54q",
    "uid": "4ba3bb71-c5fc-11e9-b2bd-025000000001"
  }
}

@lblackstone
Copy link
Member

Actually, that may be correct behavior. I think you have to base64 encode the data yourself if you use the binaryData parameter. Only possible concern there would be leaking the secret through the error message.

@lukehoban
Copy link
Contributor

lukehoban commented Aug 25, 2019

Actually, that may be correct behavior.

To first approximation yes - I think that is correct behaviour. In general, it is possible for providers which have these values in plain text to leak them through error messages via errors coming from the cloud provider. That is generally tied to places where the cloud provider does not properly treat these as secret (which Kubernetes does not in this case).

To solve this, we likely need to expand our masking technology in the CLI layer from masking on the values of secret config to being able to mask the values of any secrets that flow through the program. I've opened pulumi/pulumi#3138 to track that.

@ellismg ellismg changed the title WIP - Tracks Secrets in __inputs and lastAppliedConfig Tracks Secrets in __inputs and lastAppliedConfig Aug 26, 2019
@ellismg ellismg marked this pull request as ready for review August 26, 2019 22:23
@ellismg
Copy link
Contributor Author

ellismg commented Aug 26, 2019

Okay, @lukehoban @pgavlin @lblackstone this should now be ready for review, I wrote a small test that ensures the state file does not contain any plaintext copies of a secret, vs just checking the things we cared about being marked as secrets so if we start copying stuff to other places we'll see a test failure.

Copy link
Member

@lblackstone lblackstone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change LGTM. Should add one more test for completeness.


const pw = (new pulumi.Config()).requireSecret("message");
const cm = new k8s.core.v1.ConfigMap("cm", {
binaryData: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also test with a ConfigMap like this?

const cm = new k8s.core.v1.ConfigMap("cm", {
    data: {
        password: pw,
    }
})

k8s will base64 encode that input server-side, and it will be accessible at the same place (cm.binaryData).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, @lblackstone I have a question about this. I'm not observing the behavior you describe. For me, if I create a ConfigMap passing a secret in as just "data", the binaryData property of the object is never filled in (I've confirmed this by looking at both pulum's state file via stack export as well as the underlying object with kubectl get cm <name> -o json. In both cases, the object just has a data property. Also note that calling pulumi refresh does not cause this to appear (sensible, given the above).

Assuming that the API server behaved as you said it would - this would actually expose a problem with this. While binaryData is logically a transformed value of data and hence should be kept as a secret, it's not clear to me what part of the system would make that decision and how. I think it would have to be logic specific to ConfigMap itself, and I think that would require deeper surgery than what I have here (since lower level portions of the code that actually create resources would need to be able to observe secretness, right now everything is stripped off when we create the Unstructured objects.

So I can add the test to ensure that data is handled, but I don't think we will see the behavior you expect WRT server side encoding of data?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, whoops. I think I was mixing up Secret and ConfigMap. Let me check.

Copy link
Member

@lblackstone lblackstone Aug 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's add the following cases:

const stringSecret = new k8s.core.v1.Secret("stringSecret", {stringData: {password: pw}});
const dataSecret = new k8s.core.v1.Secret("dataSecret", {data: {password: pw.apply(d => new Buffer(d).toString("base64"))}})

export const secretStringData = stringSecret.data;
export const secretData = dataSecret.data;

(The .data field will be identical in these cases)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lblackstone - So, as I expected, this does lead to an issue. Specifically, for the stringSecret resource, the data property is not masked. Here's the relevant part of the state file for the object:

            {
                "urn": "urn:pulumi:dev-2::provider::kubernetes:core/v1:Secret::stringsecret",
                "custom": true,
                "id": "default/stringsecret-2nsgegwh",
                "type": "kubernetes:core/v1:Secret",
                "inputs": {
                    "apiVersion": "v1",
                    "kind": "Secret",
                    "metadata": {
                        "annotations": {
                            "pulumi.com/autonamed": "true"
                        },
                        "labels": {
                            "app.kubernetes.io/managed-by": "pulumi"
                        },
                        "name": "stringsecret-2nsgegwh"
                    },
                    "stringData": {
                        "password": {
                            "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
                            "ciphertext": "AAABAA/YOyfH4Zu9xLkYJKJ/pzJhUqZHf9wxjiVdPZB36SzbqpReK7t0dCUQx+kzzrY="
                        }
                    }
                },
                "outputs": {
                    "__inputs": {
                        "apiVersion": "v1",
                        "kind": "Secret",
                        "metadata": {
                            "annotations": {
                                "pulumi.com/autonamed": "true"
                            },
                            "labels": {
                                "app.kubernetes.io/managed-by": "pulumi"
                            },
                            "name": "stringsecret-2nsgegwh"
                        },
                        "stringData": {
                            "password": {
                                "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
                                "ciphertext": "AAABAO8ySfCDQbi+a52zXIQo9ptvx7RBeKU9anECcdlhihVkzsqI4ERi6CIyiDS47XA="
                            }
                        }
                    },
                    "apiVersion": "v1",
                    "data": {
                        "password": "YSBzZWNyZXQgbWVzc2FnZQ=="
                    },
                    "kind": "Secret",
                    "metadata": {
                        "annotations": {
                            "kubectl.kubernetes.io/last-applied-configuration": {
                                "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
                                "ciphertext": "AAABAHUOURpgdiT5dBi9g9Bdi4csXrxVjwMdU0x9XCCnYK7Jimq/2QzEZw+vwTfRqd++GqwU++OmlLqwjhb0AkvWQ26L0MIR7NfFCi57AQLVJ+/6kHSum1ZLsHHAb7gBPWYHvCuFZY1siWq0H/VlglY7taYC7Kg1LlRwBJgDJbEJC3n9Tm5+CQEN9lj5+u94Xq2cCKu3OebyRnStSOwUsyau4l5KDDfJLuOYz/53REhH+Olr2DlduVXTfz4JBNAcFYQezyC1Bvi+lbuTkJg2nm9A9LKzAfDo1Slpn8s4hu+X8kMa7bCQS2fzvxzlgwaV7DRfHexsP0kJhYjCmNtP0QDja/yWc7WXAre+GQXzRWgLGVvLdq7dO9zna+03NNGdFw=="
                            },
                            "pulumi.com/autonamed": "true"
                        },
                        "creationTimestamp": "2019-08-26T23:57:22Z",
                        "labels": {
                            "app.kubernetes.io/managed-by": "pulumi"
                        },
                        "name": "stringsecret-2nsgegwh",
                        "namespace": "default",
                        "resourceVersion": "67529893",
                        "selfLink": "/api/v1/namespaces/default/secrets/stringsecret-2nsgegwh",
                        "uid": "3f06a660-c85d-11e9-9301-42010a8a0098"
                    },
                    "type": "Opaque"
                },
                "parent": "urn:pulumi:dev-2::provider::pulumi:pulumi:Stack::provider-dev-2",
                "provider": "urn:pulumi:dev-2::provider::pulumi:providers:kubernetes::default_1_0_1_alpha_1566858130_g25ffc20c_dirty::1b3d4bf7-1b7e-4d7b-9ff3-6d15e3ecf88a",
                "propertyDependencies": {
                    "apiVersion": null,
                    "kind": null,
                    "stringData": null
                }
            },

Here we can see that the data property just has the b64 encoded secret value from the API server.

I'm not sure what we can do here. It feels like we would need the secret resource to understand that stringData is present and secret than data itself gets marked as a secret, but I think that will require much more work to support.

Are there other resources where the API server does something like this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, just for posterity, the following program "leaks" the pulumi.secret at the Kubernetes Secret's field .data["foo"] into the Deployment:

const s = pulumi.secret("bar");
const ss = new k8s.core.v1.Secret("bar", { stringData: { foo: s } });

// nginx container, replicated 1 time.
const appName = "nginx";
const appLabels = { app: appName };
const nginx = new k8s.apps.v1.Deployment(appName, {
    metadata: { name: "foo" },
    spec: {
        selector: { matchLabels: appLabels },
        replicas: 1,
        template: {
            metadata: {
                labels: appLabels,
                annotations: { pwd: ss.data.apply(data => (<any>data)["foo"]) },
            },
            spec: {
                containers: [
                    { name: appName, image: "nginx:1.15-alpine", ports: [{ containerPort: 80 }] },
                ],
            },
        },
    },
});

This should be pretty uncommon in practice. Secrets in the Kubernetes API are generally kept inside Secret and referenced only indirectly using something like (e.g., in the case of environment variables in containers) { envFrom: { secretRef: "my-secret" } } . But we should ship v1 knowing this is a limitation.

// the entire value in the deployment would be encrypted instead of a small chunk. It also means the entire property
// would be displayed as `[secret]` in the CLI instead of a small part.
//
// NOTE: This means that for an array, if any value in the input version is a secret, the entire output array is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be pretty impactful for things like the containers property of a deployment spec: that property is an array consisting of rich container specs. If any of these specs contains a secret, the whole array will be secret. This might be okay for now, but I would not be surprised if the user experience is not great there.

@lblackstone @hausdorff thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW - We already have the same problem today with the current implementation, since the engine does this same sort of secrets application when the provider does not understand secrets. So the containers property in the state is already going to experience this problem, even without these changes.

Copy link
Contributor

@hausdorff hausdorff Aug 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually can't think of a situation where the containers array should have a secret value directly embedded in it. Every case I know of where you'd want to embed a secret, you'd make a Secret resource, and then use the downward API to reference the relevant field in the Secret.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if someone embedded a secret value in an envvar, would we just tell them "don't do that"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think so. As long as we're not leaking the secret value, I'm fine with this workflow not being nice to work with. We should point users to the upstream docs if they run into this:

Kubernetes secret objects let you store and manage sensitive information, such as passwords, OAuth tokens, and ssh keys. Putting this information in a secret is safer and more flexible than putting it verbatim in a Pod definition or in a container image .

That also makes it work nicely with our stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect; thanks.

Copy link
Member

@pgavlin pgavlin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good overall, just one question about arrays.

@ellismg
Copy link
Contributor Author

ellismg commented Aug 27, 2019

There's an issue with this patch (which was not caught by the test I wrote) which causes a no-op update to propose changes to parts of the object. Looking into this now.

@lblackstone
Copy link
Member

@ellismg Actually, we could just special case the data field for Secrets to always be marked as secret. That’s the only case like that I’m aware of.

pkg/provider/provider.go Outdated Show resolved Hide resolved
pkg/provider/provider.go Outdated Show resolved Hide resolved
Copy link
Contributor

@hausdorff hausdorff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably can get away with not implementing the "transitive secret flow" thing I talk about here. I think we probably can't get away with not marking the auto-populated .data field in Secret, as secret when the user populates .stringData.

As was mentioned elsewhere, populating .stringData in Secret will cause the Kubernetes API server to remove that field, populating the contents of .data with b64-encoded version of whatever used to be in .stringData. This is a very common use case—right now in this case .data "leaks" this secret to the statefile.

I think it's enough to take a pretty naive approach to this—the provider should be able to just check if the resource is of the form { apiVersion: "v1", kind: "Secret", …}, and mark .data as secret as well? Could be wrong.

@hausdorff
Copy link
Contributor

hausdorff commented Aug 27, 2019

Weird, in following program, the secret value shows up as "partially leaked" and "partially null". Known issue? I would expect that all of .stringData is encrypted here.

const s = pulumi.secret({ bar: "bat" });
const ss = new k8s.core.v1.Secret("bar", { type: "Opaque", stringData: s });
    +-kubernetes:core/v1:Secret: (replace)
        [id=default/bar-5mlmrdc5]
        [urn=urn:pulumi:exp-dev::exposed-deployment::kubernetes:core/v1:Secret::bar]
        [provider=urn:pulumi:exp-dev::exposed-deployment::pulumi:providers:kubernetes::default_0_25_5::118b2eaf-a922-46a5-898e-bfa2be188f0e]
      ~ stringData: {
          + bar: <null>
          - foo: "[secret]"
        }
      + type      : "Opaque"
    --kubernetes:core/v1:Secret: (delete-replaced)
        [id=default/bar-5mlmrdc5]
        [urn=urn:pulumi:exp-dev::exposed-deployment::kubernetes:core/v1:Secret::bar]
        [provider=urn:pulumi:exp-dev::exposed-deployment::pulumi:providers:kubernetes::default_0_25_5::118b2eaf-a922-46a5-898e-bfa2be188f0e]

Yet, it is correctly reported in the statefile:

                "inputs": {
                    "apiVersion": "v1",
                    "kind": "Secret",
                    "metadata": {
                        "annotations": {
                            "pulumi.com/autonamed": "true"
                        },
                        "labels": {
                            "app.kubernetes.io/managed-by": "pulumi"
                        },
                        "name": "bar-vb4cluky"
                    },
                    "stringData": {
                        "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
                        "ciphertext": "AAABAFCguCZ0vY68z/gCeS67b6O7sktog9md3vZ7LWqA5Wg2UGXL8Lc+ym5M"
                    },
                    "type": "Opaque"
                },

@ellismg
Copy link
Contributor Author

ellismg commented Aug 27, 2019

Weird, in following program, the secret value shows up as "partially leaked" and "partially null". Known issue? I would expect that all of .stringData is encrypted here.

The output makes me believe that this is from doing an initial update, modifying the source and then doing another update with some changes. Could you please share the complete set of steps?

@hausdorff
Copy link
Contributor

The output makes me believe that this is from doing an initial update, modifying the source and then doing another update with some changes. Could you please share the complete set of steps?

Indeed, this does not happen on a fresh create. The previous version of the program is below. Change that program to the program above and it should replicate.

const s = pulumi.secret("bat");
const ss = new k8s.core.v1.Secret("bar", { type: "Opaque", stringData: { foo: s } });

@hausdorff
Copy link
Contributor

hausdorff commented Aug 27, 2019

Diffing behavior is kind of confusing when you change secrets. Change the image field in the program below to a pulumi.secret and no diff is rendered. Yet, in the statefile, I can clearly see that containers goes from being not encrypted -> encrypted. I would have expected a diff indicating plaintext -> [secret].

// nginx container, replicated 1 time.
const appName = "nginx";
const appLabels = { app: appName };
const nginx = new k8s.apps.v1.Deployment(appName, {
    metadata: { name: "foo" },
    spec: {
        selector: { matchLabels: appLabels },
        replicas: 1,
        template: {
            metadata: { labels: appLabels },
            spec: {
                containers: [
                    { name: appName, image: "nginx:1.15-alpine", ports: [{ containerPort: 80 }] },
                ],
            },
        },
    },
});

Secondly, and related to the last diff weirdness I mentioned, starting with this program:

const s = pulumi.secret({ foo: "bat"});
const ss = new k8s.core.v1.Secret("bar", { type: "Opaque", stringData: s });

and changing it to this:

const s = pulumi.secret("bat");
const ss = new k8s.core.v1.Secret("bar", { type: "Opaque", stringData: { foo: s } });

Will render no diff—and the statefile seems to keep the entire .stringData field secret—see below. This isn't a huge deal, but it might be indicative of mechanical problems that might result in "real" bugs.

                "outputs": {
                    "__inputs": {
                        "apiVersion": "v1",
                        "kind": "Secret",
                        "metadata": {
                            "annotations": {
                                "pulumi.com/autonamed": "true"
                            },
                            "labels": {
                                "app.kubernetes.io/managed-by": "pulumi"
                            },
                            "name": "bar-h0h0tphh"
                        },
                        "stringData": {
                            "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
                            "ciphertext": "AAABAJaIqOW1taCrE8B1qnSkkHCXUObE6bE9twNI95Etm2oF2BpgtCIzE6ZM"
                        },
                        "type": "Opaque"
                    },

Yet, when I change pulumi.secret("bat"); -> pulumi.secret("bar");, we can see that now only .stringData.foo is encrypted.

                "outputs": {
                    "__inputs": {
                        "apiVersion": "v1",
                        "kind": "Secret",
                        "metadata": {
                            "annotations": {
                                "pulumi.com/autonamed": "true"
                            },
                            "labels": {
                                "app.kubernetes.io/managed-by": "pulumi"
                            },
                            "name": "bar-gw54zj9e"
                        },
                        "stringData": {
                            "foo": {
                                "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270",
                                "ciphertext": "AAABANbaiqgdYXQAzw2AKQUkCKk/z6p90nd+XaRRRhoENy/Dvw=="
                            }
                        },
                        "type": "Opaque"
                    },

@hausdorff
Copy link
Contributor

FYI, I did try upgrading/downgrading kube providers, and that seems to work well. I think all my feedback is related to diff, encrypting .data, and this "transitive" secret flow that I don't think is critical to address in this review. I suspect the diff parts of this feedback are actually problems generally, though.


if hasStringData && hasData {
if stringData.IsSecret() && !data.IsSecret() {
object["data"] = resource.MakeSecret(data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to include this check rather than always marking data as secret?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we'll probably want to always mark .data as secret. Here's the current behavior:

const foo = new k8s.core.v1.Secret("foo", {
    stringData: {
        password: "foo",
    }
});
export const fooval = foo.data;
fooval          : {
      + password: "Zm9v"
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously you wouldn't want to encode secrets directly in the program like that, but they could have been generated as part of a Helm chart or similar and not marked as secret.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously you wouldn't want to encode secrets directly in the program like that, but they could have been generated as part of a Helm chart or similar and not marked as secret.

My goal here was the limit the blast radius of this change given how close we are to the end of the release. When we did stuff like this on the TF side, we ran into a bunch of issues and had to roll back the chage.

What you say would be correct, and is something we should consider doing as part of the next release, but I am concerned about making this change by default, since it will impact everyone who is using a Secret today.

If you want to opt into the behavior you describe, you can use additionalSecretOutputs to ensure the entire data field of the Secret is marked as something that should be persisted securely.

In order to support tracking secretness that flows from inputs to
outputs when using providers that do not understand secrets directly,
the engine takes any input that is secret and if there is a
coresponding output with the same name, marks it as a secret. This
works in common cases, but does not work for Kubernetes for two key
reasons:

1. The provider retains a copy of the inputs for a resource on an
object called `__inputs` inside the state object. It uses this during
Diff for reasons that are un-interesting to this PR.

2. The provider JSON stringifies the inputs and stores them as an
annotation on the object iself, as `kubectl` would.

These two decisions mean that if a secret value is used as an input to
a k8s resource, we will persist the plaintext value in the state
file, since the engine has no idea to look at `__inputs` or
`lastAppliedConfig`.

This change updates the provider to be able to handle secrets. The
engine will now pass any secret inputs as strongly typed secrets. The
provider will use this information to ensure that the relevent members
in the `__inputs` bag are marked as secrets as well as ensuring that
if there are any inputs that are secret, all of
`lastAppliedConfig` (which is a stringified JSON object) is marked as
a secret as well.

An integration test confirms this behavior by stringifying the state
and ensuring that our secret values do not end up in it (which will
catch cases where we may copy this data to other places as well).

In addition, this adds code to mark `data` as secret on
`k8s.core.v1.Secret` if `stringData` is a secret (the API Server
base64 encodes the `stringData` bag into `data` and so we should
logically flow the secretness).

Fixes #734
@ellismg
Copy link
Contributor Author

ellismg commented Aug 28, 2019

Okay, @lblackstone I have addressed all your feedback and CI should now be green.

Copy link
Member

@lblackstone lblackstone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, aside from one question: do you still need secrets.test? Not sure what that's for.

@lblackstone lblackstone merged commit f86e632 into master Aug 28, 2019
@pulumi-bot pulumi-bot deleted the ellismg/fix-734 branch August 28, 2019 16:45
@ellismg
Copy link
Contributor Author

ellismg commented Aug 28, 2019

LGTM, aside from one question: do you still need secrets.test? Not sure what that's for.

It was the test binary (generated by VSCode when trying to "debug" the test) which was incorrectly checked in because of an errant git add

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Secrets in plain text in Kubernetes resource outputs.__inputs
5 participants