[ca][api]: Root rotation object in cluster#2037
Conversation
c55742d to
4d7bb0d
Compare
|
CI is failing: Not sure if this is expected, as the PR is marked WIP. |
|
@aaronlehmann Ah sorry, I had failed to rebase this one while I was trying to track down the possible test failure in #2038. Fixing, but this one is still lacking tests for changes. |
4d7bb0d to
e79d046
Compare
Codecov Report
@@ Coverage Diff @@
## master #2037 +/- ##
==========================================
- Coverage 54.22% 54.15% -0.07%
==========================================
Files 111 111
Lines 19289 19332 +43
==========================================
+ Hits 10460 10470 +10
- Misses 7571 7612 +41
+ Partials 1258 1250 -8Continue to review full report at Codecov.
|
4a7572d to
ffd74b1
Compare
|
|
||
| externalCARootPool := updatedRootCA.Pool | ||
| if rCA.RootRotation != nil { | ||
| // the extxernal CA has to trust the new CA cert |
| @@ -548,17 +551,41 @@ func (s *Server) UpdateRootCA(ctx context.Context, cluster *api.Cluster) { | |||
|
|
|||
| // If the cluster has a RootCA, let's try to update our SecurityConfig to reflect the latest values | ||
| rCA := cluster.RootCA | ||
| if len(rCA.CACert) != 0 && len(rCA.CAKey) != 0 { | ||
| externalCACert := rCA.CACert |
There was a problem hiding this comment.
why do we call the current CACert externalCACert?
There was a problem hiding this comment.
I added a field to api.ExternalCA specifying which root certificate should be associated with the external URL so we can make sure that only the right external URLs are being added. So during root rotation, only the external URLs with the right root certificate get added to the external URL.
This is the URL to filter for. Although I guess if we are explicitly not supporting external->external rotation, this change is probably not necessary.
There was a problem hiding this comment.
I've renamed this variable and added a comment about why it's here
| for _, extCA := range cluster.Spec.CAConfig.ExternalCAs { | ||
| urlsCACert := extCA.CACert | ||
| if extCA.CACert == nil { | ||
| urlsCACert = rCA.CACert // if there was no CA cert (it was nil), assume it's the original cert |
There was a problem hiding this comment.
what's the difference between rCA.CACert and externalCACert?
There was a problem hiding this comment.
rCA.CACert is the root of trust we're generally using to validate TLS certificates. externalCACert specifies which cert this external CA will use to sign - we want to match the external CA cert to the signing cert, which may be the new root cert if a root rotation is in progress. I've renamed this variable and added a comment about why it's here.
| if extCA.CACert == nil { | ||
| urlsCACert = rCA.CACert // if there was no CA cert (it was nil), assume it's the original cert | ||
| } | ||
| if extCA.Protocol == api.ExternalCA_CAProtocolCFSSL && bytes.Equal(urlsCACert, externalCACert) { |
There was a problem hiding this comment.
Same thing here rCA is only set one right? externalCACert always == rCA.CACert
There was a problem hiding this comment.
It's either the new root rotation cert, which is not set to rCA.CACert yet, or it's the rCA.CACert if there is no outstanding root rotation.
There was a problem hiding this comment.
I've renamed this variable and added a comment about why it's here
ffd74b1 to
20a4c58
Compare
|
LGTM |
ba14e1d to
fa7b565
Compare
|
This is ready for another look, whenever you have time @aaronlehmann |
| // We want to support old external CA specifications which did not have a CA cert. If there is no cert specified, | ||
| // we assume it's the old cert | ||
| certForExtCA := extCA.CACert | ||
| if certForExtCA == nil { |
There was a problem hiding this comment.
if len(certForExtCA) == 0
| "cluster.id": cluster.ID, | ||
| "method": "(*Server).updateCluster", | ||
| }).Debugf("Root CA updated successfully") | ||
| logger.Debugf("Root CA updated successfully") |
| certForExtCA = rCA.CACert | ||
| } | ||
| if extCA.Protocol == api.ExternalCA_CAProtocolCFSSL && bytes.Equal(certForExtCA, wantedExternalCACert) { | ||
| cfsslURLs = append(cfsslURLs, extCA.URL) |
There was a problem hiding this comment.
Should we log when we skip external CAs that don't match the current cert? I think it might be difficult to understand why an external CA isn't being used. Or would that be too noisy?
There was a problem hiding this comment.
It may be noisy, just because we update every time the cluster object is changed, as opposed to specifically when the RootCA object or external CAs are changed. I wonder if we should copy the RootCA object and externalCA objects to the server so that we can skip doing any changes if the objects themselves haven't changed...
There was a problem hiding this comment.
Have added this functionality in e870413.
|
After a discussion with @aaronlehmann, going to explore putting the desired signing cert and key in the cluster spec instead of this root rotation object. |
|
@cyli do we also keep the old cert/keypair for rollbacks? |
|
@diogomonica I think I still need to keep the current root CA object in the |
…s track of the root cert + key we want to rotate to, as well as a cross-signed intermediate. Signed-off-by: cyli <ying.li@docker.com>
… in the raft store, take into account whether there is a root rotation object in the RootCA object. Signed-off-by: cyli <ying.li@docker.com>
fa7b565 to
e870413
Compare
| case a != nil && b != nil: | ||
| default: | ||
| return false | ||
| } |
There was a problem hiding this comment.
This is super nitpicky, but as a former C programmer I find this confusing. How about:
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}e870413 to
39bd2fc
Compare
| // anything to update the root CA material | ||
| func (s *Server) UpdateRootCA(ctx context.Context, cluster *api.Cluster) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() |
There was a problem hiding this comment.
Wondering if any harm can come from holding the lock for the whole function. I'm not sure whether we weren't before as an optimization, or because there's a long-lived operation here, or one that might deadlock.
There was a problem hiding this comment.
It doesn't seem super long-lived, as we're not generating any new creds, so this seems relatively short. I can create a separate lock, though if we like, just for updating the security config maybe.
| } | ||
|
|
||
| // ExternalCAsEqualStable compares lists of external CAs and determines whether they are equal | ||
| func ExternalCAsEqualStable(a, b []*api.ExternalCA) bool { |
There was a problem hiding this comment.
Curious why straight up reflect.DeepEqual doesn't work for this.
There was a problem hiding this comment.
We don't actually necessarily want a reflect.DeepEqual of the whole struct - reflect.DeepEqual doesn't treat nil and empty values the same: https://play.golang.org/p/VGgZq6MVik
I figure an empty list of external CA should be the same, for our purposes, as a nil list. It also won't treat an empty map the same as a nil map (and the api.ExternalCA object has a map of options).
There was a problem hiding this comment.
Yeah, but can this situation ever come up? I believe we are always assigning s.externalCA from the protobuf object, so I don't see how an empty map could change to a nil or vice versa.
Is the concern about the first call, before we've initialized anything in the Server struct, where nil may actually be equivalent to what we would update it to (if the protobuf object contains an empty list)?
In this case, could the function be shortened to:
if len(a) == 0 && len(b) == 0 {
return true
}
return reflect.DeepEqual(a, b)There was a problem hiding this comment.
That's true - the map is created from the same object. I was writing this from the perspective of this function can be used no matter who creates the external CAs, but happy to make a change and a comment stating the assumption.
There was a problem hiding this comment.
I think it would be easier to maintain that way. I can't see any scenario where we care about nil maps vs empty maps.
The same goes for RootCAEqualStable, except in that case you're using subtle.ConstantTimeCompare, so arguably there's a security reason to avoid using reflect.DeepEqual directly (though it's a weak one, and I'm not really sure it's worth the maintenance burden).
There was a problem hiding this comment.
I could specifically pull those out to do subtle.ConstantTimeCompare, and reflect.DeepEqual the rest.
There was a problem hiding this comment.
SGTM. I guess once the keys are confirmed to be equal, comparing them with reflect.DeepEqual would not yield any side channel information.
| logger.Debugf("Root CA updated successfully") | ||
| // only update the server cache if we've successfully updated the root CA | ||
| logger.Debug("Root CA updated successfully") | ||
| s.rootCA = &cluster.RootCA |
There was a problem hiding this comment.
Let's make this cluster.RootCA.Copy() to avoid storing a pointer into the object living in the store. Just in case we were to write into it for some reason...
|
|
||
| s.securityConfig.externalCA.UpdateURLs(cfsslURLs...) | ||
| s.securityConfig.externalCA.UpdateURLs(cfsslURLs...) | ||
| s.externalCAs = cluster.Spec.CAConfig.ExternalCAs |
There was a problem hiding this comment.
cluster.Spec.CAConfig.ExternalCAs.Copy()
| securityConfig *SecurityConfig | ||
| joinTokens *api.JoinTokens | ||
| rootCA *api.RootCA | ||
| externalCAs []*api.ExternalCA |
There was a problem hiding this comment.
Wondering if we should name these something different to avoid confusing them with actual RootCA and ExternalCA objects. Not sure what to suggest, though.
There was a problem hiding this comment.
lastSeenClusterRoot and lastSeenExternalCAs maybe?
39bd2fc to
2ff526b
Compare
| @@ -3,6 +3,8 @@ package equality | |||
| import ( | |||
| "reflect" | |||
|
|
|||
|
LGTM |
…rnal CA config has changed Signed-off-by: cyli <ying.li@docker.com>
2ff526b to
563cb1d
Compare
|
Are we ready to merge this? |
|
I think so, I was just waiting for ci after removing the extra newline. |
Add a field in the API types for the new root certificate, root key, and cross-signed root certificate for use during root rotation. Also add a field to ExternalCA types to specify the root certificate that should be expected from the external CA URL.
Also, when updating the security config on cluster change, take into account whether or not this root rotation object exists.
This is stacked on top of #2038.This was merged.