-
Notifications
You must be signed in to change notification settings - Fork 265
OCPBUGS-30119: certrotation: update all secret types to kubernetes.io/tls preserving existing content
#1681
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package certrotation | ||
|
|
||
| import ( | ||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| func ensureMetadataUpdate(secret *corev1.Secret, owner *metav1.OwnerReference, additionalAnnotations AdditionalAnnotations) bool { | ||
| needsMetadataUpdate := false | ||
| // no ownerReference set | ||
| if owner != nil { | ||
| needsMetadataUpdate = ensureOwnerReference(&secret.ObjectMeta, owner) | ||
| } | ||
| // ownership annotations not set | ||
| return additionalAnnotations.EnsureTLSMetadataUpdate(&secret.ObjectMeta) || needsMetadataUpdate | ||
| } | ||
|
|
||
| func ensureSecretTLSTypeSet(secret *corev1.Secret) bool { | ||
| // Existing secret not found - no need to update metadata (will be done by needNewSigningCertKeyPair / NeedNewTargetCertKeyPair) | ||
| if len(secret.ResourceVersion) == 0 { | ||
| return false | ||
| } | ||
|
|
||
| // convert outdated secret type (created by pre 4.7 installer) | ||
| if secret.Type != corev1.SecretTypeTLS { | ||
| secret.Type = corev1.SecretTypeTLS | ||
| // wipe secret contents if tls.crt and tls.key are missing | ||
| _, certExists := secret.Data[corev1.TLSCertKey] | ||
| _, keyExists := secret.Data[corev1.TLSPrivateKeyKey] | ||
| if !certExists || !keyExists { | ||
| secret.Data = map[string][]byte{} | ||
| } | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,24 +62,24 @@ func (c RotatedSigningCASecret) EnsureSigningCertKeyPair(ctx context.Context) (* | |
| signingCertKeyPairSecret := originalSigningCertKeyPairSecret.DeepCopy() | ||
| if apierrors.IsNotFound(err) { | ||
| // create an empty one | ||
| signingCertKeyPairSecret = &corev1.Secret{ObjectMeta: NewTLSArtifactObjectMeta( | ||
| c.Name, | ||
| c.Namespace, | ||
| c.AdditionalAnnotations, | ||
| )} | ||
| signingCertKeyPairSecret = &corev1.Secret{ | ||
| ObjectMeta: NewTLSArtifactObjectMeta( | ||
| c.Name, | ||
| c.Namespace, | ||
| c.AdditionalAnnotations, | ||
| ), | ||
| Type: corev1.SecretTypeTLS, | ||
| } | ||
| } | ||
| signingCertKeyPairSecret.Type = corev1.SecretTypeTLS | ||
|
|
||
| needsMetadataUpdate := false | ||
| if c.Owner != nil { | ||
| needsMetadataUpdate = ensureOwnerReference(&signingCertKeyPairSecret.ObjectMeta, c.Owner) | ||
| } | ||
| needsMetadataUpdate = c.AdditionalAnnotations.EnsureTLSMetadataUpdate(&signingCertKeyPairSecret.ObjectMeta) || needsMetadataUpdate | ||
| if needsMetadataUpdate && len(signingCertKeyPairSecret.ResourceVersion) > 0 { | ||
| _, _, err := resourceapply.ApplySecret(ctx, c.Client, c.EventRecorder, signingCertKeyPairSecret) | ||
| // apply necessary metadata (possibly via delete+recreate) if secret exists | ||
| // this is done before content update to prevent unexpected rollouts | ||
| if ensureMetadataUpdate(signingCertKeyPairSecret, c.Owner, c.AdditionalAnnotations) && ensureSecretTLSTypeSet(signingCertKeyPairSecret) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to update labels before content changes only when:
The function are also mutating
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You'll want to make sure your secret always passes both functions, right? Either operator might short-circuit in a different scenario.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
correct, if either doesn't pass possible updates will be applied by ApplySecret in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thanks, this is the part that I was missing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would stuck on invalid secret type with valid metadata, #1687 fixes this case |
||
| actualSigningCertKeyPairSecret, _, err := resourceapply.ApplySecret(ctx, c.Client, c.EventRecorder, signingCertKeyPairSecret) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the following seems as a bit of a cleaner method to achieve the same var err error
signingCertKeyPairSecret, _, err = resourceapply.ApplySecret(ctx, c.Client, c.EventRecorder, signingCertKeyPairSecret)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following the pattern as in |
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| signingCertKeyPairSecret = actualSigningCertKeyPairSecret | ||
| } | ||
|
|
||
| if needed, reason := needNewSigningCertKeyPair(signingCertKeyPairSecret.Annotations, c.Refresh, c.RefreshOnlyWhenExpired); needed { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import ( | |
| clienttesting "k8s.io/client-go/testing" | ||
| "k8s.io/client-go/tools/cache" | ||
|
|
||
| "github.com/openshift/api/annotations" | ||
| "github.com/openshift/library-go/pkg/operator/events" | ||
| ) | ||
|
|
||
|
|
@@ -50,13 +51,30 @@ func TestEnsureSigningCertKeyPair(t *testing.T) { | |
| if len(actual.Data["tls.crt"]) == 0 || len(actual.Data["tls.key"]) == 0 { | ||
| t.Error(actual.Data) | ||
| } | ||
| if len(actual.Annotations) == 0 { | ||
| t.Errorf("expected certificates to be annotated") | ||
| } | ||
|
Comment on lines
+54
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks redundant, any value for annotations that would fail here is also going to fail below.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I'd prefer to a clear error message
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fine with me given that nothing out of the control of this test should be touching annotations. |
||
| ownershipValue, found := actual.Annotations[annotations.OpenShiftComponent] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer to avoid sharing values like this between tests and implementation, since it creates the opportunity to accidentally make a breaking change without seeing a test failure. Feel free to ignore if you disagree or are comfortable with it in this instance because it is declared in openshift/api.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We explicitly set
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That link didn't work for me. In this case I'm specifically referring to a change to the constant
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, hmm, |
||
| if !found { | ||
| t.Errorf("expected secret to have ownership annotations, got: %v", actual.Annotations) | ||
| } | ||
| if ownershipValue != "test" { | ||
| t.Errorf("expected ownership annotation to be 'test', got: %v", ownershipValue) | ||
| } | ||
| if len(actual.OwnerReferences) != 1 { | ||
| t.Errorf("expected to have exactly one owner reference") | ||
| } | ||
| if actual.OwnerReferences[0].Name != "operator" { | ||
| t.Errorf("expected owner reference to be 'operator', got %v", actual.OwnerReferences[0].Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "update no annotations", | ||
| initialSecret: &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "signer"}, | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "signer", ResourceVersion: "10"}, | ||
| Type: corev1.SecretTypeTLS, | ||
| Data: map[string][]byte{"tls.crt": {}, "tls.key": {}}, | ||
| }, | ||
| verifyActions: func(t *testing.T, client *kubefake.Clientset) { | ||
| t.Helper() | ||
|
|
@@ -65,27 +83,46 @@ func TestEnsureSigningCertKeyPair(t *testing.T) { | |
| t.Fatal(spew.Sdump(actions)) | ||
| } | ||
|
|
||
| if !actions[0].Matches("get", "secrets") { | ||
| t.Error(actions[0]) | ||
| } | ||
| if !actions[1].Matches("update", "secrets") { | ||
| t.Error(actions[1]) | ||
| } | ||
|
|
||
| actual := actions[1].(clienttesting.UpdateAction).GetObject().(*corev1.Secret) | ||
| if certType, _ := CertificateTypeFromObject(actual); certType != CertificateTypeSigner { | ||
| t.Errorf("expected certificate type 'signer', got: %v", certType) | ||
| } | ||
| if len(actual.Data["tls.crt"]) == 0 || len(actual.Data["tls.key"]) == 0 { | ||
| t.Error(actual.Data) | ||
| } | ||
| ownershipValue, found := actual.Annotations[annotations.OpenShiftComponent] | ||
| if !found { | ||
| t.Errorf("expected secret to have ownership annotations, got: %v", actual.Annotations) | ||
| } | ||
| if ownershipValue != "test" { | ||
| t.Errorf("expected ownership annotation to be 'test', got: %v", ownershipValue) | ||
| } | ||
| if len(actual.OwnerReferences) != 1 { | ||
| t.Errorf("expected to have exactly one owner reference") | ||
| } | ||
| if actual.OwnerReferences[0].Name != "operator" { | ||
| t.Errorf("expected owner reference to be 'operator', got %v", actual.OwnerReferences[0].Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "update no work", | ||
| initialSecret: &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "signer", | ||
| ResourceVersion: "10", | ||
| Annotations: map[string]string{ | ||
| "auth.openshift.io/certificate-not-after": "2108-09-08T22:47:31-07:00", | ||
| "auth.openshift.io/certificate-not-before": "2108-09-08T20:47:31-07:00", | ||
| annotations.OpenShiftComponent: "test", | ||
| }}, | ||
| Type: corev1.SecretTypeTLS, | ||
| Data: map[string][]byte{"tls.crt": {}, "tls.key": {}}, | ||
| }, | ||
| verifyActions: func(t *testing.T, client *kubefake.Clientset) { | ||
| t.Helper() | ||
|
|
@@ -96,6 +133,102 @@ func TestEnsureSigningCertKeyPair(t *testing.T) { | |
| }, | ||
| expectedError: "certFile missing", // this means we tried to read the cert from the existing secret. If we created one, we fail in the client check | ||
| }, | ||
| { | ||
| name: "update SecretTLSType secrets", | ||
| initialSecret: &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "signer", | ||
| ResourceVersion: "10", | ||
| Annotations: map[string]string{ | ||
| "auth.openshift.io/certificate-not-after": "2108-09-08T22:47:31-07:00", | ||
| "auth.openshift.io/certificate-not-before": "2108-09-08T20:47:31-07:00", | ||
| }}, | ||
| Type: "SecretTypeTLS", | ||
| Data: map[string][]byte{"tls.crt": {}, "tls.key": {}}, | ||
| }, | ||
| verifyActions: func(t *testing.T, client *kubefake.Clientset) { | ||
| t.Helper() | ||
| actions := client.Actions() | ||
| if len(actions) != 3 { | ||
| t.Fatal(spew.Sdump(actions)) | ||
| } | ||
|
|
||
| if !actions[0].Matches("get", "secrets") { | ||
| t.Error(actions[0]) | ||
| } | ||
| if !actions[1].Matches("delete", "secrets") { | ||
| t.Error(actions[1]) | ||
| } | ||
| if !actions[2].Matches("create", "secrets") { | ||
| t.Error(actions[2]) | ||
| } | ||
| actual := actions[2].(clienttesting.UpdateAction).GetObject().(*corev1.Secret) | ||
| if actual.Type != corev1.SecretTypeTLS { | ||
| t.Errorf("expected secret type to be kubernetes.io/tls, got: %v", actual.Type) | ||
| } | ||
| cert, found := actual.Data["tls.crt"] | ||
| if !found { | ||
| t.Errorf("expected to have tls.crt key") | ||
| } | ||
| if len(cert) != 0 { | ||
| t.Errorf("expected tls.crt to be empty, got %v", cert) | ||
| } | ||
| key, found := actual.Data["tls.key"] | ||
| if !found { | ||
| t.Errorf("expected to have tls.key key") | ||
| } | ||
| if len(key) != 0 { | ||
| t.Errorf("expected tls.key to be empty, got %v", key) | ||
| } | ||
| if len(actual.OwnerReferences) != 1 { | ||
| t.Errorf("expected to have exactly one owner reference") | ||
| } | ||
| if actual.OwnerReferences[0].Name != "operator" { | ||
| t.Errorf("expected owner reference to be 'operator', got %v", actual.OwnerReferences[0].Name) | ||
| } | ||
| }, | ||
| expectedError: "certFile missing", // this means we tried to read the cert from the existing secret. If we created one, we fail in the client check | ||
| }, | ||
| { | ||
| name: "recreate invalid type secrets", | ||
| initialSecret: &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "signer", | ||
| ResourceVersion: "10", | ||
| Annotations: map[string]string{ | ||
| "auth.openshift.io/certificate-not-after": "2108-09-08T22:47:31-07:00", | ||
| "auth.openshift.io/certificate-not-before": "2108-09-08T20:47:31-07:00", | ||
| }}, | ||
| Type: corev1.SecretTypeOpaque, | ||
| Data: map[string][]byte{"foo": {}, "bar": {}}, | ||
| }, | ||
| verifyActions: func(t *testing.T, client *kubefake.Clientset) { | ||
| t.Helper() | ||
| actions := client.Actions() | ||
| if len(actions) != 3 { | ||
| t.Fatal(spew.Sdump(actions)) | ||
| } | ||
|
|
||
| if !actions[0].Matches("get", "secrets") { | ||
| t.Error(actions[0]) | ||
| } | ||
| if !actions[1].Matches("delete", "secrets") { | ||
| t.Error(actions[1]) | ||
| } | ||
| if !actions[2].Matches("create", "secrets") { | ||
| t.Error(actions[2]) | ||
| } | ||
| actual := actions[2].(clienttesting.UpdateAction).GetObject().(*corev1.Secret) | ||
| if actual.Type != corev1.SecretTypeTLS { | ||
| t.Errorf("expected secret type to be kubernetes.io/tls, got: %v", actual.Type) | ||
| } | ||
| if len(actual.OwnerReferences) != 1 { | ||
| t.Errorf("expected to have exactly one owner reference") | ||
| } | ||
| if actual.OwnerReferences[0].Name != "operator" { | ||
| t.Errorf("expected owner reference to be 'operator', got %v", actual.OwnerReferences[0].Name) | ||
| } | ||
| }, | ||
| expectedError: "certFile missing", // this means we tried to read the cert from the existing secret. If we created one, we fail in the client check | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
|
|
@@ -116,6 +249,12 @@ func TestEnsureSigningCertKeyPair(t *testing.T) { | |
| Client: client.CoreV1(), | ||
| Lister: corev1listers.NewSecretLister(indexer), | ||
| EventRecorder: events.NewInMemoryRecorder("test"), | ||
| AdditionalAnnotations: AdditionalAnnotations{ | ||
| JiraComponent: "test", | ||
| }, | ||
| Owner: &metav1.OwnerReference{ | ||
| Name: "operator", | ||
| }, | ||
| } | ||
|
|
||
| _, err := c.EnsureSigningCertKeyPair(context.TODO()) | ||
|
|
||
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 should behave the same in practice, but since we're correcting for a specific mistake, how about:
And as an aside, can we have an issue to track removing this after N releases?
Uh oh!
There was an error while loading. Please reload this page.
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.
Confusingly
SecretTypeTLSiskubernetes.io/tlsand not "SecretTypeTLS". Soif secret.Type != corev1.SecretTypeTLSwould make sure any non-kubernetes.io/tlssecret would converted tokubernetes.io/tls(clearing up data if necessary)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.
Right, I understand, but my point is that we expect any secret managed by this controller will either have type
kubernetes.io/tlsor, if it was created pre-4.7,SecretTypeTLS. If we discover that there is some instance of a managed secret with a completely unexpected type, I would rather understand why our expectation was wrong than stomp 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.
Its perfectly possible for users to find a way to replace these certificates behind cert-syncer back and replace it with invalid contents. This code will ensure that we attempt to convert cases like pre-4.7 if possible, but also prioritize availability - invalid content would be stomped for cluster to continue functioning. Why the bad secret was stomped can be found out via audit logs (or etcd contents).
This change will also protects us from other similar cases like "old/misconfigured installer has generated certificates which we no longer expect"