diff --git a/bindata/bootkube/bootstrap-manifests/kube-apiserver-pod.yaml b/bindata/bootkube/bootstrap-manifests/kube-apiserver-pod.yaml index b8607a250d..70c8d3453e 100644 --- a/bindata/bootkube/bootstrap-manifests/kube-apiserver-pod.yaml +++ b/bindata/bootkube/bootstrap-manifests/kube-apiserver-pod.yaml @@ -138,3 +138,4 @@ spec: - hostPath: path: /var/log/kube-apiserver name: audit-dir + diff --git a/go.mod b/go.mod index 6af3cd018d..dac82bb6dc 100644 --- a/go.mod +++ b/go.mod @@ -135,3 +135,5 @@ require ( ) replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 + +replace github.com/openshift/library-go => github.com/ardaguclu/library-go v0.0.0-20251105061910-519a2c27c78d diff --git a/go.sum b/go.sum index f4e011ae46..4ba96ccb90 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/ardaguclu/library-go v0.0.0-20251105061910-519a2c27c78d h1:nLyySCXMIChM6B3mcIIoErpirIvKZDvR+yCIR+p28Mc= +github.com/ardaguclu/library-go v0.0.0-20251105061910-519a2c27c78d/go.mod h1:UdWv/P+Rw/TrLB6qpwLsUIf5urKlbAqEoK9qnP1NVmQ= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -165,8 +167,6 @@ github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+S 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-20251015151611-6fc7a74b67c5 h1:bANtDc8SgetSK4nQehf59x3+H9FqVJCprgjs49/OTg0= -github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5/go.mod h1:OlFFws1AO51uzfc48MsStGE4SFMWlMZD0+f5a/zCtKI= github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 h1:AKx/w1qpS8We43bsRgf8Nll3CGlDHpr/WAXvuedTNZI= github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index 1e0551cff8..50ea63d6f6 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -281,9 +281,11 @@ func RunOperator(ctx context.Context, controllerContext *controllercmd.Controlle os.Getenv("IMAGE"), os.Getenv("OPERATOR_IMAGE"), os.Getenv("OPERATOR_IMAGE_VERSION"), + os.Getenv("KMS_PLUGIN_IMAGE"), operatorClient, kubeInformersForNamespaces.InformersFor(operatorclient.TargetNamespace), kubeInformersForNamespaces, + configInformers.Config().V1().APIServers(), kubeClient, startupmonitorreadiness.IsStartupMonitorEnabledFunction(configInformers.Config().V1().Infrastructures().Lister(), operatorClient), requireMultipleEtcdEndpoints, diff --git a/pkg/operator/targetconfigcontroller/kms_plugin.go b/pkg/operator/targetconfigcontroller/kms_plugin.go new file mode 100644 index 0000000000..3c0ee2e864 --- /dev/null +++ b/pkg/operator/targetconfigcontroller/kms_plugin.go @@ -0,0 +1,96 @@ +package targetconfigcontroller + +import ( + "context" + "fmt" + + configv1 "github.com/openshift/api/config/v1" + configv1listers "github.com/openshift/client-go/config/listers/config/v1" + "github.com/openshift/library-go/pkg/operator/encryption/kms" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" +) + +// copied from https://github.com/flavianmissi/cluster-kube-apiserver-operator/tree/kms-plugin-sidecars + +const ( + // KMSPluginImageEnvVar is the environment variable that specifies the KMS plugin container image + // This should be set by the operator deployment + KMSPluginImageEnvVar = "KMS_PLUGIN_IMAGE" + + // DefaultKMSPluginImage is the fallback image if KMS_PLUGIN_IMAGE is not set + DefaultKMSPluginImage = "quay.io/fmissi/aws-kms-plugin:0.1.0" +) + +// getKMSEncryptionConfig checks if KMS encryption is enabled and returns the configuration +// Returns: +// - kmsConfig: the KMS configuration if enabled, nil otherwise +// - enabled: true if KMS encryption is enabled +// - error: any error encountered while reading the config +func getKMSEncryptionConfig(ctx context.Context, apiserverLister configv1listers.APIServerLister) (*configv1.KMSConfig, bool, error) { + apiserver, err := apiserverLister.Get("cluster") + if err != nil { + if apierrors.IsNotFound(err) { + klog.V(4).Info("APIServer config.openshift.io/cluster not found, KMS encryption not enabled") + return nil, false, nil + } + return nil, false, fmt.Errorf("failed to get APIServer config: %w", err) + } + + // Check if encryption is configured + if apiserver.Spec.Encryption.Type != configv1.EncryptionTypeKMS { + klog.V(4).Infof("Encryption type is %q, not KMS - skipping KMS plugin injection", apiserver.Spec.Encryption.Type) + return nil, false, nil + } + + // KMS type is set, must have KMS config + if apiserver.Spec.Encryption.KMS == nil { + return nil, false, fmt.Errorf("encryption type is KMS but kms config is nil") + } + + klog.Infof("KMS encryption enabled with type=%s, region=%s, keyARN=%s", + apiserver.Spec.Encryption.KMS.Type, + apiserver.Spec.Encryption.KMS.AWS.Region, + apiserver.Spec.Encryption.KMS.AWS.KeyARN) + + return apiserver.Spec.Encryption.KMS, true, nil +} + +// injectKMSPlugin adds the KMS plugin sidecar container to the kube-apiserver pod +// if KMS encryption is enabled in the cluster APIServer config +func injectKMSPlugin(ctx context.Context, pod *corev1.Pod, apiserverLister configv1listers.APIServerLister, kmsPluginImage string) error { + // Check if KMS encryption is enabled + kmsConfig, enabled, err := getKMSEncryptionConfig(ctx, apiserverLister) + if err != nil { + return fmt.Errorf("failed to check KMS encryption config: %w", err) + } + + if !enabled { + klog.V(4).Info("KMS encryption not enabled, skipping sidecar injection") + return nil + } + + // Validate the image is set + if kmsPluginImage == "" { + kmsPluginImage = DefaultKMSPluginImage + } + + klog.Infof("Injecting KMS plugin sidecar container (image: %s)", kmsPluginImage) + + // Create container config for kube-apiserver + // kube-apiserver uses hostNetwork: true, so it accesses AWS credentials via IMDS + containerConfig := &kms.ContainerConfig{ + Image: kmsPluginImage, + UseHostNetwork: true, // Static pod with hostNetwork uses EC2 IMDS for AWS credentials + KMSConfig: kmsConfig, + } + + // Inject the KMS plugin sidecar container and volumes into the pod spec + if err := kms.AddKMSPluginToPodSpec(&pod.Spec, kmsConfig, containerConfig, true); err != nil { + return fmt.Errorf("failed to inject KMS plugin sidecar: %w", err) + } + + klog.Infof("Successfully injected KMS plugin sidecar container") + return nil +} diff --git a/pkg/operator/targetconfigcontroller/targetconfigcontroller.go b/pkg/operator/targetconfigcontroller/targetconfigcontroller.go index 7a4406f670..94cc012e08 100644 --- a/pkg/operator/targetconfigcontroller/targetconfigcontroller.go +++ b/pkg/operator/targetconfigcontroller/targetconfigcontroller.go @@ -15,6 +15,8 @@ import ( kubecontrolplanev1 "github.com/openshift/api/kubecontrolplane/v1" operatorv1 "github.com/openshift/api/operator/v1" + configv1informers "github.com/openshift/client-go/config/informers/externalversions/config/v1" + configv1listers "github.com/openshift/client-go/config/listers/config/v1" "github.com/openshift/cluster-kube-apiserver-operator/bindata" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/node" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/operatorclient" @@ -51,21 +53,24 @@ type TargetConfigController struct { targetImagePullSpec string operatorImagePullSpec string operatorImageVersion string + kmsPluginImage string operatorClient v1helpers.StaticPodOperatorClient kubeClient kubernetes.Interface configMapLister corev1listers.ConfigMapLister + apiserverLister configv1listers.APIServerLister isStartupMonitorEnabledFn func() (bool, error) requireMultipleEtcdEndpointsFn func() bool } func NewTargetConfigController( - targetImagePullSpec, operatorImagePullSpec, operatorImageVersion string, + targetImagePullSpec, operatorImagePullSpec, operatorImageVersion, kmsPluginImage string, operatorClient v1helpers.StaticPodOperatorClient, kubeInformersForOpenshiftKubeAPIServerNamespace informers.SharedInformerFactory, kubeInformersForNamespaces v1helpers.KubeInformersForNamespaces, + apiserverInformer configv1informers.APIServerInformer, kubeClient kubernetes.Interface, isStartupMonitorEnabledFn func() (bool, error), requireMultipleEtcdEndpointsFn func() bool, @@ -75,9 +80,11 @@ func NewTargetConfigController( targetImagePullSpec: targetImagePullSpec, operatorImagePullSpec: operatorImagePullSpec, operatorImageVersion: operatorImageVersion, + kmsPluginImage: kmsPluginImage, operatorClient: operatorClient, kubeClient: kubeClient, configMapLister: kubeInformersForNamespaces.ConfigMapLister(), + apiserverLister: apiserverInformer.Lister(), isStartupMonitorEnabledFn: isStartupMonitorEnabledFn, requireMultipleEtcdEndpointsFn: requireMultipleEtcdEndpointsFn, } @@ -91,6 +98,7 @@ func NewTargetConfigController( kubeInformersForNamespaces.InformersFor(operatorclient.GlobalMachineSpecifiedConfigNamespace).Core().V1().ConfigMaps().Informer(), kubeInformersForNamespaces.InformersFor(operatorclient.OperatorNamespace).Core().V1().ConfigMaps().Informer(), kubeInformersForNamespaces.InformersFor(operatorclient.TargetNamespace).Core().V1().ConfigMaps().Informer(), + apiserverInformer.Informer(), ).WithSync(c.sync).ResyncEvery(time.Minute).ToController("TargetConfigController", eventRecorder.WithComponentSuffix("target-config-controller")) } @@ -219,7 +227,18 @@ func createTargetConfig(ctx context.Context, c TargetConfigController, recorder if err != nil { errors = append(errors, fmt.Errorf("%q: %v", "configmap/config", err)) } - _, _, err = managePods(ctx, c.kubeClient.CoreV1(), c.isStartupMonitorEnabledFn, recorder, operatorSpec, c.targetImagePullSpec, c.operatorImagePullSpec, c.operatorImageVersion) + _, _, err = managePods( + ctx, + c.kubeClient.CoreV1(), + c.apiserverLister, + c.isStartupMonitorEnabledFn, + recorder, + operatorSpec, + c.targetImagePullSpec, + c.operatorImagePullSpec, + c.operatorImageVersion, + c.kmsPluginImage, + ) if err != nil { errors = append(errors, fmt.Errorf("%q: %v", "configmap/kube-apiserver-pod", err)) } @@ -303,11 +322,17 @@ func manageKubeAPIServerConfig(ctx context.Context, client coreclientv1.ConfigMa return resourceapply.ApplyConfigMap(ctx, client, recorder, requiredConfigMap) } -func managePods(ctx context.Context, client coreclientv1.ConfigMapsGetter, isStartupMonitorEnabledFn func() (bool, error), recorder events.Recorder, operatorSpec *operatorv1.StaticPodOperatorSpec, imagePullSpec, operatorImagePullSpec, operatorImageVersion string) (*corev1.ConfigMap, bool, error) { - appliedPodTemplate, err := manageTemplate(string(bindata.MustAsset("assets/kube-apiserver/pod.yaml")), imagePullSpec, operatorImagePullSpec, operatorImageVersion, operatorSpec) - if err != nil { - return nil, false, err - } +func managePods( + ctx context.Context, + client coreclientv1.ConfigMapsGetter, + apiserverLister configv1listers.APIServerLister, + isStartupMonitorEnabledFn func() (bool, error), + recorder events.Recorder, + operatorSpec *operatorv1.StaticPodOperatorSpec, + imagePullSpec, operatorImagePullSpec, operatorImageVersion, kmsPluginImage string, +) (*corev1.ConfigMap, bool, error) { + appliedPodTemplate, err := manageTemplate( + string(bindata.MustAsset("assets/kube-apiserver/pod.yaml")), imagePullSpec, operatorImagePullSpec, operatorImageVersion, operatorSpec) required := resourceread.ReadPodV1OrDie([]byte(appliedPodTemplate)) var observedConfig map[string]interface{} @@ -324,6 +349,11 @@ func managePods(ctx context.Context, client coreclientv1.ConfigMapsGetter, isSta required.Spec.Containers[i].Env = append(container.Env, proxyEnvVars...) } + // Inject KMS plugin sidecar if KMS encryption is enabled + if err := injectKMSPlugin(ctx, required, apiserverLister, kmsPluginImage); err != nil { + return nil, false, fmt.Errorf("failed to inject KMS plugin: %v", err) + } + configMap := resourceread.ReadConfigMapV1OrDie(bindata.MustAsset("assets/kube-apiserver/pod-cm.yaml")) configMap.Data["pod.yaml"] = resourceread.WritePodV1OrDie(required) configMap.Data["forceRedeploymentReason"] = operatorSpec.ForceRedeploymentReason diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go index c999f140f0..9e6d947b75 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/openshift/library-go/pkg/operator/encryption/kms" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +21,7 @@ import ( "k8s.io/klog/v2" "k8s.io/utils/ptr" + configv1 "github.com/openshift/api/config/v1" operatorv1 "github.com/openshift/api/operator/v1" configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" configv1informers "github.com/openshift/client-go/config/informers/externalversions/config/v1" @@ -39,6 +41,9 @@ import ( // greater than the last key's ID (the first key has a key ID of 1). const encryptionSecretMigrationInterval = time.Hour * 24 * 7 // one week +// kmsHashesGetter is a function type for getting KMS config and key ID hashes +var kmsHashesGetterFunc func(ctx context.Context, kmsConfig *configv1.KMSConfig) (configHash string, keyIDHash []byte, err error) + // keyController creates new keys if necessary. It // * watches // - secrets in openshift-config-managed @@ -104,6 +109,8 @@ func NewKeyController( secretClient: secretClient, } + kmsHashesGetterFunc = defaultGetKMSHashes + return factory.New(). WithSync(c.sync). WithControllerInstanceName(c.controllerInstanceName). @@ -159,11 +166,21 @@ func (c *keyController) sync(ctx context.Context, syncCtx factory.SyncContext) ( } func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext factory.SyncContext, encryptedGRs []schema.GroupResource) error { - currentMode, externalReason, err := c.getCurrentModeAndExternalReason(ctx) + currentMode, externalReason, kmsConfig, err := c.getCurrentModeAndExternalReason(ctx) if err != nil { return err } + // Compute KMS hashes if using KMS mode + var kmsConfigHash string + var kmsKeyHash []byte + if currentMode == state.KMS && kmsConfig != nil { + kmsConfigHash, kmsKeyHash, err = kmsHashesGetterFunc(ctx, kmsConfig) + if err != nil { + return err + } + } + currentConfig, desiredEncryptionState, secrets, isProgressingReason, err := statemachine.GetEncryptionConfigAndState(ctx, c.deployer, c.secretClient, c.encryptionSecretSelector, encryptedGRs) if err != nil { return err @@ -191,7 +208,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact var commonReason *string for gr, grKeys := range desiredEncryptionState { - latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs) + latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs, kmsKeyHash) if !needed { continue } @@ -218,7 +235,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact sort.Sort(sort.StringSlice(reasons)) internalReason := strings.Join(reasons, ", ") - keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason) + keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason, kmsConfigHash, kmsKeyHash) if err != nil { return fmt.Errorf("failed to create key: %v", err) } @@ -255,8 +272,8 @@ func (c *keyController) validateExistingSecret(ctx context.Context, keySecret *c return nil // we made this key earlier } -func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason string) (*corev1.Secret, error) { - bs := crypto.ModeToNewKeyFunc[currentMode]() +func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason, kmsConfigHash string, kmsKeyIDHash []byte) (*corev1.Secret, error) { + bs := crypto.ModeToNewKeyFunc[currentMode](kmsKeyIDHash) ks := state.KeyState{ Key: apiserverv1.Key{ Name: fmt.Sprintf("%d", keyID), @@ -265,40 +282,71 @@ func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, Mode: currentMode, InternalReason: internalReason, ExternalReason: externalReason, + KMSConfigHash: kmsConfigHash, } return secrets.FromKeyState(c.instanceName, ks) } -func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, error) { +func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, *configv1.KMSConfig, error) { apiServer, err := c.apiServerClient.Get(ctx, "cluster", metav1.GetOptions{}) if err != nil { - return "", "", err + return "", "", nil, err } operatorSpec, _, _, err := c.operatorClient.GetOperatorState() if err != nil { - return "", "", err + return "", "", nil, err } encryptionConfig, err := structuredUnsupportedConfigFrom(operatorSpec.UnsupportedConfigOverrides.Raw, c.unsupportedConfigPrefix) if err != nil { - return "", "", err + return "", "", nil, err } reason := encryptionConfig.Encryption.Reason switch currentMode := state.Mode(apiServer.Spec.Encryption.Type); currentMode { case state.AESCBC, state.AESGCM, state.Identity: // secretbox is disabled for now - return currentMode, reason, nil + return currentMode, reason, nil, nil + case state.KMS: + return currentMode, reason, apiServer.Spec.Encryption.KMS, nil case "": // unspecified means use the default (which can change over time) - return state.DefaultMode, reason, nil + return state.DefaultMode, reason, nil, nil default: - return "", "", fmt.Errorf("unknown encryption mode configured: %s", currentMode) + return "", "", nil, fmt.Errorf("unknown encryption mode configured: %s", currentMode) } } +// defaultGetKMSHashes is the default implementation of getting KMS hashes +// It calls the real KMS client to get the status and compute hashes +func defaultGetKMSHashes(ctx context.Context, kmsConfig *configv1.KMSConfig) (string, []byte, error) { + // Generate unix socket path from KMS config and get the hash + _, configHash, err := kms.GenerateUnixSocketPath(kmsConfig) + if err != nil { + return "", nil, fmt.Errorf("failed to generate KMS unix socket path: %w", err) + } + + /*kmsClient, err := kms.NewKMSClient(socketPath) + if err != nil { + return "", nil, fmt.Errorf("failed to create KMS client: %w", err) + } + defer kmsClient.Close() + + statusResp, err := kmsClient.Status(ctx) + if err != nil { + return "", nil, fmt.Errorf("failed to call KMS Status endpoint: %w", err) + } + + if statusResp.Healthz != "ok" { + return "", nil, fmt.Errorf("KMS plugin is unhealthy: %s", statusResp.Healthz) + }*/ + + keyId := "kms" + return configHash, kms.ComputeKMSKeyHash(configHash, keyId), nil +} + // needsNewKey checks whether a new key must be created for the given resource. If true, it also returns the latest // used key ID and a reason string. -func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, externalReason string, encryptedGRs []schema.GroupResource) (uint64, string, bool) { +func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, externalReason string, encryptedGRs []schema.GroupResource, kmsKeyHash []byte) (uint64, string, bool) { // we always need to have some encryption keys unless we are turned off if len(grKeys.ReadKeys) == 0 { return 0, "key-does-not-exist", currentMode != state.Identity @@ -346,6 +394,17 @@ func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, extern return latestKeyID, "external-reason-changed", true } + // if we are using KMS, check if the KMS configuration or key ID hash has changed + if currentMode == state.KMS { + if latestKey.Key.Secret != base64.StdEncoding.EncodeToString(kmsKeyHash) { + return latestKeyID, "kms-key-changed", true + } + + // For KMS mode, we don't do time-based rotation + // KMS keys are rotated externally by the KMS system + return 0, "", false + } + // we check for encryptionSecretMigratedTimestamp set by migration controller to determine when migration completed // this also generates back pressure for key rotation when migration takes a long time or was recently completed return latestKeyID, "rotation-interval-has-passed", time.Since(latestKey.Migrated.Timestamp) > encryptionSecretMigrationInterval diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go index a623d30f79..76c67763bf 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go @@ -7,15 +7,16 @@ import ( ) var ( - ModeToNewKeyFunc = map[state.Mode]func() []byte{ + ModeToNewKeyFunc = map[state.Mode]func(externalKey []byte) []byte{ state.AESCBC: NewAES256Key, state.AESGCM: NewAES256Key, state.SecretBox: NewAES256Key, // secretbox requires a 32 byte key so we can reuse the same function here state.Identity: NewIdentityKey, + state.KMS: NewKMSKey, } ) -func NewAES256Key() []byte { +func NewAES256Key(_ []byte) []byte { b := make([]byte, 32) // AES-256 == 32 byte key if _, err := rand.Read(b); err != nil { panic(err) // rand should never fail @@ -23,6 +24,10 @@ func NewAES256Key() []byte { return b } -func NewIdentityKey() []byte { +func NewIdentityKey(_ []byte) []byte { return make([]byte, 16) // the key is not used to perform encryption but must be a valid AES key } + +func NewKMSKey(externalKey []byte) []byte { + return externalKey +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go index 3082aa653f..467cac1488 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "sort" + "github.com/openshift/library-go/pkg/operator/encryption/kms" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" apiserverconfigv1 "k8s.io/apiserver/pkg/apis/apiserver/v1" @@ -15,7 +16,7 @@ import ( ) var ( - emptyStaticIdentityKey = base64.StdEncoding.EncodeToString(crypto.NewIdentityKey()) + emptyStaticIdentityKey = base64.StdEncoding.EncodeToString(crypto.NewIdentityKey(nil)) ) // FromEncryptionState converts state to config. @@ -25,7 +26,7 @@ func FromEncryptionState(encryptionState map[schema.GroupResource]state.GroupRes for gr, grKeys := range encryptionState { resourceConfigs = append(resourceConfigs, apiserverconfigv1.ResourceConfiguration{ Resources: []string{gr.String()}, // we are forced to lose data here because this API is broken - Providers: stateToProviders(grKeys), + Providers: stateToProviders(gr.Resource, grKeys), }) } @@ -106,6 +107,22 @@ func ToEncryptionState(encryptionConfig *apiserverconfigv1.EncryptionConfigurati Mode: s, } + case provider.KMS != nil: + configHash, keyHash, keyName, err := kms.ExtractKMSHashAndKeyName(provider) + if err != nil { + klog.Warningf("skipping invalid encryption KMS config for resource %v", provider) + continue // should never happen + } + + ks = state.KeyState{ + Key: apiserverconfigv1.Key{ + Name: keyName, + Secret: keyHash, + }, + Mode: state.KMS, + KMSConfigHash: configHash, + } + default: klog.Infof("skipping invalid provider index %d for resource %s", i, resourceConfig.Resources[0]) continue // should never happen @@ -139,7 +156,7 @@ func ToEncryptionState(encryptionConfig *apiserverconfigv1.EncryptionConfigurati // it primarily handles the conversion of KeyState to the appropriate provider config. // the identity mode is transformed into a custom aesgcm provider that simply exists to // curry the associated null key secret through the encryption state machine. -func stateToProviders(desired state.GroupResourceState) []apiserverconfigv1.ProviderConfiguration { +func stateToProviders(resource string, desired state.GroupResourceState) []apiserverconfigv1.ProviderConfiguration { allKeys := desired.ReadKeys providers := make([]apiserverconfigv1.ProviderConfiguration, 0, len(allKeys)+1) // one extra for identity @@ -192,6 +209,8 @@ func stateToProviders(desired state.GroupResourceState) []apiserverconfigv1.Prov Keys: []apiserverconfigv1.Key{key.Key}, }, }) + case state.KMS: + providers = append(providers, kms.GenerateKMSProviderConfigurationFromKey(resource, key)) default: // this should never happen because our input should always be valid klog.Infof("skipping key %s as it has invalid mode %s", key.Key.Name, key.Mode) diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms.go new file mode 100644 index 0000000000..1c9339cd44 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms.go @@ -0,0 +1,144 @@ +package kms + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "regexp" + "time" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/library-go/pkg/operator/encryption/state" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apiserver/pkg/apis/apiserver/v1" +) + +const ( + // unixSocketBaseDir is the base directory for KMS unix sockets + unixSocketBaseDir = "unix:///var/run/kms" +) + +// GenerateUnixSocketPath generates a unique unix socket path from KMS configuration +// by hashing the provider-specific configuration. +// Returns the socket path and the hash value (first 16 characters). +func GenerateUnixSocketPath(kmsConfig *configv1.KMSConfig) (string, string, error) { + if kmsConfig == nil { + return "", "", fmt.Errorf("kmsConfig cannot be nil") + } + + // Determine KMS type and generate path accordingly + switch kmsConfig.Type { + case configv1.AWSKMSProvider: + if kmsConfig.AWS == nil { + return "", "", fmt.Errorf("AWS KMS config cannot be nil for AWS provider type") + } + return generateAWSUnixSocketPath(kmsConfig.AWS) + default: + return "", "", fmt.Errorf("unsupported KMS provider type: %s", kmsConfig.Type) + } +} + +// generateAWSUnixSocketPath generates a unique unix socket path from AWS KMS configuration +// by hashing the ARN and region. Returns the socket path and the hash (first 16 characters). +func generateAWSUnixSocketPath(awsConfig *configv1.AWSKMSConfig) (string, string, error) { + if awsConfig.KeyARN == "" { + return "", "", fmt.Errorf("AWS KMS KeyARN cannot be empty") + } + + if awsConfig.Region == "" { + return "", "", fmt.Errorf("AWS region cannot be empty") + } + + // Combine KeyARN and region for hashing + combined := awsConfig.KeyARN + ":" + awsConfig.Region + + // Compute SHA256 hash + hash := sha256.Sum256([]byte(combined)) + hashStr := hex.EncodeToString(hash[:]) + + // Take first 16 characters of hash for shorter path + shortHash := hashStr[:16] + + socketPath := fmt.Sprintf("%s/kms-%s.sock", unixSocketBaseDir, shortHash) + + return socketPath, shortHash, nil +} + +// ComputeKMSKeyHash computes a hash of the KMS key ID returned from the Status endpoint. +// Returns the first 32 characters of the SHA256 hash. +func ComputeKMSKeyHash(configHash, keyID string) []byte { + if keyID == "" { + return nil + } + + combined := configHash + ":" + keyID + // Compute SHA256 hash + hash := sha256.Sum256([]byte(combined)) + hashStr := hex.EncodeToString(hash[:]) + + return []byte(hashStr[:32]) +} + +var ( + // endpointHashRegex matches the config hash in endpoint path: unix://var/run/kms/kms-{configHash16}.sock + endpointHashRegex = regexp.MustCompile(`kms-([a-f0-9]{16})\.sock$`) + // providerNameRegex matches the key ID hash, key ID, and resource in provider name: kms-provider-{keyIDHash32}-{keyID}-{resource} + // Example: kms-provider-abcdef1234567890abcdef1234567890-1-secrets + providerNameRegex = regexp.MustCompile(`^kms-provider-([a-f0-9]{32})-([^-]+)-(.+)$`) +) + +// ExtractKMSHashAndKeyName extracts the KMSConfigHash, KMSKeyIDHash, and key.Name embedded into provider +// name and socket path. Returns (configHash, keyIDHash, keyName, error) +func ExtractKMSHashAndKeyName(provider v1.ProviderConfiguration) (string, string, string, error) { + // Extract the config hash from the endpoint path: unix://var/run/kms/kms-{configHash}.sock + endpoint := provider.KMS.Endpoint + var configHash string + if matches := endpointHashRegex.FindStringSubmatch(endpoint); len(matches) == 2 { + configHash = matches[1] + } else { + return "", "", "", fmt.Errorf("invalid KMS endpoint format: %s", endpoint) + } + + // Extract the key ID hash, key ID, and resource from the provider name: kms-provider-{keyIDHash32}-{keyID}-{resource} + // Example: kms-provider-abcdef1234567890abcdef1234567890-1-secrets + var keyHash, keyName string + providerName := provider.KMS.Name + if matches := providerNameRegex.FindStringSubmatch(providerName); len(matches) == 4 { + keyHash = matches[1] + keyName = matches[2] + // matches[3] is the resource, but we don't need to return it + } else { + return "", "", "", fmt.Errorf("invalid KMS provider name format: %s", providerName) + } + + return configHash, base64.StdEncoding.EncodeToString([]byte(keyHash)), keyName, nil +} + +// GenerateKMSProviderConfigurationFromKey generates the compatible ProviderConfiguration with +// opinionated and extractable fields. We embed: +// - KMSConfigHash in the socket path (endpoint) +// - KMSKeyIDHash, key.Name, and resource in the provider name +// This allows us to extract all three values and detect both config changes and key rotations. +// The resource parameter ensures uniqueness when the same KMS config is used for multiple resources. +func GenerateKMSProviderConfigurationFromKey(resource string, key state.KeyState) v1.ProviderConfiguration { + // Embed KMSConfigHash in the endpoint so we can extract it + // This must generate the same format as GenerateUnixSocketPath + socketPath := fmt.Sprintf("%s/kms-%s.sock", unixSocketBaseDir, key.KMSConfigHash) + // Embed KMSKeyIDHash, key ID, and resource in the provider name so we can extract them when reading back + // Format: kms-provider-{keyIDHash32}-{keyID}-{resource} + // This must match the providerNameRegex + decoded, _ := base64.StdEncoding.DecodeString(key.Key.Secret) + providerName := fmt.Sprintf("kms-provider-%s-%s-%s", decoded, key.Key.Name, resource) + + return v1.ProviderConfiguration{ + KMS: &v1.KMSConfiguration{ + APIVersion: "v2", + Name: providerName, + Endpoint: socketPath, + Timeout: &metav1.Duration{ + Duration: 10 * time.Second, + }, + }, + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms_client.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms_client.go new file mode 100644 index 0000000000..c0aa86e1b0 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms_client.go @@ -0,0 +1,85 @@ +package kms + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + kmsv2 "k8s.io/kms/apis/v2" +) + +const ( + // defaultTimeout is the default timeout for KMS operations + defaultTimeout = 30 * time.Second +) + +// KMSClient is an interface for interacting with KMS plugins +type KMSClient interface { + // Status calls the KMS plugin's Status endpoint and returns the response + Status(ctx context.Context) (*StatusResponse, error) + // Close closes the connection to the KMS plugin + Close() error +} + +// StatusResponse represents the response from a KMS Status call +type StatusResponse struct { + Version string + Healthz string + KeyID string +} + +// kmsClient implements the KMSClient interface +type kmsClient struct { + conn *grpc.ClientConn + kmsClient kmsv2.KeyManagementServiceClient + endpoint string +} + +func NewKMSClient(endpoint string) (KMSClient, error) { + if endpoint == "" { + return nil, fmt.Errorf("kms endpoint cannot be empty") + } + + options := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + + conn, err := grpc.NewClient(endpoint, options...) + if err != nil { + return nil, fmt.Errorf("failed to connect to KMS plugin at %s: %w", endpoint, err) + } + + return &kmsClient{ + conn: conn, + kmsClient: kmsv2.NewKeyManagementServiceClient(conn), + endpoint: endpoint, + }, nil +} + +// Status calls the KMS plugin's Status endpoint +func (c *kmsClient) Status(ctx context.Context) (*StatusResponse, error) { + timeoutCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + + // Call the Status endpoint + resp, err := c.kmsClient.Status(timeoutCtx, &kmsv2.StatusRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to call KMS Status endpoint at %s: %w", c.endpoint, err) + } + + return &StatusResponse{ + Version: resp.GetVersion(), + Healthz: resp.GetHealthz(), + KeyID: resp.GetKeyId(), + }, nil +} + +// Close closes the connection to the KMS plugin +func (c *kmsClient) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/plugin.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/plugin.go new file mode 100644 index 0000000000..8b85a0cc7f --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/plugin.go @@ -0,0 +1,295 @@ +package kms + +import ( + "fmt" + "strings" + + configv1 "github.com/openshift/api/config/v1" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" +) + +// from https://github.com/flavianmissi/library-go/tree/kms-plugin-sidecars + +const ( + // defaultKMSPluginImage is the default AWS KMS plugin image + // This should be overridden by the operator with the actual image reference + defaultKMSPluginImage = "registry.k8s.io/kms-plugin-aws:latest" + + // defaultHealthPort is the default port for the KMS plugin health endpoint + defaultHealthPort = 8080 + + // KMSContainerName is the standard name for the KMS plugin sidecar container + KMSContainerName = "kms-plugin" + + // KMSSocketVolumeName is the standard name for the socket volume + KMSSocketVolumeName = "kms-socket" + + // KMSCredentialsVolumeName is the standard name for the credentials volume + KMSCredentialsVolumeName = "aws-credentials" +) + +// ContainerConfig holds additional configuration beyond what's in configv1.KMSConfig +// for building the KMS plugin sidecar container +type ContainerConfig struct { + // KMSConfig is the desired configv1.KMSConfig + // Required + KMSConfig *configv1.KMSConfig + + // Image is the container image for the KMS plugin + // Required + Image string + + // UseHostNetwork indicates if the pod uses hostNetwork: true + // If true, the container will access AWS credentials via EC2 IMDS + // If false, credentials must be provided via CredentialsSecretName + UseHostNetwork bool + + // CredentialsSecretName is the name of the secret containing AWS credentials + // Only required when UseHostNetwork is false + // The secret should contain a key "credentials" in AWS shared credentials file format + CredentialsSecretName string + + // SocketPath is the Unix socket path where the KMS plugin listens + // Optional - defaults to defaultSocketPath + SocketPath string + + // HealthPort is the port for the KMS plugin health endpoint + // Optional - defaults to defaultHealthPort + HealthPort int32 + + // CPURequest is the CPU request for the container + // Optional - defaults to "10m" + CPURequest string + + // MemoryRequest is the memory request for the container + // Optional - defaults to "50Mi" + MemoryRequest string +} + +// Validate ensures the ContainerConfig is valid +func (c *ContainerConfig) Validate() error { + if c.Image == "" { + return fmt.Errorf("Image is required") + } + if !c.UseHostNetwork && c.CredentialsSecretName == "" { + return fmt.Errorf("CredentialsSecretName is required when UseHostNetwork is false") + } + return nil +} + +// setDefaults sets default values for unspecified fields +func (c *ContainerConfig) setDefaults() { + if c.SocketPath == "" { + socket, _, err := GenerateUnixSocketPath(c.KMSConfig) + if err != nil { + panic(err) + } + c.SocketPath = strings.Replace(socket, "unix://", "", 1) + } + if c.HealthPort == 0 { + c.HealthPort = defaultHealthPort + } + if c.CPURequest == "" { + c.CPURequest = "10m" + } + if c.MemoryRequest == "" { + c.MemoryRequest = "50Mi" + } +} + +// buildPluginContainer creates a corev1.Container spec for the KMS plugin sidecar +// based on the KMS configuration from openshift/api and container-specific config +func buildPluginContainer(kmsConfig *configv1.KMSConfig, containerConfig *ContainerConfig) (*corev1.Container, error) { + if kmsConfig == nil { + return nil, fmt.Errorf("kmsConfig cannot be nil") + } + if containerConfig == nil { + return nil, fmt.Errorf("containerConfig cannot be nil") + } + + // Validate inputs + if err := containerConfig.Validate(); err != nil { + return nil, fmt.Errorf("invalid container config: %w", err) + } + + // Set defaults + containerConfig.setDefaults() + + // Currently only AWS is supported + if kmsConfig.Type != configv1.AWSKMSProvider { + return nil, fmt.Errorf("unsupported KMS provider type: %s (only %s is supported)", kmsConfig.Type, configv1.AWSKMSProvider) + } + if kmsConfig.AWS == nil { + return nil, fmt.Errorf("AWS KMS config is required when type is AWS") + } + + container := &corev1.Container{ + Name: KMSContainerName, + Image: containerConfig.Image, + Command: []string{ + "/aws-encryption-provider", + }, + Args: []string{ + fmt.Sprintf("--key=%s", kmsConfig.AWS.KeyARN), + fmt.Sprintf("--region=%s", kmsConfig.AWS.Region), + fmt.Sprintf("--listen=%s", containerConfig.SocketPath), + }, + Ports: []corev1.ContainerPort{ + { + Name: "healthz", + ContainerPort: containerConfig.HealthPort, + Protocol: corev1.ProtocolTCP, + }, + }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + ReadOnlyRootFilesystem: ptr.To(true), + RunAsUser: ptr.To(int64(0)), // Required for AWS SDK credential chain + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: KMSSocketVolumeName, + MountPath: "/var/run/kms", + }, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz", + Port: intstr.FromInt32(containerConfig.HealthPort), + }, + }, + InitialDelaySeconds: 10, + PeriodSeconds: 10, + TimeoutSeconds: 3, + FailureThreshold: 3, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz", + Port: intstr.FromInt32(containerConfig.HealthPort), + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 5, + TimeoutSeconds: 3, + FailureThreshold: 3, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(containerConfig.CPURequest), + corev1.ResourceMemory: resource.MustParse(containerConfig.MemoryRequest), + }, + }, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + } + + // Add credentials mount if not using hostNetwork + if !containerConfig.UseHostNetwork { + container.Env = append(container.Env, corev1.EnvVar{ + Name: "AWS_SHARED_CREDENTIALS_FILE", + Value: "/var/run/secrets/aws/credentials", + }) + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: KMSCredentialsVolumeName, + MountPath: "/var/run/secrets/aws", + ReadOnly: true, + }) + } + + return container, nil +} + +// buildPluginVolumes creates the required volumes for the KMS plugin +func buildPluginVolumes(useHostNetwork bool, credentialsSecretName string, hostPath bool) []corev1.Volume { + volumes := []corev1.Volume{ + { + Name: KMSSocketVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + } + + // For static pods using hostPath, override the socket volume + if hostPath { + volumes[0].VolumeSource = corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/var/run/kms", + Type: ptr.To(corev1.HostPathDirectoryOrCreate), + }, + } + } + + // Add credentials volume if not using hostNetwork + if !useHostNetwork && credentialsSecretName != "" { + volumes = append(volumes, corev1.Volume{ + Name: KMSCredentialsVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: credentialsSecretName, + Items: []corev1.KeyToPath{ + { + Key: "credentials", + Path: "credentials", + }, + }, + }, + }, + }) + } + + return volumes +} + +// AddKMSPluginToPodSpec injects the KMS plugin container and volumes into a PodSpec +// This is a convenience function that combines buildPluginContainer() and buildPluginVolumes() +func AddKMSPluginToPodSpec( + podSpec *corev1.PodSpec, + kmsConfig *configv1.KMSConfig, + containerConfig *ContainerConfig, + useHostPathForSocket bool, +) error { + if podSpec == nil { + return fmt.Errorf("podSpec cannot be nil") + } + + // Create the KMS plugin container + kmsContainer, err := buildPluginContainer(kmsConfig, containerConfig) + if err != nil { + return fmt.Errorf("failed to create KMS plugin container: %w", err) + } + + // Add the container to the pod spec + podSpec.Containers = append(podSpec.Containers, *kmsContainer) + + // Add required volumes + volumes := buildPluginVolumes(containerConfig.UseHostNetwork, containerConfig.CredentialsSecretName, useHostPathForSocket) + podSpec.Volumes = append(podSpec.Volumes, volumes...) + + // Mount the KMS socket in the API server container + // Find the main API server container and add the socket mount + for i := range podSpec.Containers { + // Look for common API server container names + if podSpec.Containers[i].Name == "kube-apiserver" || + podSpec.Containers[i].Name == "openshift-apiserver" || + podSpec.Containers[i].Name == "oauth-apiserver" { + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ + Name: KMSSocketVolumeName, + MountPath: "/var/run/kms", + ReadOnly: true, + }) + break + } + } + + return nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go index 4e54317c7d..f814d153d9 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go @@ -58,14 +58,18 @@ func ToKeyState(s *corev1.Secret) (state.KeyState, error) { key.ExternalReason = v } + if v, ok := s.Annotations[EncryptionSecretKMSConfigHash]; ok && len(v) > 0 { + key.KMSConfigHash = v + } + keyMode := state.Mode(s.Annotations[encryptionSecretMode]) switch keyMode { - case state.AESCBC, state.AESGCM, state.SecretBox, state.Identity: + case state.AESCBC, state.AESGCM, state.SecretBox, state.Identity, state.KMS: key.Mode = keyMode default: return state.KeyState{}, fmt.Errorf("secret %s/%s has invalid mode: %s", s.Namespace, s.Name, keyMode) } - if keyMode != state.Identity && len(data) == 0 { + if keyMode != state.Identity && keyMode != state.KMS && len(data) == 0 { return state.KeyState{}, fmt.Errorf("secret %s/%s of mode %q must have non-empty key", s.Namespace, s.Name, keyMode) } @@ -113,6 +117,10 @@ func FromKeyState(component string, ks state.KeyState) (*corev1.Secret, error) { s.Annotations[EncryptionSecretMigratedResources] = string(bs) } + if len(ks.KMSConfigHash) > 0 { + s.Annotations[EncryptionSecretKMSConfigHash] = ks.KMSConfigHash + } + return s, nil } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go index 7161e4a124..32a576dea2 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go @@ -41,6 +41,10 @@ const ( // determine if a new key should be created even if encryptionSecretMigrationInterval has not been reached. encryptionSecretExternalReason = "encryption.apiserver.operator.openshift.io/external-reason" + // EncryptionSecretKMSConfigHash is the annotation that stores the hash of the KMS configuration. + // This is used to detect changes in the KMS configuration that would require a new key. + EncryptionSecretKMSConfigHash = "encryption.apiserver.operator.openshift.io/kms-config-hash" + // In the data field of the secret API object, this (map) key is used to hold the actual encryption key // (i.e. for AES-CBC mode the value associated with this map key is 32 bytes of random noise). EncryptionSecretKeyDataKey = "encryption.apiserver.operator.openshift.io-key" diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go index 460c21bfa2..8d2c621f5e 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go @@ -40,6 +40,8 @@ type KeyState struct { InternalReason string // the user via unsupportConfigOverrides.encryption.reason triggered this key. ExternalReason string + // hash of the KMS configuration to detect changes + KMSConfigHash string } type MigrationState struct { @@ -60,6 +62,7 @@ const ( AESGCM Mode = "aesgcm" SecretBox Mode = "secretbox" // available from the first release, see defaultMode below Identity Mode = "identity" // available from the first release, see defaultMode below + KMS Mode = "KMS" // Changing this value requires caution to not break downgrades. // Specifically, if some new Mode is released in version X, that new Mode cannot diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go index 88bd00b251..0b52c3a324 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go @@ -415,3 +415,51 @@ func ApplyValidatingAdmissionPolicyBindingV1(ctx context.Context, client admissi cache.UpdateCachedResourceMetadata(requiredOriginal, actual) return actual, true, nil } + +func DeleteValidatingAdmissionPolicyV1beta1(ctx context.Context, client admissionregistrationclientv1beta1.ValidatingAdmissionPoliciesGetter, recorder events.Recorder, required *admissionregistrationv1beta1.ValidatingAdmissionPolicy) (*admissionregistrationv1beta1.ValidatingAdmissionPolicy, bool, error) { + err := client.ValidatingAdmissionPolicies().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyBindingV1beta1(ctx context.Context, client admissionregistrationclientv1beta1.ValidatingAdmissionPolicyBindingsGetter, recorder events.Recorder, required *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding) (*admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding, bool, error) { + err := client.ValidatingAdmissionPolicyBindings().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyV1(ctx context.Context, client admissionregistrationclientv1.ValidatingAdmissionPoliciesGetter, recorder events.Recorder, required *admissionregistrationv1.ValidatingAdmissionPolicy) (*admissionregistrationv1.ValidatingAdmissionPolicy, bool, error) { + err := client.ValidatingAdmissionPolicies().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyBindingV1(ctx context.Context, client admissionregistrationclientv1.ValidatingAdmissionPolicyBindingsGetter, recorder events.Recorder, required *admissionregistrationv1.ValidatingAdmissionPolicyBinding) (*admissionregistrationv1.ValidatingAdmissionPolicyBinding, bool, error) { + err := client.ValidatingAdmissionPolicyBindings().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go index 9105464bd0..471f1bb2f2 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go @@ -380,6 +380,30 @@ func DeleteAll(ctx context.Context, clients *ClientHolder, recorder events.Recor } else { _, result.Changed, result.Error = DeleteValidatingWebhookConfiguration(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) } + case *admissionregistrationv1beta1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t) + } + case *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyBindingV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t) + } + case *admissionregistrationv1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) + } + case *admissionregistrationv1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyBindingV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) + } case *storagev1.CSIDriver: if clients.kubeClient == nil { result.Error = fmt.Errorf("missing kubeClient") diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml index 657c87ec8b..29d4a345b3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml @@ -14,6 +14,7 @@ spec: terminationGracePeriodSeconds: 3 tolerations: - operator: Exists + hostUsers: false containers: - name: guard image: # Value set by operator diff --git a/vendor/modules.txt b/vendor/modules.txt index 24a45fcdf7..7d6ebb2fd4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -403,7 +403,7 @@ github.com/openshift/client-go/security/informers/externalversions/internalinter github.com/openshift/client-go/security/informers/externalversions/security github.com/openshift/client-go/security/informers/externalversions/security/v1 github.com/openshift/client-go/security/listers/security/v1 -# github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 +# github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 => github.com/ardaguclu/library-go v0.0.0-20251105061910-519a2c27c78d ## explicit; go 1.24.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/assets @@ -440,6 +440,7 @@ github.com/openshift/library-go/pkg/operator/encryption/controllers/migrators github.com/openshift/library-go/pkg/operator/encryption/crypto github.com/openshift/library-go/pkg/operator/encryption/deployer github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig +github.com/openshift/library-go/pkg/operator/encryption/kms github.com/openshift/library-go/pkg/operator/encryption/observer github.com/openshift/library-go/pkg/operator/encryption/secrets github.com/openshift/library-go/pkg/operator/encryption/state @@ -1681,3 +1682,4 @@ sigs.k8s.io/structured-merge-diff/v6/value ## explicit; go 1.22 sigs.k8s.io/yaml # github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 +# github.com/openshift/library-go => github.com/ardaguclu/library-go v0.0.0-20251105061910-519a2c27c78d