Skip to content

pkg/tlsutil: add support for custom CA #470

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

Merged
merged 1 commit into from
Sep 17, 2018

Conversation

rithujohn191
Copy link
Contributor

this PR adds support for users to provide a custom CA Key and Cert to generate other TLS assets. A unit test has also been added to test the functionality.

@@ -161,11 +165,15 @@ func (scg *SDKCertGenerator) GenerateCert(cr runtime.Object, service *v1.Service
hasAppSecret := appSecret != nil
hasCASecretAndConfigMap := caSecret != nil && caConfigMap != nil
// TODO: handle passed in CA

Copy link
Contributor

@fanminshi fanminshi Sep 10, 2018

Choose a reason for hiding this comment

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

It appears to me that the code can be simpler if we just check if ca is passed in and package them into ca secret and config before we get the CA from the cluster using getCASecretAndConfigMapInCluster at line 160. Once you have done that, the rest of the code should take care of itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes but then we would have to call the code from 247 onwards in two locations since we would have to create the configMap and secret in the cluster.

Copy link
Contributor

Choose a reason for hiding this comment

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

if we have CA config and secret are created from the passed in CA, the hasCASecretAndConfigMap is always true. Then the code never hits the case case: both CA and Application TLS assets don't exist.. It will only be either the case hasAppSecret && hasCASecretAndConfigMap or !hasAppSecret && hasCASecretAndConfigMap.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I meant to say was if we have to create a CA config and secret from the passed in CA, the flow would be as follows:

  1. Read the PEM bytes from the input file for Key and Cert
  2. Parse PEM encoded bytes
  3. Call toCASecretAndConfigmap to generate secret and ConfigMap
  4. Call scg.KubeClient.CoreV1().Secrets(ns).Create to create secret and configMap

Since the above steps are already being done in the case where both CA and Application TLS assets don't exist, I thought it would be more clear if we combined the custom CA case with it.

Copy link
Member

Choose a reason for hiding this comment

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

I thought that we should skip 4 for custom CA's?

Copy link
Contributor

@fanminshi fanminshi Sep 11, 2018

Choose a reason for hiding this comment

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

@rithujohn191 we can skip step 4. so that we don't put ca configmap and secret into the cluster which is in agreement of the interface definition.

see:

// - If CA is given:
//  - CA's key is packaged into a Secret as shown below.
//  - CA's cert is packaged in a ConfigMap as shown below.
//  - The CA Secret and ConfigMap are returned but not created in the K8s cluster in the CR's
//    namespace. The CertGenerator doesn't manage the CA because the user controls the lifecycle
//    of the CA.

https://github.com/operator-framework/operator-sdk/blob/master/pkg/tlsutil/tls.go#L79-L81

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah I see. Looks like this was finalized when I was out. Okay I will make the change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have made the changes according to the discussion

Copy link
Member

@shawn-hurley shawn-hurley left a comment

Choose a reason for hiding this comment

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

I think that we are missing the logic about not creating the config map and secret in the cluster when a custom CA is passed in.

caKey, err := newPrivateKey()
if err != nil {
return nil, nil, nil, err
if config.CAKey != "" && config.CACert != "" {
Copy link
Member

Choose a reason for hiding this comment

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

I think this chain of if and if/else could be clearer if in a switch? Thoughts?

Copy link
Contributor Author

@rithujohn191 rithujohn191 Sep 11, 2018

Choose a reason for hiding this comment

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

I switched this from an if/else to only if cause I thought it would be cleared. You would just have to look at the conditional statement as opposed to calculate which case is remaining

}
caSecret, caConfigMap := toCASecretAndConfigmap(caKey, caCert, caSecretAndConfigMapName)

caSecret, caConfigMap := toCASecretAndConfigmap(caKeyData, caCertData, caSecretAndConfigMapName)
Copy link
Member

Choose a reason for hiding this comment

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

This code would create the config map/secret for the custom CA. I thought we decided we should not do this?

@@ -158,14 +162,49 @@ func (scg *SDKCertGenerator) GenerateCert(cr runtime.Object, service *v1.Service
return nil, nil, nil, err
}

var customCAKey *rsa.PrivateKey
Copy link
Contributor

Choose a reason for hiding this comment

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

you can have

var (
caSecret *v1.Secret
caConfigMap *v1.ConfigMap
)

here instead of var customCAKey *rsa.PrivateKey

And In your if config.CAKey != "" && config.CACert != "" { block, you can wrap customCAKey and customCAKey into caSecret and caConfigMap by calling toCASecretAndConfigmap. then the rest of code should take care of itself.

The steps can be:

  1. check if custom ca is passed in.
  2. if passed in, transform those into caSecret and caConfigMap in your if config.CAKey != "" && config.CACert != "" block.
  3. if not passed, call getCASecretAndConfigMapInCluster to get CA from cluster.
  4. rest of code doesn't need to be modify.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but why would we want to transform a custom CA provided to us in PEM format to the type *v1.Secret and *v1.ConfigMap. Its a just an extra overhead

@rithujohn191
Copy link
Contributor Author

One down side of this PR is that if the user passes in a custom CA while a CA ConfigMap and secret exists the the custom CA takes preference. I have added a note in the code to highlight this:

"// Note: if a custom CA is passed in my the user it takes preference over an already
// generated CA secret and CA configmap that might exist in the cluster
"

if err != nil {
return nil, nil, nil, err
}

if config.CAKey != "" && config.CACert != "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can have a boolean value hasPassedInCA := config.CAKey != "" && config.CACert != "".
And we only call getCASecretAndConfigMapInCluster if hasPassedInCA is false. e.g

if hasPassedInCA {
...
}

if config.CAKey != "" || config.CACert != "" {
	// if only one of the custom CA Key or Cert is provided
	return nil, nil, nil, ErrCAKeyAndCACertReq
}

if !hasPassedInCA {
  caSecret, caConfigMap, err = getCASecretAndConfigMapInCluster(scg.KubeClient, caSecretAndConfigMapName, ns)
 if err != nil {
   return nil, nil, nil, err
 }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesnt really matter cause if getCASecretAndConfigMapInCluster is called before and no configMap exists the values of caSecret & caConfigMap will be nil. Then in the if config.CAKey != "" && config.CACert != "" { block the values get updated

Copy link
Member

Choose a reason for hiding this comment

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

I think this makes sense if I was using a generated cert but now have a custom CA I would expect that to take precedence.

I could see us making the change @fanminshi suggested only if we are worried about two extra get calls to the API server that we know will fail/don't care about. I personally believe that the way it is more readable and would like to keep it the way it is, but I could see a reason to change if we are worried about too many calls to the api server.

Copy link
Contributor

Choose a reason for hiding this comment

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

@rithujohn191 your code is correct. My suggestion is just an optimization because getCASecretAndConfigMapInCluster makes calls to k8s cluster which is unnecessary if custom ca is passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had the same thought process, I think code readability is better the way it is right now and 2 GET calls from the api server is not heavy weight at all. I think the way the code is currently written makes it more clear that the custom CA takes preference. But if you insist I could change it. Either way its not much of a difference.

Copy link
Contributor

Choose a reason for hiding this comment

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

@rithujohn191 let's just keep the code as it is for readability. we can revisit the code if we want to optimize it.

if err != nil {
return nil, nil, nil, err
}

if config.CAKey != "" && config.CACert != "" {
Copy link
Member

Choose a reason for hiding this comment

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

I think this makes sense if I was using a generated cert but now have a custom CA I would expect that to take precedence.

I could see us making the change @fanminshi suggested only if we are worried about two extra get calls to the API server that we know will fail/don't care about. I personally believe that the way it is more readable and would like to keep it the way it is, but I could see a reason to change if we are worried about too many calls to the api server.

caSecret, caConfigMap = toCASecretAndConfigmap(customCAKey, customCACert, caSecretAndConfigMapName)
}

if config.CAKey != "" || config.CACert != "" {
Copy link
Member

Choose a reason for hiding this comment

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

Just thinking through this, I am a little worried about this line. I believe that both if statements could be true and we could be exiting early.

config.CAKey != "" && config.CACert != ""
and
config.CAKey != "" || config.CACert != ""

I think this should be changed to an else if block on the above if statement?

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah agreed. we need an else if here. cc. @rithujohn191

cg := tlsutil.NewSDKCertGenerator(f.KubeClient)

ccfg.CAKey = "./testdata/ca.key"
ccfg.CACert = "./testdata/ca.crt"
Copy link
Contributor

Choose a reason for hiding this comment

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

let's keep the test variable the same for all tests. I think modifying the ccfgwill cause other tests to error out.

Let's just create a local *tlsutil.CertConfig for this test only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh ok will do

@rithujohn191 rithujohn191 force-pushed the custom_CA branch 12 times, most recently from 6deb3e5 to 625e18f Compare September 13, 2018 22:37
@fanminshi
Copy link
Contributor

CI is failing due to current dir is changed in TestMemcached which resulting the TestCustomCA not able to resolve the test/e2e/testdata folder ref: #483.

@rithujohn191 rithujohn191 force-pushed the custom_CA branch 4 times, most recently from a8368e2 to cd40ee5 Compare September 14, 2018 18:35
@rithujohn191
Copy link
Contributor Author

All tests pass and code has been updated. Please take a second look.

t.Fatal(err)
}

verifyAppSecret(t, appSecret, namespace)
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to check that no ca is being put into k8s cluster.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean that we should explicitly try to 'GET' the ca secret and Configmap with the given name and ensure nothing is returned?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, that's correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

@shawn-hurley shawn-hurley left a comment

Choose a reason for hiding this comment

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

LGTM after adding that ca is not added to cluster in the test

@rithujohn191 rithujohn191 force-pushed the custom_CA branch 3 times, most recently from 0a0427e to 97c4be3 Compare September 14, 2018 20:41

// ensure caConfigMap does not exist in k8s cluster.
_, err = framework.Global.KubeClient.CoreV1().Secrets(namespace).Get(caConfigMapAndSecretName, metav1.GetOptions{})
if !strings.Contains(err.Error(), "not found") {
Copy link
Contributor

Choose a reason for hiding this comment

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

you can use functions within the k8s errors pkg to check if the err is a not found error. e.g

import apiErrors "k8s.io/apimachinery/pkg/api/errors"

func TestCustomCA(t *testing.T) {
  ...
  if !apiErrors.IsNotFound(err) {
    t.Fatal(err)
 }
}
``


// ensure caConfigMap does not exist in k8s cluster.
_, err = framework.Global.KubeClient.CoreV1().Secrets(namespace).Get(caConfigMapAndSecretName, metav1.GetOptions{})
if !strings.Contains(err.Error(), "not found") {
Copy link
Contributor

Choose a reason for hiding this comment

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

use apiErrors.IsNotFound(err) instead.

@fanminshi
Copy link
Contributor

lgtm after nits.

@rithujohn191 rithujohn191 merged commit 190f8a9 into operator-framework:master Sep 17, 2018
@rithujohn191 rithujohn191 deleted the custom_CA branch September 17, 2018 17:03
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.

3 participants