Skip to content

Conversation

@vineet-garg
Copy link

@vineet-garg vineet-garg commented Aug 16, 2017

What this PR does / why we need it: Implements encryption provider based on Vault based KMS as described in proposal: PR:888
Which issue this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close that issue when PR gets merged): fixes # 49817

Special notes for your reviewer:

Release note:encryption provider based on Vault based KMS

Wu Qiang and others added 8 commits August 10, 2017 23:22
Signed-off-by: Oracle Public Cloud User <opc@k8ssecvm3.sub04190007390.secwest.oraclevcn.com>
Signed-off-by: Oracle Public Cloud User <opc@k8ssecvm3.sub04190007390.secwest.oraclevcn.com>
Signed-off-by: Oracle Public Cloud User <opc@k8ssecvm3.sub04190007390.secwest.oraclevcn.com>
Signed-off-by: Oracle Public Cloud User <opc@k8ssecvm3.sub04190007390.secwest.oraclevcn.com>
Signed-off-by: Oracle Public Cloud User <opc@k8ssecvm3.sub04190007390.secwest.oraclevcn.com>
@tjfontaine
Copy link

does this need the target branch updated?

@vineet-garg
Copy link
Author

It will be nice to have this branch kept synced with Kubernetes/kubernetes:master to avoid large merge conflicts at a later stage.

client *api.Client
encryptPath string
decryptPath string
authPath string
Copy link

Choose a reason for hiding this comment

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

A go fmt would be good as there's some mixed/misaligned indentation here and a couple of other places.

Copy link
Author

Choose a reason for hiding this comment

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

corrected the code using gofmt -w, also local verification hack/make-rules/../../hack/verify-gofmt.sh going through now.

@kksriram
Copy link

