-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
pkg/tlsutil: add support for custom CA #470
Conversation
pkg/tlsutil/tls.go
Outdated
@@ -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 | |||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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:
- Read the PEM bytes from the input file for Key and Cert
- Parse PEM encoded bytes
- Call
toCASecretAndConfigmap
to generate secret and ConfigMap - 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this 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.
pkg/tlsutil/tls.go
Outdated
caKey, err := newPrivateKey() | ||
if err != nil { | ||
return nil, nil, nil, err | ||
if config.CAKey != "" && config.CACert != "" { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
pkg/tlsutil/tls.go
Outdated
} | ||
caSecret, caConfigMap := toCASecretAndConfigmap(caKey, caCert, caSecretAndConfigMapName) | ||
|
||
caSecret, caConfigMap := toCASecretAndConfigmap(caKeyData, caCertData, caSecretAndConfigMapName) |
There was a problem hiding this comment.
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?
cad07d3
to
9adb7f3
Compare
pkg/tlsutil/tls.go
Outdated
@@ -158,14 +162,49 @@ func (scg *SDKCertGenerator) GenerateCert(cr runtime.Object, service *v1.Service | |||
return nil, nil, nil, err | |||
} | |||
|
|||
var customCAKey *rsa.PrivateKey |
There was a problem hiding this comment.
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:
- check if custom ca is passed in.
- if passed in, transform those into
caSecret
andcaConfigMap
in yourif config.CAKey != "" && config.CACert != ""
block. - if not passed, call
getCASecretAndConfigMapInCluster
to get CA from cluster. - rest of code doesn't need to be modify.
There was a problem hiding this comment.
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
9adb7f3
to
86a4ec4
Compare
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 |
if err != nil { | ||
return nil, nil, nil, err | ||
} | ||
|
||
if config.CAKey != "" && config.CACert != "" { |
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 != "" { |
There was a problem hiding this comment.
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.
pkg/tlsutil/tls.go
Outdated
caSecret, caConfigMap = toCASecretAndConfigmap(customCAKey, customCACert, caSecretAndConfigMapName) | ||
} | ||
|
||
if config.CAKey != "" || config.CACert != "" { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
test/e2e/tls_util_test.go
Outdated
cg := tlsutil.NewSDKCertGenerator(f.KubeClient) | ||
|
||
ccfg.CAKey = "./testdata/ca.key" | ||
ccfg.CACert = "./testdata/ca.crt" |
There was a problem hiding this comment.
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 ccfg
will cause other tests to error out.
Let's just create a local *tlsutil.CertConfig
for this test only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh ok will do
6deb3e5
to
625e18f
Compare
CI is failing due to current dir is changed in |
a8368e2
to
cd40ee5
Compare
All tests pass and code has been updated. Please take a second look. |
t.Fatal(err) | ||
} | ||
|
||
verifyAppSecret(t, appSecret, namespace) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, that's correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this 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
0a0427e
to
97c4be3
Compare
test/e2e/tls_util_test.go
Outdated
|
||
// 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") { |
There was a problem hiding this comment.
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)
}
}
``
test/e2e/tls_util_test.go
Outdated
|
||
// 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") { |
There was a problem hiding this comment.
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.
lgtm after nits. |
97c4be3
to
636b3fa
Compare
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.