-
Notifications
You must be signed in to change notification settings - Fork 119
OCPBUGS-31384: UPSTREAM: <carry>: allow type mutation for specific secrets #1924
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,53 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package validation | ||
|
||
import ( | ||
"k8s.io/kubernetes/pkg/apis/core" | ||
) | ||
|
||
var ( | ||
// we have multiple controllers reconciling the same secret, | ||
// resulting in unexpected outcomes such as the generation of new key pairs. | ||
// our goal is to prevent the generation of new key pairs by disallowing | ||
// deletions and permitting only updates, which appear to be 'safe'. | ||
// | ||
// thus we make an exception for the secrets in the following namespaces, during update | ||
// we allow the secret type to mutate from: | ||
// "SecretTypeTLS" -> "kubernetes.io/tls" | ||
// some of our operators were accidentally creating secrets of type | ||
// "SecretTypeTLS", and this patch enables us to move these secrets | ||
// objects to the intended type in a ratcheting manner. | ||
// | ||
// we can drop this patch when we migrate all of the affected secret | ||
// objects to to intended type: https://issues.redhat.com/browse/API-1800 | ||
whitelist = map[string]struct{}{ | ||
"openshift-kube-apiserver-operator": {}, | ||
"openshift-kube-apiserver": {}, | ||
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. |
||
"openshift-kube-controller-manager-operator": {}, | ||
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. and 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. |
||
} | ||
tkashem marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
func openShiftValidateSecretUpdateIsTypeMutationAllowed(newSecret, oldSecret *core.Secret) bool { | ||
// we allow "SecretTypeTLS" -> "kubernetes.io/tls" only | ||
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. Explain that some of our operators were accidentally creating secrets with type SecretTypeTLS and this patch is to allow ratcheting them to the intended type. |
||
if oldSecret.Type == "SecretTypeTLS" && newSecret.Type == core.SecretTypeTLS { | ||
if _, ok := whitelist[oldSecret.Namespace]; ok { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package validation | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/util/validation/field" | ||
"k8s.io/kubernetes/pkg/apis/core" | ||
) | ||
|
||
func TestOpenShiftValidateSecretUpdate(t *testing.T) { | ||
newSecretFn := func(ns, name string, secretType core.SecretType) *core.Secret { | ||
return &core.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: name, | ||
Namespace: ns, | ||
ResourceVersion: "1", | ||
}, | ||
Type: secretType, | ||
Data: map[string][]byte{ | ||
"tls.key": []byte("foo"), | ||
"tls.crt": []byte("bar"), | ||
}, | ||
} | ||
} | ||
invalidTypeErrFn := func(secretType core.SecretType) field.ErrorList { | ||
return field.ErrorList{ | ||
field.Invalid(field.NewPath("type"), secretType, "field is immutable"), | ||
} | ||
} | ||
tlsKeyRequiredErrFn := func() field.ErrorList { | ||
return field.ErrorList{ | ||
field.Required(field.NewPath("data").Key(core.TLSCertKey), ""), | ||
field.Required(field.NewPath("data").Key(core.TLSPrivateKeyKey), ""), | ||
} | ||
} | ||
|
||
t.Run("verify whitelist", func(t *testing.T) { | ||
for key := range whitelist { | ||
ns, name := key, "foo" | ||
|
||
// exercise a valid type mutation: "SecretTypeTLS" -> "kubernetes.io/tls" | ||
oldSecret, newSecret := newSecretFn(ns, name, "SecretTypeTLS"), newSecretFn(ns, name, core.SecretTypeTLS) | ||
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. Please add the reverse direction too. |
||
if errs := ValidateSecretUpdate(newSecret, oldSecret); len(errs) > 0 { | ||
t.Errorf("unexpected error: %v", errs) | ||
} | ||
|
||
// the reverse should not be allowed | ||
errExpected := invalidTypeErrFn("SecretTypeTLS") | ||
oldSecret, newSecret = newSecretFn(ns, name, core.SecretTypeTLS), newSecretFn(ns, name, "SecretTypeTLS") | ||
if errGot := ValidateSecretUpdate(newSecret, oldSecret); !cmp.Equal(errExpected, errGot) { | ||
t.Errorf("expected error: %v, diff: %s", errExpected, cmp.Diff(errExpected, errGot)) | ||
} | ||
|
||
// no type change, no validation failure expected | ||
oldSecret, newSecret = newSecretFn(ns, name, core.SecretTypeTLS), newSecretFn(ns, name, core.SecretTypeTLS) | ||
if errs := ValidateSecretUpdate(newSecret, oldSecret); len(errs) > 0 { | ||
t.Errorf("unexpected error: %v", errs) | ||
} | ||
|
||
// exercise an invalid type mutation, we expect validation failure | ||
errExpected = invalidTypeErrFn(core.SecretTypeOpaque) | ||
oldSecret, newSecret = newSecretFn(ns, name, "SecretTypeTLS"), newSecretFn(ns, name, core.SecretTypeOpaque) | ||
if errGot := ValidateSecretUpdate(newSecret, oldSecret); !cmp.Equal(errExpected, errGot) { | ||
t.Errorf("expected error: %v, diff: %s", errExpected, cmp.Diff(errExpected, errGot)) | ||
} | ||
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 don't doubt that it works but I would like to see a test that shows the kubernetes.io/tls key validation is still enforced on these updates. |
||
|
||
// verify that kbernetes.io/tls validation ar enforced | ||
errExpected = tlsKeyRequiredErrFn() | ||
oldSecret, newSecret = newSecretFn(ns, name, "SecretTypeTLS"), newSecretFn(ns, name, core.SecretTypeTLS) | ||
newSecret.Data = nil | ||
if errGot := ValidateSecretUpdate(newSecret, oldSecret); !cmp.Equal(errExpected, errGot) { | ||
t.Errorf("expected error: %v, diff: %s", errExpected, cmp.Diff(errExpected, errGot)) | ||
} | ||
} | ||
}) | ||
|
||
// we must not break secrets that are not in the whitelist | ||
tests := []struct { | ||
name string | ||
oldSecret *core.Secret | ||
newSecret *core.Secret | ||
errExpected field.ErrorList | ||
}{ | ||
{ | ||
name: "secret is not whitelisted, valid type transition, update not allowed", | ||
oldSecret: newSecretFn("foo", "bar", "SecretTypeTLS"), | ||
newSecret: newSecretFn("foo", "bar", core.SecretTypeTLS), | ||
errExpected: invalidTypeErrFn(core.SecretTypeTLS), | ||
}, | ||
{ | ||
name: "secret is not whitelisted, invalid type transition, update not allowed", | ||
oldSecret: newSecretFn("foo", "bar", "SecretTypeTLS"), | ||
newSecret: newSecretFn("foo", "bar", core.SecretTypeOpaque), | ||
errExpected: invalidTypeErrFn(core.SecretTypeOpaque), | ||
}, | ||
{ | ||
name: "secret is not whitelisted, no type transition, update allowed", | ||
oldSecret: newSecretFn("foo", "bar", core.SecretTypeTLS), | ||
newSecret: newSecretFn("foo", "bar", core.SecretTypeTLS), | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if _, ok := whitelist[test.oldSecret.Namespace]; ok { | ||
t.Errorf("misconfigured test: secret is in whitelist: %s", test.oldSecret.Namespace) | ||
return | ||
} | ||
|
||
errGot := ValidateSecretUpdate(test.newSecret, test.oldSecret) | ||
if !cmp.Equal(test.errExpected, errGot) { | ||
t.Errorf("expected error: %v, diff: %s", test.errExpected, cmp.Diff(test.errExpected, errGot)) | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package apiserver | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" | ||
"k8s.io/kubernetes/test/integration/framework" | ||
) | ||
|
||
// the list was copied from pkg/apis/core/validation/validation_patch.go | ||
var whitelistedSecretNamespaces = map[string]struct{}{ | ||
"openshift-kube-apiserver-operator": {}, | ||
"openshift-kube-apiserver": {}, | ||
"openshift-kube-controller-manager-operator": {}, | ||
} | ||
|
||
func TestOpenShiftValidateWhiteListedSecretTypeMutationUpdateAllowed(t *testing.T) { | ||
ctx := context.Background() | ||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
t.Cleanup(server.TearDownFn) | ||
client, err := kubernetes.NewForConfig(server.ClientConfig) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
for whiteListedSecretNamespace := range whitelistedSecretNamespaces { | ||
_, err := client.CoreV1().Namespaces().Get(ctx, whiteListedSecretNamespace, metav1.GetOptions{}) | ||
if apierrors.IsNotFound(err) { | ||
testNamespace := framework.CreateNamespaceOrDie(client, whiteListedSecretNamespace, t) | ||
t.Cleanup(func() { framework.DeleteNamespaceOrDie(client, testNamespace, t) }) | ||
} else if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
secret := constructSecretWithOldType(whiteListedSecretNamespace, "foo") | ||
createdSecret, err := client.CoreV1().Secrets(whiteListedSecretNamespace).Create(ctx, secret, metav1.CreateOptions{}) | ||
if err != nil { | ||
t.Errorf("failed to create secret, err = %v", err) | ||
} | ||
|
||
createdSecret.Type = corev1.SecretTypeTLS | ||
updatedSecret, err := client.CoreV1().Secrets(whiteListedSecretNamespace).Update(ctx, createdSecret, metav1.UpdateOptions{}) | ||
if err != nil { | ||
t.Errorf("failed to update the type of the secret, err = %v", err) | ||
} | ||
if updatedSecret.Type != corev1.SecretTypeTLS { | ||
t.Errorf("unexpected type of the secret = %v, expected = %v", updatedSecret.Type, corev1.SecretTypeTLS) | ||
} | ||
|
||
// "kubernetes.io/tls" -> "SecretTypeTLS" is not allowed | ||
toUpdateSecret := updatedSecret | ||
toUpdateSecret.Type = "SecretTypeTLS" | ||
_, err = client.CoreV1().Secrets(whiteListedSecretNamespace).Update(ctx, toUpdateSecret, metav1.UpdateOptions{}) | ||
if !apierrors.IsInvalid(err) { | ||
t.Errorf("unexpected error returned: %v", err) | ||
} | ||
} | ||
} | ||
|
||
func TestNotWhiteListedSecretTypeMutationUpdateDisallowed(t *testing.T) { | ||
ctx := context.Background() | ||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
t.Cleanup(server.TearDownFn) | ||
client, err := kubernetes.NewForConfig(server.ClientConfig) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
testNamespace := framework.CreateNamespaceOrDie(client, "secret-type-update-disallowed", t) | ||
t.Cleanup(func() { framework.DeleteNamespaceOrDie(client, testNamespace, t) }) | ||
|
||
secret := constructSecretWithOldType(testNamespace.Name, "foo") | ||
createdSecret, err := client.CoreV1().Secrets(testNamespace.Name).Create(ctx, secret, metav1.CreateOptions{}) | ||
if err != nil { | ||
t.Errorf("failed to create secret, err = %v", err) | ||
} | ||
|
||
createdSecret.Type = corev1.SecretTypeTLS | ||
_, err = client.CoreV1().Secrets(testNamespace.Name).Update(ctx, createdSecret, metav1.UpdateOptions{}) | ||
if !apierrors.IsInvalid(err) { | ||
t.Errorf("unexpected error returned: %v", err) | ||
} | ||
} | ||
|
||
func constructSecretWithOldType(ns, name string) *corev1.Secret { | ||
return &corev1.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Namespace: ns, | ||
Name: name, | ||
}, | ||
Type: "SecretTypeTLS", | ||
Data: map[string][]byte{"tls.crt": {}, "tls.key": {}}, | ||
} | ||
} |
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.
shouldn't we change the commit title to
UPSTREAM: <drop>: allow type mutation for specific secrets
?