@vineet-garg My initial reaction - this commit is too large (110 files). Let's figure out offline how to break this up into smaller commits.

}
wrapper := &clientWrapper{
client: client,
encryptPath: "/v1/" + transit + "/encrypt/",
Copy link
Member

Choose a reason for hiding this comment

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

You should use path.Join for all the url building. That will let you remove all the extra trim/slash checking above. right now if config.AuthPath isn't set authPath below will be auth//. path.Join handles all that for you.


func newVaultClient(config *EnvelopeConfig) (*api.Client, error) {
vaultConfig := api.DefaultConfig()
vaultConfig.Address = config.Address
Copy link
Member

Choose a reason for hiding this comment

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

What are your thoughts on supporting ReadEnvironment() out of the box. In my experience, this is typically always a feature everyone wants since it allows you to seamlessly integrate with existing vault deployments that depend on the env vars.

You can see them all here.

Copy link
Author

Choose a reason for hiding this comment

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

Its a good idea, Though we need more configuration parameters than just client parameters, like authentication parameters, key names etc.

Choose a reason for hiding this comment

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

@jhorwit2 perhaps as a follow-on PR. I'd like to get the basic support in first.

err = c.appRoleToken(config)
default:
err = fmt.Errorf("invalid authentication configuration %+v", config)
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this validation required? The config is validated after it is initially deserialized. Nothing that I saw modifies these values either.

Copy link
Author

Choose a reason for hiding this comment

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

It is not validation, just error handling in a default case. The Default case is not expected to be reachable, but its still good to have the default case.

Copy link
Member

Choose a reason for hiding this comment

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

Should add a comment then to say this is not an expected code path. The fact that it's there currently makes it seem like it's a possible outcome.


result, ok := resp.Data["plaintext"].(string)
if !ok {
return result, fmt.Errorf("failed type assertion of vault decrypt response to string")
Copy link
Member

Choose a reason for hiding this comment

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

should add the type and/or value to the error

Copy link
Author

Choose a reason for hiding this comment

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

values are secrets and cannot be put in error messages. Type "String" is already mentioned in the err message.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, i meant add the type of the response that was not a string reflect.TypeOf(resp.Data["plaintext"])

Copy link
Author

Choose a reason for hiding this comment

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

fixed

if resp != nil {
defer resp.Body.Close()
}
if resp.StatusCode == 403 {
Copy link
Member

Choose a reason for hiding this comment

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

The way this is structured can cause this to panic if err is non-nil. It should be something like:

resp, err := c.client.RawRequest(req)
if err != nil {
  return nil, err
}

defer resp.Body.close()
if resp.StatusCode == 403 {
  return nil, &forbiddenError{version: c.version, err: err}
}

)

func init() {
KMSPluginRegistry.Register("vault", vault.KMSFactory)
Copy link
Member

Choose a reason for hiding this comment

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

This constant should be a variable in the vault package and used as the key when stripping / adding it to data.

Copy link
Author

Choose a reason for hiding this comment

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

stripping and adding of the provider name prefix to data stored is done in the core kmstransformer code which is not owned by us, It takes the provider name from here and use it for that particular provider.

Separate from this, there is also prefix processing in client.go, it just strips and adds the prefix used by vault itself.
Both prefixes can vary seperately

Copy link
Author

Choose a reason for hiding this comment

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

@jhorwit2 Thanks for reviewing the code, it was very helpful, I have made most of the changes suggested by you. Does the PR look good to you? Once internal review is over, I will create a new branch which has only 2 commits (one for code changes, one for dependencies and build) sync with latest upstream master and run all local verify/test/integration-test again.

return err
}

func (c *clientWrapper) tlsToken(config *EnvelopeConfig) error {
Copy link
Member

Choose a reason for hiding this comment

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

I feel these token methods should return (string, error) and the token should only be set in refreshToken.

Copy link
Author

Choose a reason for hiding this comment

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

changed

}

// The function type for clientWrapper.encrypt and clientWrapper.decrypt.
type encryptOrDecryptFunc func(*clientWrapper, string, string) (string, error)
Copy link
Member

Choose a reason for hiding this comment

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

The pattern here and withRefreshToken seems odd to me. Why doesn't the clientWrapper store the config that it needs to do authentication? That way you could just call encrypt/decrypt on the clientWrapper and it handles the refreshToken retry logic for you. It appears to me you always want to do the encrypt/decrypt action with the refresh token, so it should go down one level into the client wrapper.

Choose a reason for hiding this comment

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

Yes, put retry logic to client is another choice. For current implementation, it expects vault.go handle provider logic (for example parsing config, handle keyNames), client.go handle all details that communicates with vault server. I think "refresh token and retry" belongs to provider logic, because using retry or how many time to retry can be configured by provider if we want.


// Get token by login and set the value to api.Client.
func (c *clientWrapper) refreshToken(config *EnvelopeConfig, version uint) error {
c.rwmutex.Lock()
Copy link
Member

@jhorwit2 jhorwit2 Aug 22, 2017

Choose a reason for hiding this comment

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

I'm fairly certain there is a race condition here. If the token can only be used once before having to be renewed that would cause encrypt/decrypt calls to fail since they only retry once and multiple goroutines may block waiting on the same refresh token.

Copy link

@wu-qiang wu-qiang Aug 22, 2017

Choose a reason for hiding this comment

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

Yes, for those tokens with small number of use, encrypt/decrypt may fail. Actually even with more retry (for example retry 5 times), the failure may also happen. Not use retry is just for simplicity. We can make it configurable if needed.

Copy link
Member

Choose a reason for hiding this comment

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

Is this planning to be addressed prior to opening upstream? That seems like a pretty severe issue.

Copy link
Author

Choose a reason for hiding this comment

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

we will address this. There will be performance impact if num of use/expiry time is short, but it will not fail

// We may update token for api.Client, but there is no sync for api.Client.
// Read lock for encrypt/decrypt requests, write lock for login requests which
// will update token for api.Client.
rwmutex sync.RWMutex
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible for these tokens to be renewable? If so, shouldn't we be able to do all this w/o locks by calling Renew on the client?

Choose a reason for hiding this comment

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

Not all vault tokens are renewable, so we don't consider Renew on vault client.

Copy link
Member

@jhorwit2 jhorwit2 Aug 22, 2017

Choose a reason for hiding this comment

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

Should it support renewable tokens as "first class" though instead of defaulting to a refresh approach? That seems like the best performance especially in a large cluster and the least complexity. (no locking required). If not now, I imagine we would want to in the future, so thinking about that in the current design would help make that easier. If you moved the refresh logic into the client you could have a refreshClientWrapper and in the future we could add a renewalClientWrapper with the same interface that's constructed dynamically based on info returned on the original token.

Copy link
Author

Choose a reason for hiding this comment

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

I would defer this to follow-up PR

@oracle oracle deleted a comment from vineet-garg Aug 23, 2017
client: client,
encryptPath: "/" + path.Join("v1", transit, "encrypt") + "/",
decryptPath: "/" + path.Join("v1", transit, "decrypt") + "/",
authPath: "/" + path.Join(auth) + "/",
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to prefix or suffix this with slashes it can just be path.Join(stuff...)

Copy link
Author

Choose a reason for hiding this comment

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

fixed

}

func (c *clientWrapper) tlsToken(config *EnvelopeConfig) (string, error) {
resp, err := c.client.Logical().Write(c.authPath+"cert/login", nil)
Copy link
Member

@jhorwit2 jhorwit2 Aug 24, 2017

Choose a reason for hiding this comment

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

path.Join -- same goes for appRoleToken, decrypt and other requests calls if there are any.

Copy link
Author

Choose a reason for hiding this comment

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

fixed

}
return secret, nil
}
return nil, fmt.Errorf("Unexpected response code: %v received for POST request to %v", resp.StatusCode, path)
Copy link
Member

Choose a reason for hiding this comment

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

errors should start with a lowercase (this goes for other errors like below): https://github.com/golang/go/wiki/CodeReviewComments#error-strings

Copy link
Author

Choose a reason for hiding this comment

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

fixed

return result, err
}

secret, requestErr := f(s.client, key, data)
Copy link
Member

@jhorwit2 jhorwit2 Aug 24, 2017

Choose a reason for hiding this comment

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

To me this still appears like it can fail due to race conditions if the token has a limited number of uses and/or there are enough goroutines making calls to encrypt/decrypt.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, i see what you changed here. This shouldn't technically error anymore; however, it's going to have potentially bad performance when multiple goroutines all fail during the read lock calls and block on the write mutex. I think in its current state it's fine, but I feel there may be push back upstream.

Copy link
Author

Choose a reason for hiding this comment

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

Given the usage, the contention is less likely:

  1. There is caching of DEK in the framework code, so most decrypt operations will not require call to KMS providers.
  2. Encrypt operations will not be frequent as encrypt will happen only during creation or update of a secret.
  3. It is not expected that multiple secrets will be created or updated concurrently frequently.
  4. If token policy is chosen to have a reasonable num of use/expiry, 403 will not be that frequent.

As long as technically it is guaranteed to work in rare worst case scenario at a degraded performance, I think it should be good. I can put the explanation in comments so that someone reads and validates the assumptions.

Copy link
Member

Choose a reason for hiding this comment

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

👍 great explanation. Thanks!

}

