From dacdc2c37bcef8d9d9f34272a337c8b27cd96c97 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Fri, 31 Jul 2026 13:59:12 -0400 Subject: [PATCH] OCPBUGS-86895: Ensure packageserver pod seccompProfile is always set The openshift-operator-lifecycle-manager namespace enforces pod-security.kubernetes.io/enforce: restricted:latest, which requires every pod to have securityContext.seccompProfile.type set to RuntimeDefault or Localhost. The packageserver CSV template (pkg/manifests/csv.yaml) includes the correct pod-level seccompProfile, but there is a window where the cluster-stored CSV can diverge from the template: - The cluster CSV may have been created before seccompProfile was added to the template (commit 26b20611d), leaving it absent in the stored object. - During OLM upgrades, a race between the PSM and OLM can cause OLM to process a cached, pre-update version of the CSV and generate a Deployment without the field. In both cases OLM faithfully reproduces whatever is in the cluster CSV, so the generated Deployment pod template is missing seccompProfile. On most clusters this is masked: OpenShift's restricted-v2 SCC mutating admission adds seccompProfile to pods before PSA validates them. On OSD/ROSA Classic the managed admission chain processes PSA before (or independently of) SCC mutation, so pods are rejected, the Deployment stays unhealthy, and OLM enters a perpetual cert-rotation reinstall loop (observed at generation=716, revision=566). Fix: explicitly enforce seccompProfile in ensureCSVHighAvailability, which runs on every PSM reconcile. This makes the field mandatory regardless of what the stored CSV contains, closes the upgrade race window, and does not depend on the reflect.DeepEqual full-spec comparison in ensureCSV catching a single missing field among many. Add three test cases covering: missing seccompProfile with HA topology, missing seccompProfile with single-replica topology, and a nil SecurityContext (defensive nil-safety). Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- pkg/package-server-manager/config.go | 15 ++++++++ pkg/package-server-manager/controller_test.go | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/pkg/package-server-manager/config.go b/pkg/package-server-manager/config.go index 04ec8a75bb..fc85710d91 100644 --- a/pkg/package-server-manager/config.go +++ b/pkg/package-server-manager/config.go @@ -149,6 +149,21 @@ func ensureCSVHighAvailability(image string, csv *olmv1alpha1.ClusterServiceVers modified = true } + // Ensure pod-level seccompProfile is always set. The openshift-operator-lifecycle-manager + // namespace enforces restricted:latest PodSecurity, which requires seccompProfile. The CSV + // template includes this field, but it may be absent if the cluster CSV was created by an + // older version or if SCC/PSA admission ordering masks the missing field on most clusters. + if deployment.Template.Spec.SecurityContext == nil { + deployment.Template.Spec.SecurityContext = &corev1.PodSecurityContext{} + } + if deployment.Template.Spec.SecurityContext.SeccompProfile == nil || + deployment.Template.Spec.SecurityContext.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault { + deployment.Template.Spec.SecurityContext.SeccompProfile = &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + } + modified = true + } + if modified { csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs[0].Spec = *deployment } diff --git a/pkg/package-server-manager/controller_test.go b/pkg/package-server-manager/controller_test.go index ae7fa6423c..2293329cf1 100644 --- a/pkg/package-server-manager/controller_test.go +++ b/pkg/package-server-manager/controller_test.go @@ -94,6 +94,21 @@ func withAffinity(affinity *corev1.Affinity) func(*olmv1alpha1.ClusterServiceVer csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs[0].Spec.Template.Spec.Affinity = affinity } } + +func withoutSeccompProfile() testCSVOption { + return func(csv *olmv1alpha1.ClusterServiceVersion) { + podSpec := &csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs[0].Spec.Template.Spec + if podSpec.SecurityContext != nil { + podSpec.SecurityContext.SeccompProfile = nil + } + } +} + +func withNilPodSecurityContext() testCSVOption { + return func(csv *olmv1alpha1.ClusterServiceVersion) { + csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs[0].Spec.Template.Spec.SecurityContext = nil + } +} func withRollingUpdateStrategy(strategy *appsv1.RollingUpdateDeployment) func(*olmv1alpha1.ClusterServiceVersion) { return func(csv *olmv1alpha1.ClusterServiceVersion) { csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs[0].Spec.Strategy.RollingUpdate = strategy @@ -255,6 +270,27 @@ func TestEnsureCSV(t *testing.T) { inputCSV: newTestCSV(withReplicas(singleReplicas), withRollingUpdateStrategy(emptyRollout), withAffinity(&corev1.Affinity{})), expectedCSV: newTestCSV(withReplicas(singleReplicas), withRollingUpdateStrategy(emptyRollout), withAffinity(&corev1.Affinity{})), }, + { + name: "Modified/HighlyAvailable/MissingSeccompProfile", + want: wanted{true, nil}, + highlyAvailable: true, + inputCSV: newTestCSV(withReplicas(defaultReplicas), withRollingUpdateStrategy(defaultRollout), withAffinity(defaultAffinity), withoutSeccompProfile()), + expectedCSV: newTestCSV(withReplicas(defaultReplicas), withRollingUpdateStrategy(defaultRollout), withAffinity(defaultAffinity)), + }, + { + name: "Modified/SingleReplica/MissingSeccompProfile", + want: wanted{true, nil}, + highlyAvailable: false, + inputCSV: newTestCSV(withReplicas(singleReplicas), withRollingUpdateStrategy(emptyRollout), withAffinity(&corev1.Affinity{}), withoutSeccompProfile()), + expectedCSV: newTestCSV(withReplicas(singleReplicas), withRollingUpdateStrategy(emptyRollout), withAffinity(&corev1.Affinity{})), + }, + { + name: "Modified/HighlyAvailable/NilPodSecurityContext", + want: wanted{true, nil}, + highlyAvailable: true, + inputCSV: newTestCSV(withReplicas(defaultReplicas), withRollingUpdateStrategy(defaultRollout), withAffinity(defaultAffinity), withNilPodSecurityContext()), + expectedCSV: newTestCSV(withReplicas(defaultReplicas), withRollingUpdateStrategy(defaultRollout), withAffinity(defaultAffinity)), + }, } for _, tc := range tt {