diff --git a/Makefile b/Makefile index 25c9f92d8c..25ebcd6d96 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,14 @@ test-e2e-oidc: GO_TEST_FLAGS += -count 1 test-e2e-oidc: test-unit .PHONY: test-e2e-oidc +# KMS encryption tests +test-e2e-encryption-kms: GO_TEST_PACKAGES :=./test/e2e-encryption-kms/... +test-e2e-encryption-kms: GO_TEST_FLAGS += -v +test-e2e-encryption-kms: GO_TEST_FLAGS += -timeout 4h +test-e2e-encryption-kms: GO_TEST_FLAGS += -p 1 +test-e2e-encryption-kms: test-unit +.PHONY: test-e2e-encryption-kms + # Configure the 'telepresence' target # See vendor/github.com/openshift/build-machinery-go/scripts/run-telepresence.sh for usage and configuration details export TP_DEPLOYMENT_YAML ?=./manifests/07_deployment.yaml diff --git a/go.mod b/go.mod index 9a84314259..3bc49e1edd 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-cmp v0.7.0 - github.com/openshift/api v0.0.0-20251106190826-ebe535b08719 + github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7 github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 - github.com/openshift/library-go v0.0.0-20251107090138-0de9712313a5 + github.com/openshift/library-go v0.0.0-20260303081410-9c30edf843c6 github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 diff --git a/go.sum b/go.sum index 0006cbcca7..32f65855b8 100644 --- a/go.sum +++ b/go.sum @@ -147,14 +147,14 @@ github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/openshift/api v0.0.0-20251106190826-ebe535b08719 h1:KEwYyKaJniwhoyLB75tAMmJn9pMlk0PUlRfrsXYOhwM= -github.com/openshift/api v0.0.0-20251106190826-ebe535b08719/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7 h1:fdvcDJySvjVJctbPbdLPoMiMk+bls34+eq6tWOqdFZg= +github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4= github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM= -github.com/openshift/library-go v0.0.0-20251107090138-0de9712313a5 h1:Gq8jCFgSrilZ2ZHjQleFZWlblikc1aaRZ0hqs+yvrP4= -github.com/openshift/library-go v0.0.0-20251107090138-0de9712313a5/go.mod h1:OlFFws1AO51uzfc48MsStGE4SFMWlMZD0+f5a/zCtKI= +github.com/openshift/library-go v0.0.0-20260303081410-9c30edf843c6 h1:9PoupWybtdTNB7bVBKac/tR5X+3IYydcTIrSyO5QR7E= +github.com/openshift/library-go v0.0.0-20260303081410-9c30edf843c6/go.mod h1:ErDfiIrPHH+menTP/B4LKd0nxFDdvCbTamAc6SWMIh8= github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d h1:Rzx23P63JFNNz5D23ubhC0FCN5rK8CeJhKcq5QKcdyU= github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d/go.mod h1:iVi9Bopa5cLhjG5ie9DoZVVqkH8BGb1FQVTtecOLn4I= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= diff --git a/test/e2e-encryption-kms/encryption_kms_test.go b/test/e2e-encryption-kms/encryption_kms_test.go new file mode 100644 index 0000000000..517e23da24 --- /dev/null +++ b/test/e2e-encryption-kms/encryption_kms_test.go @@ -0,0 +1,82 @@ +package e2e_encryption_kms + +import ( + "context" + "math/rand/v2" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + + configv1 "github.com/openshift/api/config/v1" + operatorencryption "github.com/openshift/cluster-authentication-operator/test/library/encryption" + library "github.com/openshift/library-go/test/library/encryption" + librarykms "github.com/openshift/library-go/test/library/encryption/kms" +) + +// TestKMSEncryptionOnOff tests KMS encryption on/off cycle. +// This test: +// 2. Creates a test OAuth access token (TokenOfLife) +// 3. Enables KMS encryption +// 4. Verifies token is encrypted +// 5. Disables encryption (Identity) +// 6. Verifies token is NOT encrypted +// 7. Re-enables KMS encryption +// 8. Verifies token is encrypted again +// 9. Disables encryption (Identity) again +// 10. Verifies token is NOT encrypted again +func TestKMSEncryptionOnOff(t *testing.T) { + // Deploy the mock KMS plugin for testing. + // NOTE: This manual deployment is only required for KMS v1. In the future, + // the platform will manage the KMS plugins, and this code will no longer be needed. + librarykms.DeployUpstreamMockKMSPlugin(context.Background(), t, library.GetClients(t).Kube, librarykms.WellKnownUpstreamMockKMSPluginNamespace, librarykms.WellKnownUpstreamMockKMSPluginImage) + library.TestEncryptionTurnOnAndOff(t, library.OnOffScenario{ + BasicScenario: library.BasicScenario{ + Namespace: "openshift-config-managed", + LabelSelector: "encryption.apiserver.operator.openshift.io/component" + "=" + "openshift-oauth-apiserver", + EncryptionConfigSecretName: "encryption-config-openshift-oauth-apiserver", + EncryptionConfigSecretNamespace: "openshift-config-managed", + OperatorNamespace: "openshift-authentication-operator", + TargetGRs: operatorencryption.DefaultTargetGRs, + AssertFunc: operatorencryption.AssertTokens, + }, + CreateResourceFunc: func(t testing.TB, _ library.ClientSet, namespace string) runtime.Object { + return operatorencryption.CreateAndStoreTokenOfLife(context.TODO(), t, operatorencryption.GetClients(t)) + }, + AssertResourceEncryptedFunc: operatorencryption.AssertTokenOfLifeEncrypted, + AssertResourceNotEncryptedFunc: operatorencryption.AssertTokenOfLifeNotEncrypted, + ResourceFunc: func(t testing.TB, _ string) runtime.Object { return operatorencryption.TokenOfLife(t) }, + ResourceName: "TokenOfLife", + EncryptionProvider: configv1.EncryptionTypeKMS, + }) +} + +// TestKMSEncryptionProvidersMigration tests migration between KMS and AES encryption providers. +// This test: +// 1. Deploys the mock KMS plugin +// 2. Creates a test OAuth access token (TokenOfLife) +// 3. Randomly picks one AES encryption provider (AESGCM or AESCBC) +// 4. Shuffles the selected AES provider with KMS to create a randomized migration order +// 5. Migrates between the providers in the shuffled order +// 6. Verifies token is correctly encrypted after each migration +func TestKMSEncryptionProvidersMigration(t *testing.T) { + librarykms.DeployUpstreamMockKMSPlugin(context.Background(), t, library.GetClients(t).Kube, librarykms.WellKnownUpstreamMockKMSPluginNamespace, librarykms.WellKnownUpstreamMockKMSPluginImage) + library.TestEncryptionProvidersMigration(t, library.ProvidersMigrationScenario{ + BasicScenario: library.BasicScenario{ + Namespace: "openshift-config-managed", + LabelSelector: "encryption.apiserver.operator.openshift.io/component" + "=" + "openshift-oauth-apiserver", + EncryptionConfigSecretName: "encryption-config-openshift-oauth-apiserver", + EncryptionConfigSecretNamespace: "openshift-config-managed", + OperatorNamespace: "openshift-authentication-operator", + TargetGRs: operatorencryption.DefaultTargetGRs, + AssertFunc: operatorencryption.AssertTokens, + }, + CreateResourceFunc: func(t testing.TB, _ library.ClientSet, namespace string) runtime.Object { + return operatorencryption.CreateAndStoreTokenOfLife(context.TODO(), t, operatorencryption.GetClients(t)) + }, + AssertResourceEncryptedFunc: operatorencryption.AssertTokenOfLifeEncrypted, + AssertResourceNotEncryptedFunc: operatorencryption.AssertTokenOfLifeNotEncrypted, + ResourceFunc: func(t testing.TB, _ string) runtime.Object { return operatorencryption.TokenOfLife(t) }, + ResourceName: "TokenOfLife", + EncryptionProviders: library.ShuffleEncryptionProviders([]configv1.EncryptionType{configv1.EncryptionTypeKMS, library.SupportedStaticEncryptionProviders[rand.IntN(len(library.SupportedStaticEncryptionProviders))]}), + }) +} diff --git a/test/e2e-encryption-kms/main_test.go b/test/e2e-encryption-kms/main_test.go new file mode 100644 index 0000000000..ebdd195578 --- /dev/null +++ b/test/e2e-encryption-kms/main_test.go @@ -0,0 +1,31 @@ +package e2e_encryption_kms + +import ( + "math/rand" + "os" + "reflect" + "testing" + "time" + "unsafe" +) + +func TestMain(m *testing.M) { + randomizeTestOrder(m) + os.Exit(m.Run()) +} + +func randomizeTestOrder(m *testing.M) { + pointerVal := reflect.ValueOf(m) + val := reflect.Indirect(pointerVal) + + testsMember := val.FieldByName("tests") + ptrToTests := unsafe.Pointer(testsMember.UnsafeAddr()) + realPtrToTests := (*[]testing.InternalTest)(ptrToTests) + + tests := *realPtrToTests + + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(tests), func(i, j int) { tests[i], tests[j] = tests[j], tests[i] }) + + *realPtrToTests = tests +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go index 33a09ae16e..bff6155c2f 100644 --- a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go +++ b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go @@ -242,35 +242,41 @@ func ValidCipherSuites() []string { sort.Strings(validCipherSuites) return validCipherSuites } + +// DefaultCiphers returns the default cipher suites for TLS connections. +// +// RECOMMENDATION: Instead of relying on this function directly, consumers should respect +// TLSSecurityProfile settings from one of the OpenShift API configuration resources: +// - For API servers: Use apiserver.config.openshift.io/cluster Spec.TLSSecurityProfile +// - For ingress controllers: Use operator.openshift.io/v1 IngressController Spec.TLSSecurityProfile +// - For kubelet: Use machineconfiguration.openshift.io/v1 KubeletConfig Spec.TLSSecurityProfile +// +// These API resources allow cluster administrators to choose between Old, Intermediate, +// Modern, or Custom TLS profiles. Components should observe these settings. func DefaultCiphers() []uint16 { - // HTTP/2 mandates TLS 1.2 or higher with an AEAD cipher - // suite (GCM, Poly1305) and ephemeral key exchange (ECDHE, DHE) for - // perfect forward secrecy. Servers may provide additional cipher - // suites for backwards compatibility with HTTP/1.1 clients. - // See RFC7540, section 9.2 (Use of TLS Features) and Appendix A - // (TLS 1.2 Cipher Suite Black List). + // Aligned with intermediate profile of the 5.7 version of the Mozilla Server + // Side TLS guidelines found at: https://ssl-config.mozilla.org/guidelines/5.7.json + // + // Latest guidelines: https://ssl-config.mozilla.org/guidelines/latest.json + // + // This profile provides strong security with wide compatibility. + // It requires TLS 1.2+ and uses only AEAD cipher suites (GCM, ChaCha20-Poly1305) + // with ECDHE key exchange for perfect forward secrecy. + // + // All CBC-mode ciphers have been removed due to padding oracle vulnerabilities. + // All RSA key exchange ciphers have been removed due to lack of perfect forward secrecy. + // + // HTTP/2 compliance: All ciphers are compliant with RFC7540, section 9.2. return []uint16{ + // TLS 1.2 cipher suites with ECDHE + AEAD tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // required by http/2 + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // required by HTTP/2 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // forbidden by http/2, not flagged by http2isBadCipher() in go1.8 - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // forbidden by http/2, not flagged by http2isBadCipher() in go1.8 - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // forbidden by http/2 - // the next one is in the intermediate suite, but go1.8 http2isBadCipher() complains when it is included at the recommended index - // because it comes after ciphers forbidden by the http/2 spec - // tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - // tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, // forbidden by http/2, disabled to mitigate SWEET32 attack - // tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // forbidden by http/2, disabled to mitigate SWEET32 attack - tls.TLS_RSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 + + // TLS 1.3 cipher suites (negotiated automatically, not configurable) tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go index 1cb4e55542..c2c8b8368f 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go @@ -188,7 +188,7 @@ func getValidityFromAnnotations(annotations map[string]string) (notBefore time.T return notBefore, notAfter, fmt.Sprintf("bad expiry: %q", notAfterString) } notBeforeString := annotations[CertificateNotBeforeAnnotation] - if len(notAfterString) == 0 { + if len(notBeforeString) == 0 { return notBefore, notAfter, "missing notBefore" } notBefore, err = time.Parse(time.RFC3339, notBeforeString) diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go b/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go index 636a139753..c2641a5343 100644 --- a/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go +++ b/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go @@ -34,6 +34,8 @@ var ( // plus 10 additional minutes for actual migration waitPollTimeout = 69*time.Minute + 10*time.Minute defaultEncryptionMode = string(configv1.EncryptionTypeIdentity) + + SupportedStaticEncryptionProviders = []configv1.EncryptionType{configv1.EncryptionTypeAESGCM, configv1.EncryptionTypeAESCBC} ) type ClientSet struct { diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_configmap.yaml b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_configmap.yaml new file mode 100644 index 0000000000..d9c3e9a0f6 --- /dev/null +++ b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8s-mock-kms-plugin + namespace: {{ .Namespace }} +data: + softhsm-config.json: | + { + "Path": "/usr/lib/softhsm/libsofthsm2.so", + "TokenLabel": "kms-test", + "Pin": "1234" + } + # pre-generated SoftHSM token with AES-256 key. + # run ../k8s-mock-plugin-key-gen/generate.sh to regenerate. + softhsm-tokens.tar.gz.b64: | + H4sIAAAAAAAAA9PTZ6A5MDAwMDQ3NwfRIIBOg9mGpoYmQFVmpsZmQHFzcyMzBgVT2juNgaG0uCSx + SEGBoSg/vwSfOkLyQxTo6SclWSalGaYk6SanJVvqmhmaGemaGxumAlkG5gbmJmZmhpaplCUSUASb + Y8Y77vg3NDExH41/ugAi4z89NS+1KLEkMz+PDDtAEWyGL/4N0ePfzNTElEHBgOq+xQJGePxDAdNA + O2AUDAwgMv8D62+jNEsjS91EQ8tkXVOTlFRdy9REE13TxCQjU2PjRBMjixS9nPzkbGx2EMz/6OW/ + kZGhseFo/qcHoGb85ydlpSZjCSQC8W9oZGqGHv8mZqPlPz2BMhofVh+wQGlGGP0fTQFMgoEZjebI + zi3WLUkthgWaIJq8wtmYw16NO3743+n8IbrBQ654o8P1RS3cTE2rvqxes3f2681QdW3oFk1At9A9 + /CyaEpjj5KHiTGhaYEbC+HDfwb0L9SYjK7oAG7oAO7oAB7oAF7oAD7q1Ajjchx5mMPFEKAPmLwWo + eBK6wcnoVqegC6Sia0lDNVugASpegK6zEF2gCE2ACeYtiNkODEww/8CCGSYuhF0cFtYMrAyjgEaA + yPK/JD87NQ9nBU8AkFz/G5qZmBmMlv/0ACTFP64KngAgFP+mGPFvbmBgPBr/dATgOqqBIdgTyofX + 0rBaXAEHgOrzQtMnAEpCyCkIqs4bKg+tXVh0oeI+aPo9hLuLzFeazTs0z0uff0K8jyJbufak7QIT + v1jacNd/lLvW0Bink9F64W7jVjf1Y/o3JrhXvTgRdIo7qjH6Q94aa7Wis/x+aYIib+56Qc33RTd/ + mrLoLKtVa1bK/Mt6/MqI48T9mDkzbzYfleKNm8ytfO3yvnVnmdZzNTxQ5330z2/9zpt+B+dP+bn+ + UWq1aNfvZvMIhu2djYvqNbukYbXlKBgFo2AUDDkAAM1LQHIAGgAA \ No newline at end of file diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_daemonset.yaml b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_daemonset.yaml new file mode 100644 index 0000000000..1b670fbc1c --- /dev/null +++ b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_daemonset.yaml @@ -0,0 +1,87 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: k8s-mock-kms-plugin + namespace: {{ .Namespace }} +spec: + selector: + matchLabels: + app: k8s-mock-kms-plugin + template: + metadata: + labels: + app: k8s-mock-kms-plugin + spec: + nodeSelector: + node-role.kubernetes.io/control-plane: "" + priorityClassName: system-node-critical + serviceAccountName: k8s-mock-kms-plugin + tolerations: + - operator: Exists + initContainers: + - name: init-softhsm + image: {{ .Image }} + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + command: + - /bin/sh + - -c + args: + - | + set -e + set -x + + # if token exists, skip initialization + if [ $(ls -1 /var/lib/softhsm/tokens 2>/dev/null | wc -l) -ge 1 ]; then + echo "Skipping initialization of softhsm" + exit 0 + fi + + mkdir -p /var/lib/softhsm/tokens + cd /var/lib/softhsm/tokens + + # extract tokens from the configmap + # see ../k8s-mock-plugin-key-gen/README.md for details. + cat /etc/softhsm-tokens.tar.gz.b64 | base64 -d | tar xzf - + volumeMounts: + - mountPath: /var/lib/softhsm/tokens + name: softhsm-tokens + - mountPath: /etc/softhsm-tokens.tar.gz.b64 + name: softhsm-config + subPath: softhsm-tokens.tar.gz.b64 + containers: + - name: kms-plugin + image: {{ .Image }} + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + command: + - /bin/sh + - -c + args: + - | + # remove the socket to prevent "bind: address already in use" + # not sure this is the best way + rm -f /var/run/kmsplugin/kms.sock + exec /usr/local/bin/mock-kms-plugin -listen-addr=unix:///var/run/kmsplugin/kms.sock -config-file-path=/etc/softhsm-config.json + volumeMounts: + - name: socket + mountPath: /var/run/kmsplugin + - name: softhsm-config + mountPath: /etc/softhsm-config.json + subPath: softhsm-config.json + - name: softhsm-tokens + mountPath: /var/lib/softhsm/tokens + volumes: + - name: socket + hostPath: + path: /var/run/kmsplugin + type: DirectoryOrCreate + - name: softhsm-tokens + hostPath: + path: /var/lib/softhsm/tokens + type: DirectoryOrCreate + - name: softhsm-config + configMap: + name: k8s-mock-kms-plugin diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_namespace.yaml b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_namespace.yaml new file mode 100644 index 0000000000..4141b1d701 --- /dev/null +++ b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Namespace }} + labels: + pod-security.kubernetes.io/enforce: privileged + pod-security.kubernetes.io/audit: privileged + pod-security.kubernetes.io/warn: privileged \ No newline at end of file diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_rolebinding.yaml b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_rolebinding.yaml new file mode 100644 index 0000000000..9c42e8b826 --- /dev/null +++ b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_rolebinding.yaml @@ -0,0 +1,16 @@ +# RoleBinding to grant the k8s-mock-kms-plugin ServiceAccount access to the +# privileged SCC. This is required because the KMS plugin needs privileged +# access to create the Unix socket on the host filesystem. +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: k8s-mock-kms-plugin + namespace: {{ .Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:privileged +subjects: + - kind: ServiceAccount + name: k8s-mock-kms-plugin + namespace: {{ .Namespace }} diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_serviceaccount.yaml b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_serviceaccount.yaml new file mode 100644 index 0000000000..5eebaf6ad3 --- /dev/null +++ b/vendor/github.com/openshift/library-go/test/library/encryption/kms/assets/k8s_mock_kms_plugin_serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8s-mock-kms-plugin + namespace: {{ .Namespace }} diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/kms/k8s_mock_kms_plugin_deployer.go b/vendor/github.com/openshift/library-go/test/library/encryption/kms/k8s_mock_kms_plugin_deployer.go new file mode 100644 index 0000000000..07d0176535 --- /dev/null +++ b/vendor/github.com/openshift/library-go/test/library/encryption/kms/k8s_mock_kms_plugin_deployer.go @@ -0,0 +1,152 @@ +package kms + +import ( + "bytes" + "context" + "embed" + "path/filepath" + "testing" + "text/template" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/utils/clock" + + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/resource/resourceapply" + "github.com/openshift/library-go/pkg/operator/resource/resourceread" +) + +//go:embed assets +var assetsFS embed.FS + +const ( + // WellKnownUpstreamMockKMSPluginNamespace is the default namespace where the KMS plugin runs. + WellKnownUpstreamMockKMSPluginNamespace = "k8s-mock-plugin" + + // WellKnownUpstreamMockKMSPluginImage is the pre-built mock KMS plugin image. + WellKnownUpstreamMockKMSPluginImage = "quay.io/openshifttest/mock-kms-plugin@sha256:998e1d48eba257f589ab86c30abd5043f662213e9aeff253e1c308301879d48a" + + // defaultPollTimeout the default poll timeout used by the deployer + defaultPollTimeout = 2 * time.Minute +) + +var manifestFilesToApplyDirectly = []string{ + "k8s_mock_kms_plugin_namespace.yaml", + "k8s_mock_kms_plugin_serviceaccount.yaml", + "k8s_mock_kms_plugin_rolebinding.yaml", + "k8s_mock_kms_plugin_configmap.yaml", +} + +var daemonSetManifestFile = "k8s_mock_kms_plugin_daemonset.yaml" + +// yamlTemplateData holds the template variables for YAML manifests. +// Fields must be exported (uppercase) for Go templates to access them. +type yamlTemplateData struct { + Namespace string + Image string +} + +// DeployUpstreamMockKMSPlugin deploys the upstream mock KMS v2 plugin using embedded YAML assets. +func DeployUpstreamMockKMSPlugin(ctx context.Context, t testing.TB, kubeClient kubernetes.Interface, namespace, image string) { + t.Helper() + + t.Logf("Deploying upstream mock KMS v2 plugin in namespace %q using image %s", namespace, image) + daemonSetName, err := applyUpstreamMockKMSPluginManifests(ctx, t, kubeClient, namespace, image) + if err != nil { + t.Fatalf("Failed to apply manifests: %v", err) + } + if err := waitForDaemonSetReady(ctx, t, kubeClient, namespace, daemonSetName); err != nil { + t.Fatalf("DaemonSet not ready: %v", err) + } + t.Logf("Upstream mock KMS v2 plugin deployed successfully!") +} + +// applyUpstreamMockKMSPluginManifests applies all the KMS plugin manifests. +// Returns the DaemonSet name on success. +func applyUpstreamMockKMSPluginManifests(ctx context.Context, t testing.TB, kubeClient kubernetes.Interface, namespace, image string) (string, error) { + t.Helper() + + data := yamlTemplateData{ + Namespace: namespace, + Image: image, + } + + recorder := events.NewInMemoryRecorder("k8s-mock-kms-plugin-deployer", clock.RealClock{}) + assetFunc := wrapAssetWithTemplateDataFunc(data) + + clientHolder := resourceapply.NewKubeClientHolder(kubeClient) + results := resourceapply.ApplyDirectly(ctx, clientHolder, recorder, resourceapply.NewResourceCache(), assetFunc, manifestFilesToApplyDirectly...) + + for _, result := range results { + if result.Error != nil { + return "", result.Error + } + t.Logf("Applied %s (changed=%v)", result.File, result.Changed) + } + + rawDaemonSet, err := assetFunc(daemonSetManifestFile) + if err != nil { + return "", err + } + + daemonSet := resourceread.ReadDaemonSetV1OrDie(rawDaemonSet) + _, daemonSetChanged, err := resourceapply.ApplyDaemonSet(ctx, kubeClient.AppsV1(), recorder, daemonSet, -1) + if err != nil { + return "", err + } + t.Logf("Applied DaemonSet %s/%s (changed=%v)", namespace, daemonSet.Name, daemonSetChanged) + + return daemonSet.Name, nil +} + +// waitForDaemonSetReady waits for the KMS plugin DaemonSet to be ready. +func waitForDaemonSetReady(ctx context.Context, t testing.TB, kubeClient kubernetes.Interface, namespace, daemonSetName string) error { + t.Helper() + + t.Logf("Waiting for DaemonSet %s/%s to be ready...", namespace, daemonSetName) + + return wait.PollUntilContextTimeout(ctx, time.Second, defaultPollTimeout, true, func(ctx context.Context) (bool, error) { + ds, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, daemonSetName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + t.Logf("DaemonSet %s/%s status: desired=%d, ready=%d, available=%d", + namespace, daemonSetName, ds.Status.DesiredNumberScheduled, ds.Status.NumberReady, ds.Status.NumberAvailable) + + // for simplicity just ensure at least one pod is scheduled before checking readiness + if ds.Status.DesiredNumberScheduled == 0 { + return false, nil + } + return ds.Status.NumberReady == ds.Status.DesiredNumberScheduled, nil + }) +} + +// wrapAssetWithTemplateDataFunc returns an AssetFunc that templates the YAML with the given data. +func wrapAssetWithTemplateDataFunc(data yamlTemplateData) resourceapply.AssetFunc { + return func(name string) ([]byte, error) { + content, err := assetsFS.ReadFile(filepath.Join("assets", name)) + if err != nil { + return nil, err + } + + tmpl, err := template.New(name).Parse(string(content)) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return nil, err + } + + return buf.Bytes(), nil + } +} diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go b/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go index 99d60bfe49..ea510da8aa 100644 --- a/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go +++ b/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go @@ -2,6 +2,7 @@ package encryption import ( "fmt" + mathrand "math/rand/v2" "strings" "testing" @@ -50,12 +51,21 @@ func TestEncryptionTypeAESGCM(t *testing.T, scenario BasicScenario) { AssertEncryptionConfig(e, clientSet, scenario.EncryptionConfigSecretName, scenario.EncryptionConfigSecretNamespace, scenario.TargetGRs) } +func TestEncryptionTypeKMS(t *testing.T, scenario BasicScenario) { + e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace)) + clientSet := SetAndWaitForEncryptionType(e, configv1.EncryptionTypeKMS, scenario.TargetGRs, scenario.Namespace, scenario.LabelSelector) + scenario.AssertFunc(e, clientSet, configv1.EncryptionTypeKMS, scenario.Namespace, scenario.LabelSelector) + AssertEncryptionConfig(e, clientSet, scenario.EncryptionConfigSecretName, scenario.EncryptionConfigSecretNamespace, scenario.TargetGRs) +} + func TestEncryptionType(t *testing.T, scenario BasicScenario, provider configv1.EncryptionType) { switch provider { case configv1.EncryptionTypeAESCBC: TestEncryptionTypeAESCBC(t, scenario) case configv1.EncryptionTypeAESGCM: TestEncryptionTypeAESGCM(t, scenario) + case configv1.EncryptionTypeKMS: + TestEncryptionTypeKMS(t, scenario) case configv1.EncryptionTypeIdentity, "": TestEncryptionTypeIdentity(t, scenario) default: @@ -73,11 +83,13 @@ type OnOffScenario struct { EncryptionProvider configv1.EncryptionType } +type testStep struct { + name string + testFunc func(*testing.T) +} + func TestEncryptionTurnOnAndOff(t *testing.T, scenario OnOffScenario) { - scenarios := []struct { - name string - testFunc func(*testing.T) - }{ + scenarios := []testStep{ {name: fmt.Sprintf("CreateAndStore%s", scenario.ResourceName), testFunc: func(t *testing.T) { e := NewE(t) scenario.CreateResourceFunc(e, GetClients(e), scenario.Namespace) @@ -114,6 +126,91 @@ func TestEncryptionTurnOnAndOff(t *testing.T, scenario OnOffScenario) { } } +// ProvidersMigrationScenario defines a test scenario for migrating encryption +// between multiple providers. +// +// See TestEncryptionProvidersMigration for more details. +type ProvidersMigrationScenario struct { + BasicScenario + CreateResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) runtime.Object + AssertResourceEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object) + AssertResourceNotEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object) + ResourceFunc func(t testing.TB, namespace string) runtime.Object + ResourceName string + // EncryptionProviders is the list of encryption providers to migrate through. + // The test will migrate through each provider in order, then always end by + // switching to identity (off) to verify the resource is re-written unencrypted. + EncryptionProviders []configv1.EncryptionType +} + +// ShuffleEncryptionProviders returns a new slice with the providers in random order, +// leaving the original slice unchanged. Use this to test different migration orderings. +func ShuffleEncryptionProviders(providers []configv1.EncryptionType) []configv1.EncryptionType { + shuffled := make([]configv1.EncryptionType, len(providers)) + copy(shuffled, providers) + mathrand.Shuffle(len(shuffled), func(i, j int) { + shuffled[i], shuffled[j] = shuffled[j], shuffled[i] + }) + return shuffled +} + +// TestEncryptionProvidersMigration tests migration between given encryption providers. +// It creates a resource, migrates through each provider, +// verifies the resource is encrypted after each migration, and finally +// switches to identity (off). +func TestEncryptionProvidersMigration(t *testing.T, scenario ProvidersMigrationScenario) { + if len(scenario.EncryptionProviders) < 2 { + t.Fatalf("ProvidersMigrationScenario requires at least 2 encryption providers, got %d", len(scenario.EncryptionProviders)) + } + + for _, provider := range scenario.EncryptionProviders { + if provider == configv1.EncryptionTypeIdentity || provider == "" { + t.Fatalf("Unsupported encryption provider %q passed", provider) + } + } + + // step 1: create the resource + scenarios := []testStep{ + {name: fmt.Sprintf("CreateAndStore%s", scenario.ResourceName), testFunc: func(t *testing.T) { + e := NewE(t) + scenario.CreateResourceFunc(e, GetClients(e), scenario.Namespace) + }}, + } + + // step 2: migrate through each provider in sequence + for i, provider := range scenario.EncryptionProviders { + prefix := "EncryptWith" + if i > 0 { + prefix = "MigrateTo" + } + scenarios = append(scenarios, + testStep{name: fmt.Sprintf("%s%s", prefix, strings.ToUpper(string(provider))), testFunc: func(t *testing.T) { + TestEncryptionType(t, scenario.BasicScenario, provider) + }}, + testStep{name: fmt.Sprintf("Assert%sEncrypted", scenario.ResourceName), testFunc: func(t *testing.T) { + e := NewE(t) + scenario.AssertResourceEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace)) + }}, + ) + } + + // step 3: switch to identity (off) to verify the resource is re-written unencrypted + scenarios = append(scenarios, testStep{name: fmt.Sprintf("OffIdentityAndAssert%sNotEncrypted", scenario.ResourceName), testFunc: func(t *testing.T) { + TestEncryptionTypeIdentity(t, scenario.BasicScenario) + e := NewE(t) + scenario.AssertResourceNotEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace)) + }}) + + // run scenarios + for _, testScenario := range scenarios { + t.Run(testScenario.name, testScenario.testFunc) + if t.Failed() { + t.Errorf("stopping the test as %q scenario failed", testScenario.name) + return + } + } +} + type RotationScenario struct { BasicScenario CreateResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) runtime.Object diff --git a/vendor/modules.txt b/vendor/modules.txt index bfcb979b72..fc6272b71e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -173,7 +173,7 @@ github.com/modern-go/reflect2 # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 ## explicit github.com/munnerz/goautoneg -# github.com/openshift/api v0.0.0-20251106190826-ebe535b08719 +# github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7 ## explicit; go 1.24.0 github.com/openshift/api github.com/openshift/api/annotations @@ -319,7 +319,7 @@ github.com/openshift/client-go/user/applyconfigurations/internal github.com/openshift/client-go/user/applyconfigurations/user/v1 github.com/openshift/client-go/user/clientset/versioned/scheme github.com/openshift/client-go/user/clientset/versioned/typed/user/v1 -# github.com/openshift/library-go v0.0.0-20251107090138-0de9712313a5 +# github.com/openshift/library-go v0.0.0-20260303081410-9c30edf843c6 ## explicit; go 1.24.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/apps/deployment @@ -392,6 +392,7 @@ github.com/openshift/library-go/pkg/route/routeapihelpers github.com/openshift/library-go/pkg/serviceability github.com/openshift/library-go/test/library github.com/openshift/library-go/test/library/encryption +github.com/openshift/library-go/test/library/encryption/kms # github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d ## explicit; go 1.22.0 github.com/openshift/multi-operator-manager/pkg/flagtypes