for _, testCase := range invalidConfigs {
_, err := serviceTestFactory(testCase.config, server.URL, key)
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

fixed

@jhorwit2
Copy link
Member

jhorwit2 commented Aug 24, 2017

@vineet-garg I still have some worries about the locking with race conditions under enough load causing errors since it only retries once.

nit: I feel the tests could be cleaned up and consolidated using table driven tests which is the de facto standard in k8s. For example, TestOneKey and TestMoreThanOneKeys are largely the same tests. You could have a struct like

testCases := []struct{
  name string
  text string
  encryptKeys []string
}{
  { name: "test one key", text: "foo", encryptKeys: []string{"one"}},
  { name: "test two keys", text: "foo", encryptKeys: []string{"one", "two"}},
}

for _, tc := range testCases {
  t.Run(tc.name, func(t *testing.T) {
    for _, key := range tc.ecnryptKeys
        // encypt & store in slice. 

    check the stored ciphers by decrypting. 
  })
}

Also, the tests are fairly integration testy. Does the vault client not implement interfaces you can use to avoid creating test http servers and thus simplify the unit tests? Seems not to implement one. We can do that later since it'll require a new interface + fake client.

edit: the example above was just something i threw together. Idk if it's correct for those appear very similar.

@oracle oracle deleted a comment from wu-qiang Aug 25, 2017
@oracle oracle deleted a comment from vineet-garg Aug 25, 2017
@vineet-garg
Copy link
Author

@jhorwit2
I checked the test cases, the only test that could be factored into subtests was TestInvalidConfiguration.
It had that pattern where same behavior was being tested under different inputs and different configurations. In all other test cases, a different behavior is tested in each test case eg. TestOneKey TestMoreThanOneKey

@vineet-garg
Copy link
Author

Closing this pull requests as it has a large number of unorganized commits. review comments incorporated and commits organized in new pull request: #4

jhorwit2 pushed a commit that referenced this pull request Feb 21, 2018
jhorwit2 pushed a commit that referenced this pull request Apr 6, 2018
jhorwit2 pushed a commit that referenced this pull request Jul 9, 2018
Adding recent upstream changes to k8s.
jhorwit2 pushed a commit that referenced this pull request Jan 25, 2019
update from kubernetes master
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.

6 participants