From 9fc9063b992838d66e4fd414eccb9769a8206ee4 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Wed, 30 Jul 2025 19:42:37 +0200 Subject: [PATCH 01/15] podsecurityreadinesscontroller: refactor, return test PSS, fix err ret The refactoring is necessary to split the condition into condition and classification as the classification is quite tricky. If we can't determine the PSS, we shouldn' error out, but default to global config. --- .../podsecurityreadinesscontroller.go | 2 +- .../violation.go | 51 ++++++++----------- .../violation_test.go | 4 +- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go index 080546e4dd..4e2ac7541b 100644 --- a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go +++ b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go @@ -71,7 +71,7 @@ func (c *PodSecurityReadinessController) sync(ctx context.Context, syncCtx facto conditions := podSecurityOperatorConditions{} for _, ns := range nsList.Items { err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - isViolating, err := c.isNamespaceViolating(ctx, &ns) + isViolating, _, err := c.isNamespaceViolating(ctx, &ns) if apierrors.IsNotFound(err) { return nil } diff --git a/pkg/operator/podsecurityreadinesscontroller/violation.go b/pkg/operator/podsecurityreadinesscontroller/violation.go index 97a0ed3fe7..e4775177b2 100644 --- a/pkg/operator/podsecurityreadinesscontroller/violation.go +++ b/pkg/operator/podsecurityreadinesscontroller/violation.go @@ -2,7 +2,6 @@ package podsecurityreadinesscontroller import ( "context" - "fmt" securityv1 "github.com/openshift/api/security/v1" corev1 "k8s.io/api/core/v1" @@ -21,17 +20,16 @@ var ( alertLabels = sets.New(psapi.WarnLevelLabel, psapi.AuditLevelLabel) ) -func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Context, ns *corev1.Namespace) (bool, error) { +// isNamespaceViolating checks if a namespace is ready for Pod Security Admission enforcement. +// It returns true if the namespace is violating the Pod Security Admission policy, along with +// the enforce label it was tested against. +func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Context, ns *corev1.Namespace) (bool, string, error) { nsApplyConfig, err := applyconfiguration.ExtractNamespace(ns, syncerControllerName) if err != nil { - return false, err - } - - enforceLabel, err := determineEnforceLabelForNamespace(nsApplyConfig) - if err != nil { - return false, err + return false, "", err } + enforceLabel := determineEnforceLabelForNamespace(nsApplyConfig) nsApply := applyconfiguration.Namespace(ns.Name).WithLabels(map[string]string{ psapi.EnforceLevelLabel: enforceLabel, }) @@ -43,41 +41,34 @@ func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Contex FieldManager: "pod-security-readiness-controller", }) if err != nil { - return false, err + return false, "", err } // If there are warnings, the namespace is violating. - return len(c.warningsHandler.PopAll()) > 0, nil + warnings := c.warningsHandler.PopAll() + if len(warnings) > 0 { + return true, enforceLabel, nil + } + + return false, "", nil } -func determineEnforceLabelForNamespace(ns *applyconfiguration.NamespaceApplyConfiguration) (string, error) { - if label, ok := ns.Annotations[securityv1.MinimallySufficientPodSecurityStandard]; ok { +func determineEnforceLabelForNamespace(ns *applyconfiguration.NamespaceApplyConfiguration) string { + if _, ok := ns.Annotations[securityv1.MinimallySufficientPodSecurityStandard]; ok { // This should generally exist and will be the only supported method of determining // the enforce level going forward - however, we're keeping the label fallback for // now to account for any workloads not yet annotated using a new enough version of // the syncer, such as during upgrade scenarios. - return label, nil + return ns.Annotations[securityv1.MinimallySufficientPodSecurityStandard] } - viableLabels := map[string]string{} - - for alertLabel := range alertLabels { - if value, ok := ns.Labels[alertLabel]; ok { - viableLabels[alertLabel] = value + targetLevel := "" + for label := range alertLabels { + value, ok := ns.Labels[label] + if !ok { + continue } - } - - if len(viableLabels) == 0 { - // If there are no labels/annotations managed by the syncer, we can't make a decision. - return "", fmt.Errorf("unable to determine if the namespace is violating because no appropriate labels or annotations were found") - } - return pickStrictest(viableLabels), nil -} - -func pickStrictest(viableLabels map[string]string) string { - targetLevel := "" - for label, value := range viableLabels { level, err := psapi.ParseLevel(value) if err != nil { klog.V(4).InfoS("invalid level", "label", label, "value", value) diff --git a/pkg/operator/podsecurityreadinesscontroller/violation_test.go b/pkg/operator/podsecurityreadinesscontroller/violation_test.go index f25cce81ca..e1db30031f 100644 --- a/pkg/operator/podsecurityreadinesscontroller/violation_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/violation_test.go @@ -88,7 +88,7 @@ func TestIsNamespaceViolating(t *testing.T) { return &mockKubeClientWithResponse{} }, expectViolating: false, - expectError: true, + expectError: false, }, { name: "Apply returns error", @@ -124,7 +124,7 @@ func TestIsNamespaceViolating(t *testing.T) { tc.namespace.ManagedFields = managedFields - violating, err := controller.isNamespaceViolating(context.Background(), tc.namespace) + violating, _, err := controller.isNamespaceViolating(context.Background(), tc.namespace) if (err != nil) != tc.expectError { t.Errorf("isNamespaceViolating() error = %v, expectError %v", err, tc.expectError) From a2def2c0deeff7b16c2c00bc962e412a4167d5ba Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Wed, 30 Jul 2025 19:45:26 +0200 Subject: [PATCH 02/15] podsecurityreadinesscontroller: create classification, add README --- .../podsecurityreadinesscontroller/README.md | 113 +++++ .../classification.go | 133 +++++ .../classification_test.go | 471 ++++++++++++++++++ .../podsecurityreadinesscontroller.go | 18 +- 4 files changed, 730 insertions(+), 5 deletions(-) create mode 100644 pkg/operator/podsecurityreadinesscontroller/README.md create mode 100644 pkg/operator/podsecurityreadinesscontroller/classification.go create mode 100644 pkg/operator/podsecurityreadinesscontroller/classification_test.go diff --git a/pkg/operator/podsecurityreadinesscontroller/README.md b/pkg/operator/podsecurityreadinesscontroller/README.md new file mode 100644 index 0000000000..23913ede16 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/README.md @@ -0,0 +1,113 @@ +# Pod Security Readiness Controller + +The Pod Security Readiness Controller evaluates namespace compatibility with Pod Security Admission (PSA) enforcement in clusters. + +## Purpose + +This controller performs dry-run PSA evaluations to determine which namespaces would experience pod creation failures if PSA enforcement labels were applied. + +The controller generates telemetry data for `ClusterFleetEvaluation` and helps us to understand PSA compatibility before enabling enforcement. + +## Implementation + +The controller follows this evaluation algorithm: + +1. **Namespace Discovery** - Find namespaces without PSA enforcement +2. **PSA Level Determination** - Predict what enforcement level would be applied +3. **Dry-Run Evaluation** - Test namespace against predicted PSA level +4. **Violation Classification** - Categorize any violations found for telemetry + +### Namespace Discovery + +Selects namespaces without PSA enforcement labels: + +```go +selector := "!pod-security.kubernetes.io/enforce" +``` + +### PSA Level Determination + +The controller determines the effective PSA enforcement level using this precedence: + +1. `security.openshift.io/MinimallySufficientPodSecurityStandard` annotation +2. Most restrictive of existing `pod-security.kubernetes.io/warn` or `pod-security.kubernetes.io/audit` labels, if owned by the PSA label syncer +3. Kube API server's future global default: `restricted` + +### Dry-Run Evaluation + +The controller performs the equivalent of this oc command: + +```bash +oc label --dry-run=server --overwrite namespace $NAMESPACE_NAME \ + pod-security.kubernetes.io/enforce=$POD_SECURITY_STANDARD +``` + +PSA warnings during dry-run indicate the namespace contains violating workloads. + +### Violation Classification + +Violating namespaces are categorized for telemetry analysis: + +| Classification | Criteria | Purpose | +|------------------|-----------------------------------------------------------------|----------------------------------------| +| `runLevelZero` | Core namespaces: `kube-system`, `default`, `kube-public` | Platform infrastructure tracking | +| `openshift` | Namespaces with `openshift-` prefix | OpenShift component tracking | +| `disabledSyncer` | Label `security.openshift.io/scc.podSecurityLabelSync: "false"` | Intentionally excluded namespaces | +| `userSCC` | Contains user workloads that violate PSA | SCC vs PSA policy conflicts | +| `customer` | All other violating namespaces | Customer workload compatibility issues | +| `inconclusive` | Evaluation failed due to API errors | Operational problems | + +#### User SCC Detection + +The PSA label syncer bases its evaluation exclusively on a ServiceAccount's SCCs, ignoring a user's SCCs. +When a pod's SCC assignment comes from user permissions rather than its ServiceAccount, the syncer's predicted PSA level may be incorrect. +Therefore we need to evaluate the affected pods (if any) against the target PSA level. + +### Inconclusive Handling + +When the evaluation process fails, namespaces are marked as `inconclusive`. + +Common causes for inconclusive results: + +- **API server unavailable** - Network timeouts, etcd issues +- **Resource conflicts** - Concurrent namespace modifications +- **Invalid PSA levels** - Malformed enforcement level strings +- **Pod listing failures** - RBAC issues or resource pressure + +High rates of inconclusive results across the fleet may indicate systematic issues that requires investigation. + +## Output + +The controller updates `OperatorStatus` conditions for each violation type: + +```go +type podSecurityOperatorConditions struct { + violatingRunLevelZeroNamespaces []string + violatingOpenShiftNamespaces []string + violatingDisabledSyncerNamespaces []string + violatingCustomerNamespaces []string + userSCCViolationNamespaces []string + inconclusiveNamespaces []string +} +``` + +Conditions follow the pattern: + +- `PodSecurity{Type}EvaluationConditionsDetected` +- Status: `True` (violations found) / `False` (no violations) +- Message includes violating namespace list + +## Configuration + +The controller runs with a configurable interval (default: 4 hours) and uses rate limiting to avoid overwhelming the API server: + +```go +kubeClientCopy.QPS = 2 +kubeClientCopy.Burst = 2 +``` + +## Integration Points + +- **PSA Label Syncer**: Reads syncer-managed PSA labels to predict enforcement levels +- **Cluster Operator**: Reports status through standard operator conditions +- **Telemetry**: Violation data feeds into cluster fleet analysis systems diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go new file mode 100644 index 0000000000..1031ff84d9 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -0,0 +1,133 @@ +package podsecurityreadinesscontroller + +import ( + "context" + "fmt" + "strings" + + securityv1 "github.com/openshift/api/security/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" + psapi "k8s.io/pod-security-admission/api" +) + +var ( + runLevelZeroNamespaces = sets.New[string]( + "default", + "kube-system", + "kube-public", + "kube-node-lease", + ) +) + +func (c *PodSecurityReadinessController) classifyViolatingNamespace(ctx context.Context, conditions *podSecurityOperatorConditions, ns *corev1.Namespace, enforceLevel string) error { + if runLevelZeroNamespaces.Has(ns.Name) { + conditions.addViolatingRunLevelZero(ns) + return nil + } + if strings.HasPrefix(ns.Name, "openshift") { + conditions.addViolatingOpenShift(ns) + return nil + } + if ns.Labels[labelSyncControlLabel] == "false" { + conditions.addViolatingDisabledSyncer(ns) + return nil + } + + // TODO@ibihim: increase log level + klog.InfoS("Checking for user violations", "namespace", ns.Name, "enforceLevel", enforceLevel) + isUserViolation, err := c.isUserViolation(ctx, ns, enforceLevel) + if err != nil { + klog.V(2).ErrorS(err, "Error checking user violations", "namespace", ns.Name) + // Transient API server error or temporary resource unavailability (most likely). + // Theoretically, psapi parsing errors could occur that retry without hope for recovery. + return err + } + + // TODO@ibihim: increase log level + klog.InfoS("User violation check result", "namespace", ns.Name, "isUserViolation", isUserViolation) + if isUserViolation { + // TODO@ibihim: increase log level + klog.InfoS("Adding namespace to user SCC violations", "namespace", ns.Name) + conditions.addViolatingUserSCC(ns) + return nil + } + + // Historically, we assume that this is a customer issue, but + // actually it means we don't know what the root cause is. + conditions.addViolatingCustomer(ns) + + return nil +} + +func (c *PodSecurityReadinessController) isUserViolation(ctx context.Context, ns *corev1.Namespace, label string) (bool, error) { + var enforcementLevel psapi.Level + switch strings.ToLower(label) { + case "restricted": + enforcementLevel = psapi.LevelRestricted + case "baseline": + enforcementLevel = psapi.LevelBaseline + case "privileged": + // If privileged is violating, something is seriously wrong + // but testing against privileged level is pointless (everything passes) + klog.V(2).InfoS("Namespace violating privileged level - skipping user check", + "namespace", ns.Name) + return false, nil + default: + return false, fmt.Errorf("unknown level: %q", label) + } + + allPods, err := c.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) + if err != nil { + klog.V(2).ErrorS(err, "Failed to list pods in namespace", "namespace", ns.Name) + return false, err + } + + var userPods []corev1.Pod + for _, pod := range allPods.Items { + // TODO@ibihim: we should exclude Pod that have restricted-v2. + // restricted-v2 SCCs are allowed for all system:authenticated. ServiceAccounts + // are able to use that, but they are not part of the group. So restricted-v2 + // will always result in user. + if pod.Annotations[securityv1.ValidatedSCCSubjectTypeAnnotation] == "user" { + userPods = append(userPods, pod) + } + } + + if len(userPods) == 0 { + return false, nil // No user pods = violation is from service accounts + } + + enforcementVersion := psapi.LatestVersion() + for _, pod := range userPods { + klog.InfoS("Evaluating user pod against PSA level", + "namespace", ns.Name, "pod", pod.Name, "level", label, + "podSecurityContext", pod.Spec.SecurityContext) + + results := c.psaEvaluator.EvaluatePod( + psapi.LevelVersion{Level: enforcementLevel, Version: enforcementVersion}, + &pod.ObjectMeta, + &pod.Spec, + ) + + klog.InfoS("PSA evaluation results", + "namespace", ns.Name, "pod", pod.Name, "level", label, + "resultCount", len(results)) + + for _, result := range results { + klog.InfoS("PSA evaluation result", + "namespace", ns.Name, "pod", pod.Name, "level", label, + "allowed", result.Allowed, "reason", result.ForbiddenReason, + "detail", result.ForbiddenDetail) + if !result.Allowed { + klog.InfoS("User pod violates PSA level", + "namespace", ns.Name, "pod", pod.Name, "level", label) + return true, nil + } + } + } + + return false, nil +} diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go new file mode 100644 index 0000000000..9c506bb949 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -0,0 +1,471 @@ +package podsecurityreadinesscontroller + +import ( + "context" + "fmt" + "slices" + "strings" + "testing" + + securityv1 "github.com/openshift/api/security/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" + "k8s.io/pod-security-admission/policy" +) + +func TestClassifyViolatingNamespaceWithAPIErrors(t *testing.T) { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "error-test-ns", + }, + } + + fakeClient := fake.NewSimpleClientset() + fakeClient.PrependReactor("list", "pods", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, fmt.Errorf("simulated API error: connection refused") + }) + + psaEvaluator, err := policy.NewEvaluator(policy.DefaultChecks()) + if err != nil { + t.Fatalf("Failed to create PSA evaluator: %v", err) + } + + controller := &PodSecurityReadinessController{ + kubeClient: fakeClient, + psaEvaluator: psaEvaluator, + } + + conditions := podSecurityOperatorConditions{} + + err = controller.classifyViolatingNamespace( + context.Background(), &conditions, + namespace, "restricted", + ) + + if err == nil { + t.Errorf("Expected error from API failure, got nil") + } + + if !strings.Contains(err.Error(), "simulated API error") { + t.Errorf("Expected API error, got: %v", err) + } + + // Ensure no classifications were made due to the error + if len(conditions.violatingCustomerNamespaces) != 0 || + len(conditions.violatingUserSCCNamespaces) != 0 || + len(conditions.violatingOpenShiftNamespaces) != 0 || + len(conditions.violatingRunLevelZeroNamespaces) != 0 || + len(conditions.violatingDisabledSyncerNamespaces) != 0 { + t.Errorf("Expected no classifications due to API error, but got: %+v", conditions) + } +} + +func TestClassifyViolatingNamespace(t *testing.T) { + for _, tt := range []struct { + name string + namespace *corev1.Namespace + pods []corev1.Pod + enforceLevel string + expectedConditions podSecurityOperatorConditions + expectError bool + }{ + { + name: "run-level zero namespace - kube-system", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingRunLevelZeroNamespaces: []string{"kube-system"}, + }, + expectError: false, + }, + { + name: "run-level zero namespace - default", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingRunLevelZeroNamespaces: []string{"default"}, + }, + expectError: false, + }, + { + name: "run-level zero namespace - kube-public", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-public", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingRunLevelZeroNamespaces: []string{"kube-public"}, + }, + expectError: false, + }, + { + name: "openshift namespace", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-test", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingOpenShiftNamespaces: []string{"openshift-test"}, + }, + expectError: false, + }, + { + name: "disabled syncer namespace", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disabled", + Labels: map[string]string{ + "security.openshift.io/scc.podSecurityLabelSync": "false", + }, + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingDisabledSyncerNamespaces: []string{"test-disabled"}, + }, + expectError: false, + }, + { + name: "customer namespace with user SCC violation", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newUserSCCPodPrivileged("user-pod", "customer-ns"), + }, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingUserSCCNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "user pod with privileged container - exact test manifest case", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-scc-violation-test", + }, + }, + pods: []corev1.Pod{ + newUserSCCPodWithPrivilegedContainer("user-scc-violating-pod", "user-scc-violation-test"), + }, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingUserSCCNamespaces: []string{"user-scc-violation-test"}, + }, + expectError: false, + }, + { + name: "customer namespace without user SCC violation", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newServiceAccountPod("sa-pod", "customer-ns"), + }, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingCustomerNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + // TODO: Ideally we would not drop the "customer" condition. + name: "customer namespace with mixed pods - user violates", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newServiceAccountPod("sa-pod", "customer-ns"), + newUserSCCPodPrivileged("user-pod", "customer-ns"), + }, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingUserSCCNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "customer namespace with user pods that pass PSA", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newUserSCCPodRestricted("user-pod", "customer-ns"), + }, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingCustomerNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "customer namespace with no pods", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingCustomerNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "customer namespace with pods without SCC annotation", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-without-annotation", + Namespace: "customer-ns", + // No SCC annotation + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "image", + }, + }, + }, + }, + }, + enforceLevel: "restricted", + expectedConditions: podSecurityOperatorConditions{ + violatingCustomerNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "namespace tested against privileged level", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newUserSCCPodPrivileged("user-pod", "customer-ns"), + }, + enforceLevel: "privileged", + expectedConditions: podSecurityOperatorConditions{ + violatingCustomerNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "invalid PSA level causes error", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: "invalid-level", + expectedConditions: podSecurityOperatorConditions{}, + expectError: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + controller, err := createTestController(tt.pods) + if err != nil { + t.Fatal(err) + } + + conditions := podSecurityOperatorConditions{} + err = controller.classifyViolatingNamespace( + context.Background(), &conditions, + tt.namespace, tt.enforceLevel, + ) + if hasError := err != nil; hasError != tt.expectError { + t.Errorf("classifyViolatingNamespace() error = %v, expectError %v", err, tt.expectError) + return + } + if err != nil { + return + } + + if !deepEqualPodSecurityOperatorConditions(&conditions, &tt.expectedConditions) { + t.Errorf("Conditions mismatch.\nHave: %+v\nWant: %+v", conditions, tt.expectedConditions) + } + }) + } +} + +func createTestController(pods []corev1.Pod) (*PodSecurityReadinessController, error) { + fakeClient := fake.NewSimpleClientset() + + for _, pod := range pods { + _, err := fakeClient.CoreV1(). + Pods(pod.Namespace). + Create(context.Background(), &pod, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("Failed to create test pod: %v", err) + } + } + + psaEvaluator, err := policy.NewEvaluator(policy.DefaultChecks()) + if err != nil { + return nil, fmt.Errorf("Failed to create PSA evaluator: %v", err) + } + + return &PodSecurityReadinessController{ + kubeClient: fakeClient, + psaEvaluator: psaEvaluator, + }, nil +} + +func newUserSCCPodPrivileged(name, namespace string) corev1.Pod { + return corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + securityv1.ValidatedSCCSubjectTypeAnnotation: "user", + }, + }, + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{false}[0], + }, + Containers: []corev1.Container{ + { + Name: "container", + Image: "image", + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &[]bool{true}[0], + }, + }, + }, + }, + } +} + +func newServiceAccountPod(name, namespace string) corev1.Pod { + return corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + securityv1.ValidatedSCCSubjectTypeAnnotation: "service-account", + }, + }, + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{false}[0], + }, + }, + } +} + +func newUserSCCPodWithPrivilegedContainer(name, namespace string) corev1.Pod { + return corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + securityv1.ValidatedSCCSubjectTypeAnnotation: "user", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "busybox:latest", + Command: []string{"sleep", "infinity"}, + SecurityContext: &corev1.SecurityContext{ + Privileged: &[]bool{true}[0], + RunAsNonRoot: &[]bool{false}[0], + RunAsUser: &[]int64{0}[0], + }, + }, + }, + RestartPolicy: corev1.RestartPolicyNever, + }, + } +} + +func newUserSCCPodRestricted(name, namespace string) corev1.Pod { + return corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + securityv1.ValidatedSCCSubjectTypeAnnotation: "user", + }, + }, + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1000}[0], + SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}, + FSGroup: &[]int64{1000}[0], + }, + Containers: []corev1.Container{ + { + Name: "container", + Image: "image", + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: &[]bool{true}[0], + }, + }, + }, + }, + } +} + +func deepEqualPodSecurityOperatorConditions( + a, b *podSecurityOperatorConditions, +) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + return slices.Equal(a.violatingOpenShiftNamespaces, b.violatingOpenShiftNamespaces) && + slices.Equal(a.violatingRunLevelZeroNamespaces, b.violatingRunLevelZeroNamespaces) && + slices.Equal(a.violatingCustomerNamespaces, b.violatingCustomerNamespaces) && + slices.Equal(a.violatingDisabledSyncerNamespaces, b.violatingDisabledSyncerNamespaces) && + slices.Equal(a.violatingUserSCCNamespaces, b.violatingUserSCCNamespaces) && + slices.Equal(a.inconclusiveNamespaces, b.inconclusiveNamespaces) +} diff --git a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go index 4e2ac7541b..6988ba335e 100644 --- a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go +++ b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go @@ -13,6 +13,7 @@ import ( "k8s.io/client-go/util/retry" "k8s.io/klog/v2" psapi "k8s.io/pod-security-admission/api" + "k8s.io/pod-security-admission/policy" "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/events" @@ -30,6 +31,7 @@ type PodSecurityReadinessController struct { warningsHandler *warningsHandler namespaceSelector string + psaEvaluator policy.Evaluator } func NewPodSecurityReadinessController( @@ -49,11 +51,17 @@ func NewPodSecurityReadinessController( return nil, err } + psaEvaluator, err := policy.NewEvaluator(policy.DefaultChecks()) + if err != nil { + return nil, err + } + c := &PodSecurityReadinessController{ operatorClient: operatorClient, kubeClient: kubeClient, warningsHandler: warningsHandler, namespaceSelector: selector, + psaEvaluator: psaEvaluator, } return factory.New(). @@ -62,7 +70,7 @@ func NewPodSecurityReadinessController( ToController("PodSecurityReadinessController", recorder), nil } -func (c *PodSecurityReadinessController) sync(ctx context.Context, syncCtx factory.SyncContext) error { +func (c *PodSecurityReadinessController) sync(ctx context.Context, _ factory.SyncContext) error { nsList, err := c.kubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{LabelSelector: c.namespaceSelector}) if err != nil { return err @@ -71,18 +79,18 @@ func (c *PodSecurityReadinessController) sync(ctx context.Context, syncCtx facto conditions := podSecurityOperatorConditions{} for _, ns := range nsList.Items { err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - isViolating, _, err := c.isNamespaceViolating(ctx, &ns) + isViolating, enforceLevel, err := c.isNamespaceViolating(ctx, &ns) if apierrors.IsNotFound(err) { return nil } if err != nil { return err } - if isViolating { - conditions.addViolation(&ns) + if !isViolating { + return nil } - return nil + return c.classifyViolatingNamespace(ctx, &conditions, &ns, enforceLevel) }) if err != nil { klog.V(2).ErrorS(err, "namespace:", ns.Name) From 10fcc51d54627bb03efd533c834906ad2d4af902 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Wed, 30 Jul 2025 19:45:56 +0200 Subject: [PATCH 03/15] podsecurityreadinesscontroller: wire classification, simplify conditions --- .../conditions.go | 47 ++++++++----------- .../conditions_test.go | 21 ++++++++- .../podsecurityreadinesscontroller_test.go | 10 ++-- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions.go b/pkg/operator/podsecurityreadinesscontroller/conditions.go index 7d8f09f536..2c9a352feb 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions.go @@ -3,11 +3,9 @@ package podsecurityreadinesscontroller import ( "fmt" "sort" - "strings" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" operatorv1 "github.com/openshift/api/operator/v1" "github.com/openshift/library-go/pkg/operator/v1helpers" @@ -19,6 +17,7 @@ const ( PodSecurityRunLevelZeroType = "PodSecurityRunLevelZeroEvaluationConditionsDetected" PodSecurityDisabledSyncerType = "PodSecurityDisabledSyncerEvaluationConditionsDetected" PodSecurityInconclusiveType = "PodSecurityInconclusiveEvaluationConditionsDetected" + PodSecurityUserSCCType = "PodSecurityUserSCCEvaluationConditionsDetected" labelSyncControlLabel = "security.openshift.io/scc.podSecurityLabelSync" @@ -26,46 +25,37 @@ const ( inconclusiveReason = "PSViolationDecisionInconclusive" ) -var ( - // run-level zero namespaces, shouldn't avoid openshift namespaces - runLevelZeroNamespaces = sets.New[string]( - "default", - "kube-system", - "kube-public", - ) -) - type podSecurityOperatorConditions struct { violatingOpenShiftNamespaces []string violatingRunLevelZeroNamespaces []string violatingCustomerNamespaces []string violatingDisabledSyncerNamespaces []string + violatingUserSCCNamespaces []string inconclusiveNamespaces []string } -func (c *podSecurityOperatorConditions) addViolation(ns *corev1.Namespace) { - if runLevelZeroNamespaces.Has(ns.Name) { - c.violatingRunLevelZeroNamespaces = append(c.violatingRunLevelZeroNamespaces, ns.Name) - return - } +func (c *podSecurityOperatorConditions) addInconclusive(ns *corev1.Namespace) { + c.inconclusiveNamespaces = append(c.inconclusiveNamespaces, ns.Name) +} - isOpenShift := strings.HasPrefix(ns.Name, "openshift") - if isOpenShift { - c.violatingOpenShiftNamespaces = append(c.violatingOpenShiftNamespaces, ns.Name) - return - } +func (c *podSecurityOperatorConditions) addViolatingRunLevelZero(ns *corev1.Namespace) { + c.violatingRunLevelZeroNamespaces = append(c.violatingRunLevelZeroNamespaces, ns.Name) +} - if ns.Labels[labelSyncControlLabel] == "false" { - // This is the only case in which the controller wouldn't enforce the pod security standards. - c.violatingDisabledSyncerNamespaces = append(c.violatingDisabledSyncerNamespaces, ns.Name) - return - } +func (c *podSecurityOperatorConditions) addViolatingOpenShift(ns *corev1.Namespace) { + c.violatingOpenShiftNamespaces = append(c.violatingOpenShiftNamespaces, ns.Name) +} +func (c *podSecurityOperatorConditions) addViolatingDisabledSyncer(ns *corev1.Namespace) { + c.violatingDisabledSyncerNamespaces = append(c.violatingDisabledSyncerNamespaces, ns.Name) +} + +func (c *podSecurityOperatorConditions) addViolatingCustomer(ns *corev1.Namespace) { c.violatingCustomerNamespaces = append(c.violatingCustomerNamespaces, ns.Name) } -func (c *podSecurityOperatorConditions) addInconclusive(ns *corev1.Namespace) { - c.inconclusiveNamespaces = append(c.inconclusiveNamespaces, ns.Name) +func (c *podSecurityOperatorConditions) addViolatingUserSCC(ns *corev1.Namespace) { + c.violatingUserSCCNamespaces = append(c.violatingUserSCCNamespaces, ns.Name) } func makeCondition(conditionType, conditionReason string, namespaces []string) operatorv1.OperatorCondition { @@ -108,6 +98,7 @@ func (c *podSecurityOperatorConditions) toConditionFuncs() []v1helpers.UpdateSta v1helpers.UpdateConditionFn(makeCondition(PodSecurityOpenshiftType, violationReason, c.violatingOpenShiftNamespaces)), v1helpers.UpdateConditionFn(makeCondition(PodSecurityRunLevelZeroType, violationReason, c.violatingRunLevelZeroNamespaces)), v1helpers.UpdateConditionFn(makeCondition(PodSecurityDisabledSyncerType, violationReason, c.violatingDisabledSyncerNamespaces)), + v1helpers.UpdateConditionFn(makeCondition(PodSecurityUserSCCType, violationReason, c.violatingUserSCCNamespaces)), v1helpers.UpdateConditionFn(makeCondition(PodSecurityInconclusiveType, inconclusiveReason, c.inconclusiveNamespaces)), } } diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go index d36bc8ffc9..e954002c78 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go @@ -1,6 +1,7 @@ package podsecurityreadinesscontroller import ( + "strings" "testing" operatorv1 "github.com/openshift/api/operator/v1" @@ -117,6 +118,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -138,6 +140,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionTrue, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -159,6 +162,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -177,6 +181,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -195,6 +200,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -221,6 +227,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionTrue, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -251,6 +258,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -270,6 +278,7 @@ func TestOperatorStatus(t *testing.T) { "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionTrue, }, }, @@ -280,7 +289,17 @@ func TestOperatorStatus(t *testing.T) { for _, ns := range tt.namespace { if tt.addViolation { - cond.addViolation(ns) + // Classify namespace based on the same logic as classifyViolatingNamespace + if runLevelZeroNamespaces.Has(ns.Name) { + cond.addViolatingRunLevelZero(ns) + } else if strings.HasPrefix(ns.Name, "openshift") { + cond.addViolatingOpenShift(ns) + } else if ns.Labels[labelSyncControlLabel] == "false" { + cond.addViolatingDisabledSyncer(ns) + } else { + // Default to customer violation for test purposes + cond.addViolatingCustomer(ns) + } } if tt.addInconclusive { cond.addInconclusive(ns) diff --git a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go index 8c70e7b1e7..59e8554df9 100644 --- a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go @@ -315,8 +315,8 @@ func TestPodSecurityViolationController(t *testing.T) { }, }, expectedViolation: false, - expectedEnforceLabel: "", - expectedError: true, + expectedEnforceLabel: "restricted", + expectedError: false, }, { name: "error against inconclusive namespace", @@ -328,8 +328,8 @@ func TestPodSecurityViolationController(t *testing.T) { }, }, expectedViolation: false, - expectedEnforceLabel: "", - expectedError: true, + expectedEnforceLabel: "restricted", + expectedError: false, }, } { tt := tt @@ -357,7 +357,7 @@ func TestPodSecurityViolationController(t *testing.T) { }, } - isViolating, err := controller.isNamespaceViolating(context.TODO(), tt.namespace) + isViolating, _, err := controller.isNamespaceViolating(context.TODO(), tt.namespace) if (err != nil) != tt.expectedError { t.Errorf("expected error %v, got %v", tt.expectedError, err) } From 8b0d2bb974d3ee953826e89db8aeb178a63c83a6 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Wed, 30 Jul 2025 20:48:06 +0200 Subject: [PATCH 04/15] podsecurityreadinesscontroller: ignore restricted SCCs, drop debugging logs --- .../classification.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go index 1031ff84d9..74f24698eb 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -36,8 +36,6 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace(ctx context. return nil } - // TODO@ibihim: increase log level - klog.InfoS("Checking for user violations", "namespace", ns.Name, "enforceLevel", enforceLevel) isUserViolation, err := c.isUserViolation(ctx, ns, enforceLevel) if err != nil { klog.V(2).ErrorS(err, "Error checking user violations", "namespace", ns.Name) @@ -46,11 +44,7 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace(ctx context. return err } - // TODO@ibihim: increase log level - klog.InfoS("User violation check result", "namespace", ns.Name, "isUserViolation", isUserViolation) if isUserViolation { - // TODO@ibihim: increase log level - klog.InfoS("Adding namespace to user SCC violations", "namespace", ns.Name) conditions.addViolatingUserSCC(ns) return nil } @@ -87,10 +81,16 @@ func (c *PodSecurityReadinessController) isUserViolation(ctx context.Context, ns var userPods []corev1.Pod for _, pod := range allPods.Items { - // TODO@ibihim: we should exclude Pod that have restricted-v2. - // restricted-v2 SCCs are allowed for all system:authenticated. ServiceAccounts - // are able to use that, but they are not part of the group. So restricted-v2 - // will always result in user. + if strings.HasPrefix(pod.Annotations[securityv1.ValidatedSCCAnnotation], "restricted-v") { + // restricted-v2 is allowed for all system:authenticated, also for ServiceAccounts. + // But ServiceAccounts are not part of the group. So restricted-v2 will always + // result in user-based SCC. So we skip them as the user-based SCCs cause harm + // if they need a higher privileged than restricted. + // We watch for any restricted version above the first one. We might introduce + // restricted-v3 for user namespaces. + continue + } + if pod.Annotations[securityv1.ValidatedSCCSubjectTypeAnnotation] == "user" { userPods = append(userPods, pod) } From 465832d535299f714173357807876200c78b6bfc Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Wed, 30 Jul 2025 21:26:28 +0200 Subject: [PATCH 05/15] go mod vendor --- .../policy/check_allowPrivilegeEscalation.go | 93 +++++++ .../policy/check_appArmorProfile.go | 143 +++++++++++ .../policy/check_capabilities_baseline.go | 110 +++++++++ .../policy/check_capabilities_restricted.go | 145 +++++++++++ .../policy/check_hostNamespaces.go | 82 +++++++ .../policy/check_hostPathVolumes.go | 76 ++++++ .../policy/check_hostPorts.go | 91 +++++++ .../policy/check_privileged.go | 75 ++++++ .../policy/check_procMount.go | 102 ++++++++ .../policy/check_restrictedVolumes.go | 173 ++++++++++++++ .../policy/check_runAsNonRoot.go | 133 +++++++++++ .../policy/check_runAsUser.go | 104 ++++++++ .../policy/check_seLinuxOptions.go | 172 +++++++++++++ .../policy/check_seccompProfile_baseline.go | 171 +++++++++++++ .../policy/check_seccompProfile_restricted.go | 155 ++++++++++++ .../policy/check_sysctls.go | 143 +++++++++++ .../policy/check_windowsHostProcess.go | 102 ++++++++ .../pod-security-admission/policy/checks.go | 184 ++++++++++++++ .../pod-security-admission/policy/doc.go | 18 ++ .../pod-security-admission/policy/helpers.go | 56 +++++ .../pod-security-admission/policy/registry.go | 226 ++++++++++++++++++ .../pod-security-admission/policy/visitor.go | 37 +++ vendor/modules.txt | 1 + 23 files changed, 2592 insertions(+) create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_allowPrivilegeEscalation.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_appArmorProfile.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_capabilities_baseline.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_capabilities_restricted.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_hostNamespaces.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_hostPathVolumes.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_hostPorts.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_privileged.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_procMount.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_restrictedVolumes.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_runAsUser.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_seLinuxOptions.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_sysctls.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/check_windowsHostProcess.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/checks.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/doc.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/helpers.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/registry.go create mode 100644 vendor/k8s.io/pod-security-admission/policy/visitor.go diff --git a/vendor/k8s.io/pod-security-admission/policy/check_allowPrivilegeEscalation.go b/vendor/k8s.io/pod-security-admission/policy/check_allowPrivilegeEscalation.go new file mode 100644 index 0000000000..d531612a1e --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_allowPrivilegeEscalation.go @@ -0,0 +1,93 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +Privilege escalation (such as via set-user-ID or set-group-ID file mode) should not be allowed. + +**Restricted Fields:** + +spec.containers[*].securityContext.allowPrivilegeEscalation +spec.initContainers[*].securityContext.allowPrivilegeEscalation + +**Allowed Values:** false +*/ + +func init() { + addCheck(CheckAllowPrivilegeEscalation) +} + +// CheckAllowPrivilegeEscalation returns a restricted level check +// that requires allowPrivilegeEscalation=false in 1.8+ +func CheckAllowPrivilegeEscalation() Check { + return Check{ + ID: "allowPrivilegeEscalation", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + // Field added in 1.8: + // https://github.com/kubernetes/kubernetes/blob/v1.8.0/staging/src/k8s.io/api/core/v1/types.go#L4797-L4804 + MinimumVersion: api.MajorMinorVersion(1, 8), + CheckPod: allowPrivilegeEscalation_1_8, + }, + { + // Starting 1.25, windows pods would be exempted from this check using pod.spec.os field when set to windows. + MinimumVersion: api.MajorMinorVersion(1, 25), + CheckPod: allowPrivilegeEscalation_1_25, + }, + }, + } +} + +func allowPrivilegeEscalation_1_8(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badContainers []string + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext == nil || container.SecurityContext.AllowPrivilegeEscalation == nil || *container.SecurityContext.AllowPrivilegeEscalation { + badContainers = append(badContainers, container.Name) + } + }) + + if len(badContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "allowPrivilegeEscalation != false", + ForbiddenDetail: fmt.Sprintf( + "%s %s must set securityContext.allowPrivilegeEscalation=false", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + ), + } + } + return CheckResult{Allowed: true} +} + +func allowPrivilegeEscalation_1_25(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // Pod API validation would have failed if podOS == Windows and if privilegeEscalation has been set. + // We can admit the Windows pod even if privilegeEscalation has not been set. + if podSpec.OS != nil && podSpec.OS.Name == corev1.Windows { + return CheckResult{Allowed: true} + } + return allowPrivilegeEscalation_1_8(podMetadata, podSpec) +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_appArmorProfile.go b/vendor/k8s.io/pod-security-admission/policy/check_appArmorProfile.go new file mode 100644 index 0000000000..1279403116 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_appArmorProfile.go @@ -0,0 +1,143 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "sort" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* +On supported hosts, the 'runtime/default' AppArmor profile is applied by default. +The baseline policy should prevent overriding or disabling the default AppArmor +profile, or restrict overrides to an allowed set of profiles. + +**Restricted Fields:** +metadata.annotations['container.apparmor.security.beta.kubernetes.io/*'] + +**Allowed Values:** 'runtime/default', 'localhost/*', empty, undefined + +**Restricted Fields:** +spec.securityContext.appArmorProfile.type +spec.containers[*].securityContext.appArmorProfile.type +spec.initContainers[*].securityContext.appArmorProfile.type +spec.ephemeralContainers[*].securityContext.appArmorProfile.type + +**Allowed Values:** 'RuntimeDefault', 'Localhost', undefined +*/ +func init() { + addCheck(CheckAppArmorProfile) +} + +// CheckAppArmorProfile returns a baseline level check +// that limits the value of AppArmor profiles in 1.0+ +func CheckAppArmorProfile() Check { + return Check{ + ID: "appArmorProfile", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: appArmorProfile_1_0, + }, + }, + } +} + +func allowedAnnotationValue(profile string) bool { + return len(profile) == 0 || + profile == corev1.DeprecatedAppArmorBetaProfileRuntimeDefault || + strings.HasPrefix(profile, corev1.DeprecatedAppArmorBetaProfileNamePrefix) +} + +func allowedProfileType(profile corev1.AppArmorProfileType) bool { + switch profile { + case corev1.AppArmorProfileTypeRuntimeDefault, + corev1.AppArmorProfileTypeLocalhost: + return true + default: + return false + } +} + +func appArmorProfile_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badSetters []string // things that explicitly set appArmorProfile.type to a bad value + badValues := sets.NewString() + + if podSpec.SecurityContext != nil && podSpec.SecurityContext.AppArmorProfile != nil { + if !allowedProfileType(podSpec.SecurityContext.AppArmorProfile.Type) { + badSetters = append(badSetters, "pod") + badValues.Insert(string(podSpec.SecurityContext.AppArmorProfile.Type)) + } + } + + var badContainers []string // containers that set apparmorProfile.type to a bad value + visitContainers(podSpec, func(c *corev1.Container) { + if c.SecurityContext != nil && c.SecurityContext.AppArmorProfile != nil { + if !allowedProfileType(c.SecurityContext.AppArmorProfile.Type) { + badContainers = append(badContainers, c.Name) + badValues.Insert(string(c.SecurityContext.AppArmorProfile.Type)) + } + } + }) + + if len(badContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + ), + ) + } + + var forbiddenAnnotations []string + for k, v := range podMetadata.Annotations { + if strings.HasPrefix(k, corev1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix) && !allowedAnnotationValue(v) { + forbiddenAnnotations = append(forbiddenAnnotations, fmt.Sprintf("%s=%q", k, v)) + } + } + + badValueList := badValues.List() + if len(forbiddenAnnotations) > 0 { + sort.Strings(forbiddenAnnotations) + badValueList = append(badValueList, forbiddenAnnotations...) + badSetters = append(badSetters, pluralize("annotation", "annotations", len(forbiddenAnnotations))) + } + + // pod or containers explicitly set bad apparmorProfiles + if len(badSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: pluralize("forbidden AppArmor profile", "forbidden AppArmor profiles", len(badValueList)), + ForbiddenDetail: fmt.Sprintf( + "%s must not set AppArmor profile type to %s", + strings.Join(badSetters, " and "), + joinQuote(badValueList), + ), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_capabilities_baseline.go b/vendor/k8s.io/pod-security-admission/policy/check_capabilities_baseline.go new file mode 100644 index 0000000000..aad61738b5 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_capabilities_baseline.go @@ -0,0 +1,110 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* +Adding NET_RAW or capabilities beyond the default set must be disallowed. + +**Restricted Fields:** +spec.containers[*].securityContext.capabilities.add +spec.initContainers[*].securityContext.capabilities.add + +**Allowed Values:** +undefined / empty +values from the default set "AUDIT_WRITE", "CHOWN", "DAC_OVERRIDE","FOWNER", "FSETID", "KILL", "MKNOD", "NET_BIND_SERVICE", "SETFCAP", "SETGID", "SETPCAP", "SETUID", "SYS_CHROOT" +*/ + +func init() { + addCheck(CheckCapabilitiesBaseline) +} + +const checkCapabilitiesBaselineID CheckID = "capabilities_baseline" + +// CheckCapabilitiesBaseline returns a baseline level check +// that limits the capabilities that can be added in 1.0+ +func CheckCapabilitiesBaseline() Check { + return Check{ + ID: checkCapabilitiesBaselineID, + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: capabilitiesBaseline_1_0, + }, + }, + } +} + +var ( + capabilities_allowed_1_0 = sets.NewString( + "AUDIT_WRITE", + "CHOWN", + "DAC_OVERRIDE", + "FOWNER", + "FSETID", + "KILL", + "MKNOD", + "NET_BIND_SERVICE", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_CHROOT", + ) +) + +func capabilitiesBaseline_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badContainers []string + nonDefaultCapabilities := sets.NewString() + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext != nil && container.SecurityContext.Capabilities != nil { + valid := true + for _, c := range container.SecurityContext.Capabilities.Add { + if !capabilities_allowed_1_0.Has(string(c)) { + valid = false + nonDefaultCapabilities.Insert(string(c)) + } + } + if !valid { + badContainers = append(badContainers, container.Name) + } + } + }) + + if len(badContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "non-default capabilities", + ForbiddenDetail: fmt.Sprintf( + "%s %s must not include %s in securityContext.capabilities.add", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + joinQuote(nonDefaultCapabilities.List()), + ), + } + } + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_capabilities_restricted.go b/vendor/k8s.io/pod-security-admission/policy/check_capabilities_restricted.go new file mode 100644 index 0000000000..9d70b0304a --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_capabilities_restricted.go @@ -0,0 +1,145 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +const ( + capabilityAll = "ALL" + capabilityNetBindService = "NET_BIND_SERVICE" +) + +/* +Containers must drop ALL, and may only add NET_BIND_SERVICE. + +**Restricted Fields:** +spec.containers[*].securityContext.capabilities.drop +spec.initContainers[*].securityContext.capabilities.drop + +**Allowed Values:** +Must include "ALL" + +**Restricted Fields:** +spec.containers[*].securityContext.capabilities.add +spec.initContainers[*].securityContext.capabilities.add + +**Allowed Values:** +undefined / empty +"NET_BIND_SERVICE" +*/ + +func init() { + addCheck(CheckCapabilitiesRestricted) +} + +// CheckCapabilitiesRestricted returns a restricted level check +// that ensures ALL capabilities are dropped in 1.22+ +func CheckCapabilitiesRestricted() Check { + return Check{ + ID: "capabilities_restricted", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 22), + CheckPod: capabilitiesRestricted_1_22, + OverrideCheckIDs: []CheckID{checkCapabilitiesBaselineID}, + }, + // Starting 1.25, windows pods would be exempted from this check using pod.spec.os field when set to windows. + { + MinimumVersion: api.MajorMinorVersion(1, 25), + CheckPod: capabilitiesRestricted_1_25, + OverrideCheckIDs: []CheckID{checkCapabilitiesBaselineID}, + }, + }, + } +} + +func capabilitiesRestricted_1_22(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var ( + containersMissingDropAll []string + containersAddingForbidden []string + forbiddenCapabilities = sets.NewString() + ) + + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext == nil || container.SecurityContext.Capabilities == nil { + containersMissingDropAll = append(containersMissingDropAll, container.Name) + return + } + + droppedAll := false + for _, c := range container.SecurityContext.Capabilities.Drop { + if c == capabilityAll { + droppedAll = true + break + } + } + if !droppedAll { + containersMissingDropAll = append(containersMissingDropAll, container.Name) + } + + addedForbidden := false + for _, c := range container.SecurityContext.Capabilities.Add { + if c != capabilityNetBindService { + addedForbidden = true + forbiddenCapabilities.Insert(string(c)) + } + } + if addedForbidden { + containersAddingForbidden = append(containersAddingForbidden, container.Name) + } + }) + var forbiddenDetails []string + if len(containersMissingDropAll) > 0 { + forbiddenDetails = append(forbiddenDetails, fmt.Sprintf( + `%s %s must set securityContext.capabilities.drop=["ALL"]`, + pluralize("container", "containers", len(containersMissingDropAll)), + joinQuote(containersMissingDropAll))) + } + if len(containersAddingForbidden) > 0 { + forbiddenDetails = append(forbiddenDetails, fmt.Sprintf( + `%s %s must not include %s in securityContext.capabilities.add`, + pluralize("container", "containers", len(containersAddingForbidden)), + joinQuote(containersAddingForbidden), + joinQuote(forbiddenCapabilities.List()))) + } + if len(forbiddenDetails) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "unrestricted capabilities", + ForbiddenDetail: strings.Join(forbiddenDetails, "; "), + } + } + return CheckResult{Allowed: true} +} + +func capabilitiesRestricted_1_25(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // Pod API validation would have failed if podOS == Windows and if capabilities have been set. + // We can admit the Windows pod even if capabilities has not been set. + if podSpec.OS != nil && podSpec.OS.Name == corev1.Windows { + return CheckResult{Allowed: true} + } + return capabilitiesRestricted_1_22(podMetadata, podSpec) +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_hostNamespaces.go b/vendor/k8s.io/pod-security-admission/policy/check_hostNamespaces.go new file mode 100644 index 0000000000..9092b64e92 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_hostNamespaces.go @@ -0,0 +1,82 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +Sharing the host network, PID, and IPC namespaces must be disallowed. + +**Restricted Fields:** + +spec.hostNetwork +spec.hostPID +spec.hostIPC + +**Allowed Values:** undefined, false +*/ + +func init() { + addCheck(CheckHostNamespaces) +} + +// CheckHostNamespaces returns a baseline level check +// that prohibits host namespaces in 1.0+ +func CheckHostNamespaces() Check { + return Check{ + ID: "hostNamespaces", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: hostNamespaces_1_0, + }, + }, + } +} + +func hostNamespaces_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var hostNamespaces []string + + if podSpec.HostNetwork { + hostNamespaces = append(hostNamespaces, "hostNetwork=true") + } + + if podSpec.HostPID { + hostNamespaces = append(hostNamespaces, "hostPID=true") + } + + if podSpec.HostIPC { + hostNamespaces = append(hostNamespaces, "hostIPC=true") + } + + if len(hostNamespaces) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "host namespaces", + ForbiddenDetail: strings.Join(hostNamespaces, ", "), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_hostPathVolumes.go b/vendor/k8s.io/pod-security-admission/policy/check_hostPathVolumes.go new file mode 100644 index 0000000000..3a419ff249 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_hostPathVolumes.go @@ -0,0 +1,76 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +HostPath volumes must be forbidden. + +**Restricted Fields:** + +spec.volumes[*].hostPath + +**Allowed Values:** undefined/null +*/ + +func init() { + addCheck(CheckHostPathVolumes) +} + +const checkHostPathVolumesID CheckID = "hostPathVolumes" + +// CheckHostPathVolumes returns a baseline level check +// that requires hostPath=undefined/null in 1.0+ +func CheckHostPathVolumes() Check { + return Check{ + ID: checkHostPathVolumesID, + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: hostPathVolumes_1_0, + }, + }, + } +} + +func hostPathVolumes_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var hostVolumes []string + + for _, volume := range podSpec.Volumes { + if volume.HostPath != nil { + hostVolumes = append(hostVolumes, volume.Name) + } + } + + if len(hostVolumes) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "hostPath volumes", + ForbiddenDetail: fmt.Sprintf("%s %s", pluralize("volume", "volumes", len(hostVolumes)), joinQuote(hostVolumes)), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_hostPorts.go b/vendor/k8s.io/pod-security-admission/policy/check_hostPorts.go new file mode 100644 index 0000000000..9e598cde8f --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_hostPorts.go @@ -0,0 +1,91 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* +HostPort ports must be forbidden. + +**Restricted Fields:** + +spec.containers[*].ports[*].hostPort +spec.initContainers[*].ports[*].hostPort + +**Allowed Values:** undefined/0 +*/ + +func init() { + addCheck(CheckHostPorts) +} + +// CheckHostPorts returns a baseline level check +// that forbids any host ports in 1.0+ +func CheckHostPorts() Check { + return Check{ + ID: "hostPorts", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: hostPorts_1_0, + }, + }, + } +} + +func hostPorts_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badContainers []string + forbiddenHostPorts := sets.NewString() + visitContainers(podSpec, func(container *corev1.Container) { + valid := true + for _, c := range container.Ports { + if c.HostPort != 0 { + valid = false + forbiddenHostPorts.Insert(strconv.Itoa(int(c.HostPort))) + } + } + if !valid { + badContainers = append(badContainers, container.Name) + } + }) + + if len(badContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "hostPort", + ForbiddenDetail: fmt.Sprintf( + "%s %s %s %s %s", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + pluralize("uses", "use", len(badContainers)), + pluralize("hostPort", "hostPorts", len(forbiddenHostPorts)), + strings.Join(forbiddenHostPorts.List(), ", "), + ), + } + } + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_privileged.go b/vendor/k8s.io/pod-security-admission/policy/check_privileged.go new file mode 100644 index 0000000000..899642e4cd --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_privileged.go @@ -0,0 +1,75 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +Privileged Pods disable most security mechanisms and must be disallowed. + +Restricted Fields: +spec.containers[*].securityContext.privileged +spec.initContainers[*].securityContext.privileged + +Allowed Values: false, undefined/null +*/ + +func init() { + addCheck(CheckPrivileged) +} + +// CheckPrivileged returns a baseline level check +// that forbids privileged=true in 1.0+ +func CheckPrivileged() Check { + return Check{ + ID: "privileged", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: privileged_1_0, + }, + }, + } +} + +func privileged_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badContainers []string + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged { + badContainers = append(badContainers, container.Name) + } + }) + if len(badContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "privileged", + ForbiddenDetail: fmt.Sprintf( + `%s %s must not set securityContext.privileged=true`, + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + ), + } + } + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_procMount.go b/vendor/k8s.io/pod-security-admission/policy/check_procMount.go new file mode 100644 index 0000000000..8ec585b9c9 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_procMount.go @@ -0,0 +1,102 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* + +The default /proc masks are set up to reduce attack surface, and should be required. + +**Restricted Fields:** +spec.containers[*].securityContext.procMount +spec.initContainers[*].securityContext.procMount + +**Allowed Values:** undefined/null, "Default" + +However, if the pod is in a user namespace (`hostUsers: false`), and the +UserNamespacesPodSecurityStandards feature is enabled, all values are allowed. + +*/ + +func init() { + addCheck(CheckProcMount) +} + +// CheckProcMount returns a baseline level check that restricts +// setting the value of securityContext.procMount to DefaultProcMount +// in 1.0+ +func CheckProcMount() Check { + return Check{ + ID: "procMount", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: procMount_1_0, + }, + }, + } +} + +func procMount_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // TODO: When we remove the UserNamespacesPodSecurityStandards feature gate (and GA this relaxation), + // create a new policy version. + // Note: pod validation will check for well formed procMount type, so avoid double validation and allow everything + // here. + if relaxPolicyForUserNamespacePod(podSpec) { + return CheckResult{Allowed: true} + } + + var badContainers []string + forbiddenProcMountTypes := sets.NewString() + visitContainers(podSpec, func(container *corev1.Container) { + // allow if the security context is nil. + if container.SecurityContext == nil { + return + } + // allow if proc mount is not set. + if container.SecurityContext.ProcMount == nil { + return + } + // check if the value of the proc mount type is valid. + if *container.SecurityContext.ProcMount != corev1.DefaultProcMount { + badContainers = append(badContainers, container.Name) + forbiddenProcMountTypes.Insert(string(*container.SecurityContext.ProcMount)) + } + }) + if len(badContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "procMount", + ForbiddenDetail: fmt.Sprintf( + "%s %s must not set securityContext.procMount to %s", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + joinQuote(forbiddenProcMountTypes.List()), + ), + } + } + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_restrictedVolumes.go b/vendor/k8s.io/pod-security-admission/policy/check_restrictedVolumes.go new file mode 100644 index 0000000000..06ae4890c9 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_restrictedVolumes.go @@ -0,0 +1,173 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* +In addition to restricting HostPath volumes, the restricted profile +limits usage of inline pod volume sources to: +* configMap +* downwardAPI +* emptyDir +* projected +* secret +* csi +* persistentVolumeClaim +* ephemeral +* image + +**Restricted Fields:** + +spec.volumes[*].hostPath +spec.volumes[*].gcePersistentDisk +spec.volumes[*].awsElasticBlockStore +spec.volumes[*].gitRepo +spec.volumes[*].nfs +spec.volumes[*].iscsi +spec.volumes[*].glusterfs +spec.volumes[*].rbd +spec.volumes[*].flexVolume +spec.volumes[*].cinder +spec.volumes[*].cephfs +spec.volumes[*].flocker +spec.volumes[*].fc +spec.volumes[*].azureFile +spec.volumes[*].vsphereVolume +spec.volumes[*].quobyte +spec.volumes[*].azureDisk +spec.volumes[*].portworxVolume +spec.volumes[*].photonPersistentDisk +spec.volumes[*].scaleIO +spec.volumes[*].storageos + +**Allowed Values:** undefined/null +*/ + +func init() { + addCheck(CheckRestrictedVolumes) +} + +// CheckRestrictedVolumes returns a restricted level check +// that limits usage of specific volume types in 1.0+ +func CheckRestrictedVolumes() Check { + return Check{ + ID: "restrictedVolumes", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: restrictedVolumes_1_0, + OverrideCheckIDs: []CheckID{checkHostPathVolumesID}, + }, + }, + } +} + +func restrictedVolumes_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badVolumes []string + badVolumeTypes := sets.NewString() + + for _, volume := range podSpec.Volumes { + switch { + case volume.ConfigMap != nil, + volume.CSI != nil, + volume.DownwardAPI != nil, + volume.EmptyDir != nil, + volume.Ephemeral != nil, + volume.Image != nil, + volume.PersistentVolumeClaim != nil, + volume.Projected != nil, + volume.Secret != nil: + continue + + default: + badVolumes = append(badVolumes, volume.Name) + + switch { + case volume.HostPath != nil: + badVolumeTypes.Insert("hostPath") + case volume.GCEPersistentDisk != nil: + badVolumeTypes.Insert("gcePersistentDisk") + case volume.AWSElasticBlockStore != nil: + badVolumeTypes.Insert("awsElasticBlockStore") + case volume.GitRepo != nil: + badVolumeTypes.Insert("gitRepo") + case volume.NFS != nil: + badVolumeTypes.Insert("nfs") + case volume.ISCSI != nil: + badVolumeTypes.Insert("iscsi") + case volume.Glusterfs != nil: + badVolumeTypes.Insert("glusterfs") + case volume.RBD != nil: + badVolumeTypes.Insert("rbd") + case volume.FlexVolume != nil: + badVolumeTypes.Insert("flexVolume") + case volume.Cinder != nil: + badVolumeTypes.Insert("cinder") + case volume.CephFS != nil: + badVolumeTypes.Insert("cephfs") + case volume.Flocker != nil: + badVolumeTypes.Insert("flocker") + case volume.FC != nil: + badVolumeTypes.Insert("fc") + case volume.AzureFile != nil: + badVolumeTypes.Insert("azureFile") + case volume.VsphereVolume != nil: + badVolumeTypes.Insert("vsphereVolume") + case volume.Quobyte != nil: + badVolumeTypes.Insert("quobyte") + case volume.AzureDisk != nil: + badVolumeTypes.Insert("azureDisk") + case volume.PhotonPersistentDisk != nil: + badVolumeTypes.Insert("photonPersistentDisk") + case volume.PortworxVolume != nil: + badVolumeTypes.Insert("portworxVolume") + case volume.ScaleIO != nil: + badVolumeTypes.Insert("scaleIO") + case volume.StorageOS != nil: + badVolumeTypes.Insert("storageos") + default: + badVolumeTypes.Insert("unknown") + } + } + } + + if len(badVolumes) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "restricted volume types", + ForbiddenDetail: fmt.Sprintf( + "%s %s %s %s %s", + pluralize("volume", "volumes", len(badVolumes)), + joinQuote(badVolumes), + pluralize("uses", "use", len(badVolumes)), + pluralize("restricted volume type", "restricted volume types", len(badVolumeTypes)), + joinQuote(badVolumeTypes.List()), + ), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go b/vendor/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go new file mode 100644 index 0000000000..87b83727b2 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go @@ -0,0 +1,133 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +Containers must be required to run as non-root users. + +**Restricted Fields:** + +spec.securityContext.runAsNonRoot +spec.containers[*].securityContext.runAsNonRoot +spec.initContainers[*].securityContext.runAsNonRoot + +**Allowed Values:** +true +undefined/null at container-level if pod-level is set to true +*/ + +func init() { + addCheck(CheckRunAsNonRoot) +} + +// CheckRunAsNonRoot returns a restricted level check +// that requires runAsNonRoot=true in 1.0+ +func CheckRunAsNonRoot() Check { + return Check{ + ID: "runAsNonRoot", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: runAsNonRoot_1_0, + }, + }, + } +} + +func runAsNonRoot_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // See KEP-127: https://github.com/kubernetes/enhancements/blob/308ba8d/keps/sig-node/127-user-namespaces/README.md?plain=1#L411-L447 + if relaxPolicyForUserNamespacePod(podSpec) { + return CheckResult{Allowed: true} + } + + // things that explicitly set runAsNonRoot=false + var badSetters []string + + podRunAsNonRoot := false + if podSpec.SecurityContext != nil && podSpec.SecurityContext.RunAsNonRoot != nil { + if !*podSpec.SecurityContext.RunAsNonRoot { + badSetters = append(badSetters, "pod") + } else { + podRunAsNonRoot = true + } + } + + // containers that explicitly set runAsNonRoot=false + var explicitlyBadContainers []string + // containers that didn't set runAsNonRoot and aren't caught by a pod-level runAsNonRoot=true + var implicitlyBadContainers []string + + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext != nil && container.SecurityContext.RunAsNonRoot != nil { + // container explicitly set runAsNonRoot + if !*container.SecurityContext.RunAsNonRoot { + // container explicitly set runAsNonRoot to a bad value + explicitlyBadContainers = append(explicitlyBadContainers, container.Name) + } + } else { + // container did not explicitly set runAsNonRoot + if !podRunAsNonRoot { + // no pod-level runAsNonRoot=true, so this container implicitly has a bad value + implicitlyBadContainers = append(implicitlyBadContainers, container.Name) + } + } + }) + + if len(explicitlyBadContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(explicitlyBadContainers)), + joinQuote(explicitlyBadContainers), + ), + ) + } + // pod or containers explicitly set runAsNonRoot=false + if len(badSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "runAsNonRoot != true", + ForbiddenDetail: fmt.Sprintf("%s must not set securityContext.runAsNonRoot=false", strings.Join(badSetters, " and ")), + } + } + + // pod didn't set runAsNonRoot and not all containers opted into runAsNonRoot + if len(implicitlyBadContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "runAsNonRoot != true", + ForbiddenDetail: fmt.Sprintf( + "pod or %s %s must set securityContext.runAsNonRoot=true", + pluralize("container", "containers", len(implicitlyBadContainers)), + joinQuote(implicitlyBadContainers), + ), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_runAsUser.go b/vendor/k8s.io/pod-security-admission/policy/check_runAsUser.go new file mode 100644 index 0000000000..eb9553ee04 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_runAsUser.go @@ -0,0 +1,104 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +Containers must not set runAsUser: 0 + +**Restricted Fields:** + +spec.securityContext.runAsUser +spec.containers[*].securityContext.runAsUser +spec.initContainers[*].securityContext.runAsUser + +**Allowed Values:** +non-zero values +undefined/null + +*/ + +func init() { + addCheck(CheckRunAsUser) +} + +// CheckRunAsUser returns a restricted level check +// that forbides runAsUser=0 in 1.23+ +func CheckRunAsUser() Check { + return Check{ + ID: "runAsUser", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 23), + CheckPod: runAsUser_1_23, + }, + }, + } +} + +func runAsUser_1_23(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // See KEP-127: https://github.com/kubernetes/enhancements/blob/308ba8d/keps/sig-node/127-user-namespaces/README.md?plain=1#L411-L447 + if relaxPolicyForUserNamespacePod(podSpec) { + return CheckResult{Allowed: true} + } + + // things that explicitly set runAsUser=0 + var badSetters []string + + if podSpec.SecurityContext != nil && podSpec.SecurityContext.RunAsUser != nil && *podSpec.SecurityContext.RunAsUser == 0 { + badSetters = append(badSetters, "pod") + } + + // containers that explicitly set runAsUser=0 + var explicitlyBadContainers []string + + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext != nil && container.SecurityContext.RunAsUser != nil && *container.SecurityContext.RunAsUser == 0 { + explicitlyBadContainers = append(explicitlyBadContainers, container.Name) + } + }) + + if len(explicitlyBadContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(explicitlyBadContainers)), + joinQuote(explicitlyBadContainers), + ), + ) + } + // pod or containers explicitly set runAsUser=0 + if len(badSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "runAsUser=0", + ForbiddenDetail: fmt.Sprintf("%s must not set runAsUser=0", strings.Join(badSetters, " and ")), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_seLinuxOptions.go b/vendor/k8s.io/pod-security-admission/policy/check_seLinuxOptions.go new file mode 100644 index 0000000000..b4263d23b9 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_seLinuxOptions.go @@ -0,0 +1,172 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* +Setting the SELinux type is restricted, and setting a custom SELinux user or role option is forbidden. + +**Restricted Fields:** +spec.securityContext.seLinuxOptions.type +spec.containers[*].securityContext.seLinuxOptions.type +spec.initContainers[*].securityContext.seLinuxOptions.type + +**Allowed Values:** +undefined/empty +container_t +container_init_t +container_kvm_t + +**Restricted Fields:** +spec.securityContext.seLinuxOptions.user +spec.containers[*].securityContext.seLinuxOptions.user +spec.initContainers[*].securityContext.seLinuxOptions.user +spec.securityContext.seLinuxOptions.role +spec.containers[*].securityContext.seLinuxOptions.role +spec.initContainers[*].securityContext.seLinuxOptions.role + +**Allowed Values:** undefined/empty +*/ + +func init() { + addCheck(CheckSELinuxOptions) +} + +// CheckSELinuxOptions returns a baseline level check +// that limits seLinuxOptions type, user, and role values in 1.0+ +func CheckSELinuxOptions() Check { + return Check{ + ID: "seLinuxOptions", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: seLinuxOptions1_0, + }, + { + MinimumVersion: api.MajorMinorVersion(1, 31), + CheckPod: seLinuxOptions1_31, + }, + }, + } +} + +var ( + selinuxAllowedTypes1_0 = sets.New("", "container_t", "container_init_t", "container_kvm_t") + selinuxAllowedTypes1_31 = sets.New("", "container_t", "container_init_t", "container_kvm_t", "container_engine_t") +) + +func seLinuxOptions1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + return seLinuxOptions(podMetadata, podSpec, selinuxAllowedTypes1_0) +} + +func seLinuxOptions1_31(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + return seLinuxOptions(podMetadata, podSpec, selinuxAllowedTypes1_31) +} + +func seLinuxOptions(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec, allowedTypes sets.Set[string]) CheckResult { + var ( + // sources that set bad seLinuxOptions + badSetters []string + + // invalid type values set + badTypes = sets.NewString() + // was user set? + setUser = false + // was role set? + setRole = false + ) + + validSELinuxOptions := func(opts *corev1.SELinuxOptions) bool { + valid := true + if !allowedTypes.Has(opts.Type) { + valid = false + badTypes.Insert(opts.Type) + } + if len(opts.User) > 0 { + valid = false + setUser = true + } + if len(opts.Role) > 0 { + valid = false + setRole = true + } + return valid + } + + if podSpec.SecurityContext != nil && podSpec.SecurityContext.SELinuxOptions != nil { + if !validSELinuxOptions(podSpec.SecurityContext.SELinuxOptions) { + badSetters = append(badSetters, "pod") + } + } + + var badContainers []string + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext != nil && container.SecurityContext.SELinuxOptions != nil { + if !validSELinuxOptions(container.SecurityContext.SELinuxOptions) { + badContainers = append(badContainers, container.Name) + } + } + }) + if len(badContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + ), + ) + } + + if len(badSetters) > 0 { + var badData []string + if len(badTypes) > 0 { + badData = append(badData, fmt.Sprintf( + "%s %s", + pluralize("type", "types", len(badTypes)), + joinQuote(badTypes.List()), + )) + } + if setUser { + badData = append(badData, "user may not be set") + } + if setRole { + badData = append(badData, "role may not be set") + } + + return CheckResult{ + Allowed: false, + ForbiddenReason: "seLinuxOptions", + ForbiddenDetail: fmt.Sprintf( + `%s set forbidden securityContext.seLinuxOptions: %s`, + strings.Join(badSetters, " and "), + strings.Join(badData, "; "), + ), + } + } + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go b/vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go new file mode 100644 index 0000000000..d45dba7076 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go @@ -0,0 +1,171 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* +If seccomp profiles are specified, only runtime default and localhost profiles are allowed. + +v1.0 - v1.18: +**Restricted Fields:** +metadata.annotations['seccomp.security.alpha.kubernetes.io/pod'] +metadata.annotations['container.seccomp.security.alpha.kubernetes.io/*'] + +**Allowed Values:** 'runtime/default', 'docker/default', 'localhost/*', undefined + +v1.19+: +**Restricted Fields:** +spec.securityContext.seccompProfile.type +spec.containers[*].securityContext.seccompProfile.type +spec.initContainers[*].securityContext.seccompProfile.type + +**Allowed Values:** 'RuntimeDefault', 'Localhost', undefined +*/ +const ( + annotationKeyPod = "seccomp.security.alpha.kubernetes.io/pod" + annotationKeyContainerPrefix = "container.seccomp.security.alpha.kubernetes.io/" + + checkSeccompBaselineID CheckID = "seccompProfile_baseline" +) + +func init() { + addCheck(CheckSeccompBaseline) +} + +func CheckSeccompBaseline() Check { + return Check{ + ID: checkSeccompBaselineID, + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: seccompProfileBaseline_1_0, + }, + { + MinimumVersion: api.MajorMinorVersion(1, 19), + CheckPod: seccompProfileBaseline_1_19, + }, + }, + } +} + +func validSeccomp(t corev1.SeccompProfileType) bool { + return t == corev1.SeccompProfileTypeLocalhost || + t == corev1.SeccompProfileTypeRuntimeDefault +} + +func validSeccompAnnotationValue(v string) bool { + return v == corev1.SeccompProfileRuntimeDefault || + v == corev1.DeprecatedSeccompProfileDockerDefault || + strings.HasPrefix(v, corev1.SeccompLocalhostProfileNamePrefix) +} + +// seccompProfileBaseline_1_0 checks baseline policy on seccomp alpha annotation +func seccompProfileBaseline_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + forbidden := sets.NewString() + + if val, ok := podMetadata.Annotations[annotationKeyPod]; ok { + if !validSeccompAnnotationValue(val) { + forbidden.Insert(fmt.Sprintf("%s=%q", annotationKeyPod, val)) + } + } + + visitContainers(podSpec, func(c *corev1.Container) { + annotation := annotationKeyContainerPrefix + c.Name + if val, ok := podMetadata.Annotations[annotation]; ok { + if !validSeccompAnnotationValue(val) { + forbidden.Insert(fmt.Sprintf("%s=%q", annotation, val)) + } + } + }) + + if len(forbidden) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + "forbidden %s %s", + pluralize("annotation", "annotations", len(forbidden)), + strings.Join(forbidden.List(), ", "), + ), + } + } + + return CheckResult{Allowed: true} +} + +// seccompProfileBaseline_1_19 checks baseline policy on securityContext.seccompProfile field +func seccompProfileBaseline_1_19(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // things that explicitly set seccompProfile.type to a bad value + var badSetters []string + badValues := sets.NewString() + + if podSpec.SecurityContext != nil && podSpec.SecurityContext.SeccompProfile != nil { + if !validSeccomp(podSpec.SecurityContext.SeccompProfile.Type) { + badSetters = append(badSetters, "pod") + badValues.Insert(string(podSpec.SecurityContext.SeccompProfile.Type)) + } + } + + // containers that explicitly set seccompProfile.type to a bad value + var explicitlyBadContainers []string + + visitContainers(podSpec, func(c *corev1.Container) { + if c.SecurityContext != nil && c.SecurityContext.SeccompProfile != nil { + // container explicitly set seccompProfile + if !validSeccomp(c.SecurityContext.SeccompProfile.Type) { + // container explicitly set seccompProfile to a bad value + explicitlyBadContainers = append(explicitlyBadContainers, c.Name) + badValues.Insert(string(c.SecurityContext.SeccompProfile.Type)) + } + } + }) + + if len(explicitlyBadContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(explicitlyBadContainers)), + joinQuote(explicitlyBadContainers), + ), + ) + } + // pod or containers explicitly set bad seccompProfiles + if len(badSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + "%s must not set securityContext.seccompProfile.type to %s", + strings.Join(badSetters, " and "), + joinQuote(badValues.List()), + ), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go b/vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go new file mode 100644 index 0000000000..9040e0fb05 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go @@ -0,0 +1,155 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* + +Seccomp profiles must be specified, and only runtime default and localhost profiles are allowed. + +v1.19+: +**Restricted Fields:** +spec.securityContext.seccompProfile.type +spec.containers[*].securityContext.seccompProfile.type +spec.initContainers[*].securityContext.seccompProfile.type + +**Allowed Values:** 'RuntimeDefault', 'Localhost' +Note: container-level fields may be undefined if pod-level field is specified. + +*/ + +func init() { + addCheck(CheckSeccompProfileRestricted) +} + +func CheckSeccompProfileRestricted() Check { + return Check{ + ID: "seccompProfile_restricted", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 19), + CheckPod: seccompProfileRestricted_1_19, + OverrideCheckIDs: []CheckID{checkSeccompBaselineID}, + }, + // Starting 1.25, windows pods would be exempted from this check using pod.spec.os field when set to windows. + { + MinimumVersion: api.MajorMinorVersion(1, 25), + CheckPod: seccompProfileRestricted_1_25, + OverrideCheckIDs: []CheckID{checkSeccompBaselineID}, + }, + }, + } +} + +// seccompProfileRestricted_1_19 checks restricted policy on securityContext.seccompProfile field +func seccompProfileRestricted_1_19(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // things that explicitly set seccompProfile.type to a bad value + var badSetters []string + badValues := sets.NewString() + + podSeccompSet := false + + if podSpec.SecurityContext != nil && podSpec.SecurityContext.SeccompProfile != nil { + if !validSeccomp(podSpec.SecurityContext.SeccompProfile.Type) { + badSetters = append(badSetters, "pod") + badValues.Insert(string(podSpec.SecurityContext.SeccompProfile.Type)) + } else { + podSeccompSet = true + } + } + + // containers that explicitly set seccompProfile.type to a bad value + var explicitlyBadContainers []string + // containers that didn't set seccompProfile and aren't caught by a pod-level seccompProfile + var implicitlyBadContainers []string + + visitContainers(podSpec, func(c *corev1.Container) { + if c.SecurityContext != nil && c.SecurityContext.SeccompProfile != nil { + // container explicitly set seccompProfile + if !validSeccomp(c.SecurityContext.SeccompProfile.Type) { + // container explicitly set seccompProfile to a bad value + explicitlyBadContainers = append(explicitlyBadContainers, c.Name) + badValues.Insert(string(c.SecurityContext.SeccompProfile.Type)) + } + } else { + // container did not explicitly set seccompProfile + if !podSeccompSet { + // no valid pod-level seccompProfile, so this container implicitly has a bad value + implicitlyBadContainers = append(implicitlyBadContainers, c.Name) + } + } + }) + + if len(explicitlyBadContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(explicitlyBadContainers)), + joinQuote(explicitlyBadContainers), + ), + ) + } + // pod or containers explicitly set bad seccompProfiles + if len(badSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + "%s must not set securityContext.seccompProfile.type to %s", + strings.Join(badSetters, " and "), + joinQuote(badValues.List()), + ), + } + } + + // pod didn't set seccompProfile and not all containers opted into seccompProfile + if len(implicitlyBadContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + `pod or %s %s must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost"`, + pluralize("container", "containers", len(implicitlyBadContainers)), + joinQuote(implicitlyBadContainers), + ), + } + } + + return CheckResult{Allowed: true} +} + +// seccompProfileRestricted_1_25 checks restricted policy on securityContext.seccompProfile field for kubernetes +// version 1.25 and above +func seccompProfileRestricted_1_25(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // Pod API validation would have failed if podOS == Windows and if secCompProfile has been set. + // We can admit the Windows pod even if seccompProfile has not been set. + if podSpec.OS != nil && podSpec.OS.Name == corev1.Windows { + return CheckResult{Allowed: true} + } + return seccompProfileRestricted_1_19(podMetadata, podSpec) +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_sysctls.go b/vendor/k8s.io/pod-security-admission/policy/check_sysctls.go new file mode 100644 index 0000000000..8e4935fdb0 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_sysctls.go @@ -0,0 +1,143 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/pod-security-admission/api" +) + +/* + +Sysctls can disable security mechanisms or affect all containers on a host, +and should be disallowed except for an allowed "safe" subset. + +A sysctl is considered safe if it is namespaced in the container or the Pod, +and it is isolated from other Pods or processes on the same Node. + +**Restricted Fields:** +spec.securityContext.sysctls[*].name + +**Allowed Values:** +'kernel.shm_rmid_forced' +'net.ipv4.ip_local_port_range' +'net.ipv4.tcp_syncookies' +'net.ipv4.ping_group_range' +'net.ipv4.ip_unprivileged_port_start' +'net.ipv4.ip_local_reserved_ports' +'net.ipv4.tcp_keepalive_time' +'net.ipv4.tcp_fin_timeout' +'net.ipv4.tcp_keepalive_intvl' +'net.ipv4.tcp_keepalive_probes' +'net.ipv4.tcp_rmem' +'net.ipv4.tcp_wmem' + +*/ + +func init() { + addCheck(CheckSysctls) +} + +// CheckSysctls returns a baseline level check +// that limits the value of sysctls in 1.0+ +func CheckSysctls() Check { + return Check{ + ID: "sysctls", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: sysctlsV1Dot0, + }, + { + MinimumVersion: api.MajorMinorVersion(1, 27), + CheckPod: sysctlsV1Dot27, + }, { + MinimumVersion: api.MajorMinorVersion(1, 29), + CheckPod: sysctlsV1Dot29, + }, + { + MinimumVersion: api.MajorMinorVersion(1, 32), + CheckPod: sysctlsV1Dot32, + }, + }, + } +} + +var ( + sysctlsAllowedV1Dot0 = sets.NewString( + "kernel.shm_rmid_forced", + "net.ipv4.ip_local_port_range", + "net.ipv4.tcp_syncookies", + "net.ipv4.ping_group_range", + "net.ipv4.ip_unprivileged_port_start", + ) + sysctlsAllowedV1Dot27 = sysctlsAllowedV1Dot0.Union(sets.NewString( + "net.ipv4.ip_local_reserved_ports", + )) + sysctlsAllowedV1Dot29 = sysctlsAllowedV1Dot27.Union(sets.NewString( + "net.ipv4.tcp_keepalive_time", + "net.ipv4.tcp_fin_timeout", + "net.ipv4.tcp_keepalive_intvl", + "net.ipv4.tcp_keepalive_probes", + )) + sysctlsAllowedV1Dot32 = sysctlsAllowedV1Dot29.Union(sets.NewString( + "net.ipv4.tcp_rmem", + "net.ipv4.tcp_wmem", + )) +) + +func sysctlsV1Dot0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + return sysctls(podMetadata, podSpec, sysctlsAllowedV1Dot0) +} + +func sysctlsV1Dot27(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + return sysctls(podMetadata, podSpec, sysctlsAllowedV1Dot27) +} + +func sysctlsV1Dot29(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + return sysctls(podMetadata, podSpec, sysctlsAllowedV1Dot29) +} + +func sysctlsV1Dot32(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + return sysctls(podMetadata, podSpec, sysctlsAllowedV1Dot32) +} + +func sysctls(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec, sysctls_allowed_set sets.String) CheckResult { + var forbiddenSysctls []string + + if podSpec.SecurityContext != nil { + for _, sysctl := range podSpec.SecurityContext.Sysctls { + if !sysctls_allowed_set.Has(sysctl.Name) { + forbiddenSysctls = append(forbiddenSysctls, sysctl.Name) + } + } + } + + if len(forbiddenSysctls) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "forbidden sysctls", + ForbiddenDetail: strings.Join(forbiddenSysctls, ", "), + } + } + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/check_windowsHostProcess.go b/vendor/k8s.io/pod-security-admission/policy/check_windowsHostProcess.go new file mode 100644 index 0000000000..9e6dbe2e35 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/check_windowsHostProcess.go @@ -0,0 +1,102 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +/* +Pod and containers must not set securityContext.windowsOptions.hostProcess to true. + +**Restricted Fields:** + +spec.securityContext.windowsOptions.hostProcess +spec.containers[*].securityContext.windowsOptions.hostProcess +spec.initContainers[*].securityContext.windowsOptions.hostProcess + +**Allowed Values:** undefined / false +*/ + +func init() { + addCheck(CheckWindowsHostProcess) +} + +// CheckWindowsHostProcess returns a baseline level check +// that forbids hostProcess=true in 1.0+ +func CheckWindowsHostProcess() Check { + return Check{ + ID: "windowsHostProcess", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: windowsHostProcess_1_0, + }, + }, + } +} + +func windowsHostProcess_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + var badContainers []string + visitContainers(podSpec, func(container *corev1.Container) { + if container.SecurityContext != nil && + container.SecurityContext.WindowsOptions != nil && + container.SecurityContext.WindowsOptions.HostProcess != nil && + *container.SecurityContext.WindowsOptions.HostProcess { + badContainers = append(badContainers, container.Name) + } + }) + + podSpecForbidden := false + if podSpec.SecurityContext != nil && + podSpec.SecurityContext.WindowsOptions != nil && + podSpec.SecurityContext.WindowsOptions.HostProcess != nil && + *podSpec.SecurityContext.WindowsOptions.HostProcess { + podSpecForbidden = true + } + + // pod or containers explicitly set hostProcess=true + var forbiddenSetters []string + if podSpecForbidden { + forbiddenSetters = append(forbiddenSetters, "pod") + } + if len(badContainers) > 0 { + forbiddenSetters = append( + forbiddenSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(badContainers)), + joinQuote(badContainers), + ), + ) + } + if len(forbiddenSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "hostProcess", + ForbiddenDetail: fmt.Sprintf("%s must not set securityContext.windowsOptions.hostProcess=true", strings.Join(forbiddenSetters, " and ")), + } + } + + return CheckResult{Allowed: true} +} diff --git a/vendor/k8s.io/pod-security-admission/policy/checks.go b/vendor/k8s.io/pod-security-admission/policy/checks.go new file mode 100644 index 0000000000..6b13d2f1cf --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/checks.go @@ -0,0 +1,184 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +type Check struct { + // ID is the unique ID of the check. + ID CheckID + // Level is the policy level this check belongs to. + // Must be Baseline or Restricted. + // Baseline checks are evaluated for baseline and restricted namespaces. + // Restricted checks are only evaluated for restricted namespaces. + Level api.Level + // Versions contains one or more revisions of the check that apply to different versions. + // If the check is not yet assigned to a version, this must be a single-item list with a MinimumVersion of "". + // Otherwise, MinimumVersion of items must represent strictly increasing versions. + Versions []VersionedCheck +} + +type VersionedCheck struct { + // MinimumVersion is the first policy version this check applies to. + // If unset, this check is not yet assigned to a policy version. + // If set, must not be "latest". + MinimumVersion api.Version + // CheckPod determines if the pod is allowed. + CheckPod CheckPodFn + // OverrideCheckIDs is an optional list of checks that should be skipped when this check is run. + // Overrides may only be set on restricted checks, and may only override baseline checks. + OverrideCheckIDs []CheckID +} + +type CheckPodFn func(*metav1.ObjectMeta, *corev1.PodSpec) CheckResult + +type CheckID string + +// CheckResult contains the result of checking a pod and indicates whether the pod is allowed, +// and if not, why it was forbidden. +// +// Example output for (false, "host ports", "8080, 9090"): +// +// When checking all pods in a namespace: +// disallowed by policy "baseline": host ports, privileged containers, non-default capabilities +// When checking an individual pod: +// disallowed by policy "baseline": host ports (8080, 9090), privileged containers, non-default capabilities (CAP_NET_RAW) +type CheckResult struct { + // Allowed indicates if the check allowed the pod. + Allowed bool + // ForbiddenReason must be set if Allowed is false. + // ForbiddenReason should be as succinct as possible and is always output. + // Examples: + // - "host ports" + // - "privileged containers" + // - "non-default capabilities" + ForbiddenReason string + // ForbiddenDetail should only be set if Allowed is false, and is optional. + // ForbiddenDetail can include specific values that were disallowed and is used when checking an individual object. + // Examples: + // - list specific invalid host ports: "8080, 9090" + // - list specific invalid containers: "container1, container2" + // - list specific non-default capabilities: "CAP_NET_RAW" + ForbiddenDetail string +} + +// AggergateCheckResult holds the aggregate result of running CheckPod across multiple checks. +type AggregateCheckResult struct { + // Allowed indicates if all checks allowed the pod. + Allowed bool + // ForbiddenReasons is a slice of the forbidden reasons from all the forbidden checks. It should not include empty strings. + // ForbiddenReasons and ForbiddenDetails must have the same number of elements, and the indexes are for the same check. + ForbiddenReasons []string + // ForbiddenDetails is a slice of the forbidden details from all the forbidden checks. It may include empty strings. + // ForbiddenReasons and ForbiddenDetails must have the same number of elements, and the indexes are for the same check. + ForbiddenDetails []string +} + +// ForbiddenReason returns a comma-separated string of of the forbidden reasons. +// Example: host ports, privileged containers, non-default capabilities +func (a *AggregateCheckResult) ForbiddenReason() string { + return strings.Join(a.ForbiddenReasons, ", ") +} + +// ForbiddenDetail returns a detailed forbidden message, with non-empty details formatted in +// parentheses with the associated reason. +// Example: host ports (8080, 9090), privileged containers, non-default capabilities (NET_RAW) +func (a *AggregateCheckResult) ForbiddenDetail() string { + var b strings.Builder + for i := 0; i < len(a.ForbiddenReasons); i++ { + b.WriteString(a.ForbiddenReasons[i]) + if a.ForbiddenDetails[i] != "" { + b.WriteString(" (") + b.WriteString(a.ForbiddenDetails[i]) + b.WriteString(")") + } + if i != len(a.ForbiddenReasons)-1 { + b.WriteString(", ") + } + } + return b.String() +} + +// UnknownForbiddenReason is used as the placeholder forbidden reason for checks that incorrectly disallow without providing a reason. +const UnknownForbiddenReason = "unknown forbidden reason" + +// AggregateCheckPod runs all the checks and aggregates the forbidden results into a single CheckResult. +// The aggregated reason is a comma-separated +func AggregateCheckResults(results []CheckResult) AggregateCheckResult { + var ( + reasons []string + details []string + ) + for _, result := range results { + if !result.Allowed { + if len(result.ForbiddenReason) == 0 { + reasons = append(reasons, UnknownForbiddenReason) + } else { + reasons = append(reasons, result.ForbiddenReason) + } + details = append(details, result.ForbiddenDetail) + } + } + return AggregateCheckResult{ + Allowed: len(reasons) == 0, + ForbiddenReasons: reasons, + ForbiddenDetails: details, + } +} + +var ( + defaultChecks []func() Check + experimentalChecks []func() Check +) + +func addCheck(f func() Check) { + // add to experimental or versioned list + c := f() + if len(c.Versions) == 1 && c.Versions[0].MinimumVersion == (api.Version{}) { + experimentalChecks = append(experimentalChecks, f) + } else { + defaultChecks = append(defaultChecks, f) + } +} + +// DefaultChecks returns checks that are expected to be enabled by default. +// The results are mutually exclusive with ExperimentalChecks. +// It returns a new copy of checks on each invocation and is expected to be called once at setup time. +func DefaultChecks() []Check { + retval := make([]Check, 0, len(defaultChecks)) + for _, f := range defaultChecks { + retval = append(retval, f()) + } + return retval +} + +// ExperimentalChecks returns checks that have not yet been assigned to policy versions. +// The results are mutually exclusive with DefaultChecks. +// It returns a new copy of checks on each invocation and is expected to be called once at setup time. +func ExperimentalChecks() []Check { + retval := make([]Check, 0, len(experimentalChecks)) + for _, f := range experimentalChecks { + retval = append(retval, f()) + } + return retval +} diff --git a/vendor/k8s.io/pod-security-admission/policy/doc.go b/vendor/k8s.io/pod-security-admission/policy/doc.go new file mode 100644 index 0000000000..f8c5831011 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package policy contains implementations of Pod Security Standards checks +package policy diff --git a/vendor/k8s.io/pod-security-admission/policy/helpers.go b/vendor/k8s.io/pod-security-admission/policy/helpers.go new file mode 100644 index 0000000000..e35348929c --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/helpers.go @@ -0,0 +1,56 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "strings" + "sync/atomic" + + corev1 "k8s.io/api/core/v1" +) + +func joinQuote(items []string) string { + if len(items) == 0 { + return "" + } + return `"` + strings.Join(items, `", "`) + `"` +} + +func pluralize(singular, plural string, count int) string { + if count == 1 { + return singular + } + return plural +} + +var relaxPolicyForUserNamespacePods = &atomic.Bool{} + +// RelaxPolicyForUserNamespacePods allows opting into relaxing runAsUser / +// runAsNonRoot restricted policies for user namespace pods, before the +// usernamespace feature has reached GA and propagated to the oldest supported +// nodes. +// This should only be opted into in clusters where the administrator ensures +// all nodes in the cluster enable the user namespace feature. +func RelaxPolicyForUserNamespacePods(relax bool) { + relaxPolicyForUserNamespacePods.Store(relax) +} + +// relaxPolicyForUserNamespacePod returns true if a policy should be relaxed +// because of enabled user namespaces in the provided pod spec. +func relaxPolicyForUserNamespacePod(podSpec *corev1.PodSpec) bool { + return relaxPolicyForUserNamespacePods.Load() && podSpec != nil && podSpec.HostUsers != nil && !*podSpec.HostUsers +} diff --git a/vendor/k8s.io/pod-security-admission/policy/registry.go b/vendor/k8s.io/pod-security-admission/policy/registry.go new file mode 100644 index 0000000000..4b91bef887 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/registry.go @@ -0,0 +1,226 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "sort" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" +) + +// Evaluator holds the Checks that are used to validate a policy. +type Evaluator interface { + // EvaluatePod evaluates the pod against the policy for the given level & version. + EvaluatePod(lv api.LevelVersion, podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) []CheckResult +} + +// checkRegistry provides a default implementation of an Evaluator. +type checkRegistry struct { + // The checks are a map policy version to a slice of checks registered for that version. + baselineChecks, restrictedChecks map[api.Version][]CheckPodFn + // maxVersion is the maximum version that is cached, guaranteed to be at least + // the max MinimumVersion of all registered checks. + maxVersion api.Version +} + +// NewEvaluator constructs a new Evaluator instance from the list of checks. If the provided checks are invalid, +// an error is returned. A valid list of checks must meet the following requirements: +// 1. Check.ID is unique in the list +// 2. Check.Level must be either Baseline or Restricted +// 3. Checks must have a non-empty set of versions, sorted in a strictly increasing order +// 4. Check.Versions cannot include 'latest' +func NewEvaluator(checks []Check) (Evaluator, error) { + if err := validateChecks(checks); err != nil { + return nil, err + } + r := &checkRegistry{ + baselineChecks: map[api.Version][]CheckPodFn{}, + restrictedChecks: map[api.Version][]CheckPodFn{}, + } + populate(r, checks) + return r, nil +} + +func (r *checkRegistry) EvaluatePod(lv api.LevelVersion, podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) []CheckResult { + if lv.Level == api.LevelPrivileged { + return nil + } + if r.maxVersion.Older(lv.Version) { + lv.Version = r.maxVersion + } + + var checks []CheckPodFn + if lv.Level == api.LevelBaseline { + checks = r.baselineChecks[lv.Version] + } else { + // includes non-overridden baseline checks + checks = r.restrictedChecks[lv.Version] + } + + var results []CheckResult + for _, check := range checks { + results = append(results, check(podMetadata, podSpec)) + } + return results +} + +func validateChecks(checks []Check) error { + ids := map[CheckID]api.Level{} + for _, check := range checks { + if _, ok := ids[check.ID]; ok { + return fmt.Errorf("multiple checks registered for ID %s", check.ID) + } + ids[check.ID] = check.Level + if check.Level != api.LevelBaseline && check.Level != api.LevelRestricted { + return fmt.Errorf("check %s: invalid level %s", check.ID, check.Level) + } + if len(check.Versions) == 0 { + return fmt.Errorf("check %s: empty", check.ID) + } + maxVersion := api.Version{} + for _, c := range check.Versions { + if c.MinimumVersion == (api.Version{}) { + return fmt.Errorf("check %s: undefined version found", check.ID) + } + if c.MinimumVersion.Latest() { + return fmt.Errorf("check %s: version cannot be 'latest'", check.ID) + } + if maxVersion == c.MinimumVersion { + return fmt.Errorf("check %s: duplicate version %s", check.ID, c.MinimumVersion) + } + if !maxVersion.Older(c.MinimumVersion) { + return fmt.Errorf("check %s: versions must be strictly increasing", check.ID) + } + maxVersion = c.MinimumVersion + } + } + // Second pass to validate overrides. + for _, check := range checks { + for _, c := range check.Versions { + if len(c.OverrideCheckIDs) == 0 { + continue + } + + if check.Level != api.LevelRestricted { + return fmt.Errorf("check %s: only restricted checks may set overrides", check.ID) + } + for _, override := range c.OverrideCheckIDs { + if overriddenLevel, ok := ids[override]; ok && overriddenLevel != api.LevelBaseline { + return fmt.Errorf("check %s: overrides %s check %s", check.ID, overriddenLevel, override) + } + } + } + } + return nil +} + +func populate(r *checkRegistry, validChecks []Check) { + // Find the max(MinimumVersion) across all checks. + for _, c := range validChecks { + lastVersion := c.Versions[len(c.Versions)-1].MinimumVersion + if r.maxVersion.Older(lastVersion) { + r.maxVersion = lastVersion + } + } + + var ( + restrictedVersionedChecks = map[api.Version]map[CheckID]VersionedCheck{} + baselineVersionedChecks = map[api.Version]map[CheckID]VersionedCheck{} + + baselineIDs, restrictedIDs []CheckID + ) + for _, c := range validChecks { + if c.Level == api.LevelRestricted { + restrictedIDs = append(restrictedIDs, c.ID) + inflateVersions(c, restrictedVersionedChecks, r.maxVersion) + } else { + baselineIDs = append(baselineIDs, c.ID) + inflateVersions(c, baselineVersionedChecks, r.maxVersion) + } + } + + // Sort the IDs to maintain consistent error messages. + sort.Slice(restrictedIDs, func(i, j int) bool { return restrictedIDs[i] < restrictedIDs[j] }) + sort.Slice(baselineIDs, func(i, j int) bool { return baselineIDs[i] < baselineIDs[j] }) + orderedIDs := append(baselineIDs, restrictedIDs...) // Baseline checks first, then restricted. + + for v := api.MajorMinorVersion(1, 0); v.Older(nextMinor(r.maxVersion)); v = nextMinor(v) { + // Aggregate all the overridden baseline check ids. + overrides := map[CheckID]bool{} + for _, c := range restrictedVersionedChecks[v] { + for _, override := range c.OverrideCheckIDs { + overrides[override] = true + } + } + // Add the filtered baseline checks to restricted. + for id, c := range baselineVersionedChecks[v] { + if overrides[id] { + continue // Overridden check: skip it. + } + if restrictedVersionedChecks[v] == nil { + restrictedVersionedChecks[v] = map[CheckID]VersionedCheck{} + } + restrictedVersionedChecks[v][id] = c + } + + r.restrictedChecks[v] = mapCheckPodFns(restrictedVersionedChecks[v], orderedIDs) + r.baselineChecks[v] = mapCheckPodFns(baselineVersionedChecks[v], orderedIDs) + } +} + +func inflateVersions(check Check, versions map[api.Version]map[CheckID]VersionedCheck, maxVersion api.Version) { + for i, c := range check.Versions { + var nextVersion api.Version + if i+1 < len(check.Versions) { + nextVersion = check.Versions[i+1].MinimumVersion + } else { + // Assumes only 1 Major version. + nextVersion = nextMinor(maxVersion) + } + // Iterate over all versions from the minimum of the current check, to the minimum of the + // next check, or the maxVersion++. + for v := c.MinimumVersion; v.Older(nextVersion); v = nextMinor(v) { + if versions[v] == nil { + versions[v] = map[CheckID]VersionedCheck{} + } + versions[v][check.ID] = check.Versions[i] + } + } +} + +// mapCheckPodFns converts the versioned check map to an ordered slice of CheckPodFn, +// using the order specified by orderedIDs. All checks must have a corresponding ID in orderedIDs. +func mapCheckPodFns(checks map[CheckID]VersionedCheck, orderedIDs []CheckID) []CheckPodFn { + fns := make([]CheckPodFn, 0, len(checks)) + for _, id := range orderedIDs { + if check, ok := checks[id]; ok { + fns = append(fns, check.CheckPod) + } + } + return fns +} + +// nextMinor increments the minor version +func nextMinor(v api.Version) api.Version { + if v.Latest() { + return v + } + return api.MajorMinorVersion(v.Major(), v.Minor()+1) +} diff --git a/vendor/k8s.io/pod-security-admission/policy/visitor.go b/vendor/k8s.io/pod-security-admission/policy/visitor.go new file mode 100644 index 0000000000..5778651c9b --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/policy/visitor.go @@ -0,0 +1,37 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + corev1 "k8s.io/api/core/v1" +) + +// ContainerVisitor is called with each container and the field.Path to that container +type ContainerVisitor func(container *corev1.Container) + +// visitContainers invokes the visitor function for every container in the given pod spec +func visitContainers(podSpec *corev1.PodSpec, visitor ContainerVisitor) { + for i := range podSpec.InitContainers { + visitor(&podSpec.InitContainers[i]) + } + for i := range podSpec.Containers { + visitor(&podSpec.Containers[i]) + } + for i := range podSpec.EphemeralContainers { + visitor((*corev1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon)) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 98e85f1481..75310e3779 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1537,6 +1537,7 @@ k8s.io/kube-openapi/pkg/validation/strfmt/bson # k8s.io/pod-security-admission v0.33.2 ## explicit; go 1.24.0 k8s.io/pod-security-admission/api +k8s.io/pod-security-admission/policy # k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 ## explicit; go 1.18 k8s.io/utils/buffer From d6adece043ec9c711f3156b0725e459076038269 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 12:53:23 +0200 Subject: [PATCH 06/15] podsecurityreadinesscontroller: wire down psapi.ParseLevel to classific --- .../classification.go | 44 +++++-------------- .../classification_test.go | 37 ++++++---------- .../violation.go | 24 ++++++---- 3 files changed, 39 insertions(+), 66 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go index 74f24698eb..6818b202fb 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -2,7 +2,6 @@ package podsecurityreadinesscontroller import ( "context" - "fmt" "strings" securityv1 "github.com/openshift/api/security/v1" @@ -22,7 +21,12 @@ var ( ) ) -func (c *PodSecurityReadinessController) classifyViolatingNamespace(ctx context.Context, conditions *podSecurityOperatorConditions, ns *corev1.Namespace, enforceLevel string) error { +func (c *PodSecurityReadinessController) classifyViolatingNamespace( + ctx context.Context, + conditions *podSecurityOperatorConditions, + ns *corev1.Namespace, + enforceLevel psapi.Level, +) error { if runLevelZeroNamespaces.Has(ns.Name) { conditions.addViolatingRunLevelZero(ns) return nil @@ -56,23 +60,11 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace(ctx context. return nil } -func (c *PodSecurityReadinessController) isUserViolation(ctx context.Context, ns *corev1.Namespace, label string) (bool, error) { - var enforcementLevel psapi.Level - switch strings.ToLower(label) { - case "restricted": - enforcementLevel = psapi.LevelRestricted - case "baseline": - enforcementLevel = psapi.LevelBaseline - case "privileged": - // If privileged is violating, something is seriously wrong - // but testing against privileged level is pointless (everything passes) - klog.V(2).InfoS("Namespace violating privileged level - skipping user check", - "namespace", ns.Name) - return false, nil - default: - return false, fmt.Errorf("unknown level: %q", label) - } - +func (c *PodSecurityReadinessController) isUserViolation( + ctx context.Context, + ns *corev1.Namespace, + enforcementLevel psapi.Level, +) (bool, error) { allPods, err := c.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) if err != nil { klog.V(2).ErrorS(err, "Failed to list pods in namespace", "namespace", ns.Name) @@ -102,28 +94,14 @@ func (c *PodSecurityReadinessController) isUserViolation(ctx context.Context, ns enforcementVersion := psapi.LatestVersion() for _, pod := range userPods { - klog.InfoS("Evaluating user pod against PSA level", - "namespace", ns.Name, "pod", pod.Name, "level", label, - "podSecurityContext", pod.Spec.SecurityContext) - results := c.psaEvaluator.EvaluatePod( psapi.LevelVersion{Level: enforcementLevel, Version: enforcementVersion}, &pod.ObjectMeta, &pod.Spec, ) - klog.InfoS("PSA evaluation results", - "namespace", ns.Name, "pod", pod.Name, "level", label, - "resultCount", len(results)) - for _, result := range results { - klog.InfoS("PSA evaluation result", - "namespace", ns.Name, "pod", pod.Name, "level", label, - "allowed", result.Allowed, "reason", result.ForbiddenReason, - "detail", result.ForbiddenDetail) if !result.Allowed { - klog.InfoS("User pod violates PSA level", - "namespace", ns.Name, "pod", pod.Name, "level", label) return true, nil } } diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go index 9c506bb949..a7a30bf29f 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -13,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" clienttesting "k8s.io/client-go/testing" + psapi "k8s.io/pod-security-admission/api" "k8s.io/pod-security-admission/policy" ) @@ -68,7 +69,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { name string namespace *corev1.Namespace pods []corev1.Pod - enforceLevel string + enforceLevel psapi.Level expectedConditions podSecurityOperatorConditions expectError bool }{ @@ -80,7 +81,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, }, pods: []corev1.Pod{}, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingRunLevelZeroNamespaces: []string{"kube-system"}, }, @@ -94,7 +95,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, }, pods: []corev1.Pod{}, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingRunLevelZeroNamespaces: []string{"default"}, }, @@ -108,7 +109,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, }, pods: []corev1.Pod{}, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingRunLevelZeroNamespaces: []string{"kube-public"}, }, @@ -122,7 +123,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, }, pods: []corev1.Pod{}, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingOpenShiftNamespaces: []string{"openshift-test"}, }, @@ -155,7 +156,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{ newUserSCCPodPrivileged("user-pod", "customer-ns"), }, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingUserSCCNamespaces: []string{"customer-ns"}, }, @@ -171,7 +172,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{ newUserSCCPodWithPrivilegedContainer("user-scc-violating-pod", "user-scc-violation-test"), }, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingUserSCCNamespaces: []string{"user-scc-violation-test"}, }, @@ -187,7 +188,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{ newServiceAccountPod("sa-pod", "customer-ns"), }, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingCustomerNamespaces: []string{"customer-ns"}, }, @@ -221,7 +222,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{ newUserSCCPodRestricted("user-pod", "customer-ns"), }, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingCustomerNamespaces: []string{"customer-ns"}, }, @@ -235,7 +236,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, }, pods: []corev1.Pod{}, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingCustomerNamespaces: []string{"customer-ns"}, }, @@ -265,7 +266,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, }, }, - enforceLevel: "restricted", + enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ violatingCustomerNamespaces: []string{"customer-ns"}, }, @@ -281,24 +282,12 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{ newUserSCCPodPrivileged("user-pod", "customer-ns"), }, - enforceLevel: "privileged", + enforceLevel: psapi.LevelPrivileged, expectedConditions: podSecurityOperatorConditions{ violatingCustomerNamespaces: []string{"customer-ns"}, }, expectError: false, }, - { - name: "invalid PSA level causes error", - namespace: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "customer-ns", - }, - }, - pods: []corev1.Pod{}, - enforceLevel: "invalid-level", - expectedConditions: podSecurityOperatorConditions{}, - expectError: true, - }, } { t.Run(tt.name, func(t *testing.T) { controller, err := createTestController(tt.pods) diff --git a/pkg/operator/podsecurityreadinesscontroller/violation.go b/pkg/operator/podsecurityreadinesscontroller/violation.go index e4775177b2..5722d3e7fd 100644 --- a/pkg/operator/podsecurityreadinesscontroller/violation.go +++ b/pkg/operator/podsecurityreadinesscontroller/violation.go @@ -23,7 +23,7 @@ var ( // isNamespaceViolating checks if a namespace is ready for Pod Security Admission enforcement. // It returns true if the namespace is violating the Pod Security Admission policy, along with // the enforce label it was tested against. -func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Context, ns *corev1.Namespace) (bool, string, error) { +func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Context, ns *corev1.Namespace) (bool, psapi.Level, error) { nsApplyConfig, err := applyconfiguration.ExtractNamespace(ns, syncerControllerName) if err != nil { return false, "", err @@ -31,7 +31,7 @@ func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Contex enforceLabel := determineEnforceLabelForNamespace(nsApplyConfig) nsApply := applyconfiguration.Namespace(ns.Name).WithLabels(map[string]string{ - psapi.EnforceLevelLabel: enforceLabel, + psapi.EnforceLevelLabel: string(enforceLabel), }) _, err = c.kubeClient.CoreV1(). @@ -53,16 +53,22 @@ func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Contex return false, "", nil } -func determineEnforceLabelForNamespace(ns *applyconfiguration.NamespaceApplyConfiguration) string { - if _, ok := ns.Annotations[securityv1.MinimallySufficientPodSecurityStandard]; ok { +func determineEnforceLabelForNamespace(ns *applyconfiguration.NamespaceApplyConfiguration) psapi.Level { + if pssAnnotation, ok := ns.Annotations[securityv1.MinimallySufficientPodSecurityStandard]; ok { // This should generally exist and will be the only supported method of determining // the enforce level going forward - however, we're keeping the label fallback for // now to account for any workloads not yet annotated using a new enough version of // the syncer, such as during upgrade scenarios. - return ns.Annotations[securityv1.MinimallySufficientPodSecurityStandard] + level, err := psapi.ParseLevel(pssAnnotation) + if err == nil { + return level + } + if err != nil { + klog.V(2).InfoS("invalid level in scc annotation", "value", level) + } } - targetLevel := "" + var targetLevel psapi.Level for label := range alertLabels { value, ok := ns.Labels[label] if !ok { @@ -76,18 +82,18 @@ func determineEnforceLabelForNamespace(ns *applyconfiguration.NamespaceApplyConf } if targetLevel == "" { - targetLevel = value + targetLevel = level continue } if psapi.CompareLevels(psapi.Level(targetLevel), level) < 0 { - targetLevel = value + targetLevel = level } } if targetLevel == "" { // Global Config will set it to "restricted", but shouldn't happen. - return string(psapi.LevelRestricted) + return psapi.LevelRestricted } return targetLevel From 3a06aeb897d9e2dee7738fc9296ec84060c0ae1f Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 15:58:57 +0200 Subject: [PATCH 07/15] podsecurityreadinesscontroller: create filter without side-effects --- .../classification.go | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go index 6818b202fb..b033cfe845 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" psapi "k8s.io/pod-security-admission/api" + "k8s.io/pod-security-admission/policy" ) var ( @@ -40,15 +41,12 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace( return nil } - isUserViolation, err := c.isUserViolation(ctx, ns, enforceLevel) + allPods, err := c.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) if err != nil { - klog.V(2).ErrorS(err, "Error checking user violations", "namespace", ns.Name) - // Transient API server error or temporary resource unavailability (most likely). - // Theoretically, psapi parsing errors could occur that retry without hope for recovery. + klog.V(2).ErrorS(err, "Failed to list pods in namespace", "namespace", ns.Name) return err } - - if isUserViolation { + if hasUserSCCViolatingPod(c.psaEvaluator, enforceLevel, allPods.Items) { conditions.addViolatingUserSCC(ns) return nil } @@ -60,26 +58,16 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace( return nil } -func (c *PodSecurityReadinessController) isUserViolation( - ctx context.Context, - ns *corev1.Namespace, +func hasUserSCCViolatingPod( + psaEvaluator policy.Evaluator, enforcementLevel psapi.Level, -) (bool, error) { - allPods, err := c.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) - if err != nil { - klog.V(2).ErrorS(err, "Failed to list pods in namespace", "namespace", ns.Name) - return false, err - } - + pods []corev1.Pod, +) bool { var userPods []corev1.Pod - for _, pod := range allPods.Items { + for _, pod := range pods { if strings.HasPrefix(pod.Annotations[securityv1.ValidatedSCCAnnotation], "restricted-v") { - // restricted-v2 is allowed for all system:authenticated, also for ServiceAccounts. - // But ServiceAccounts are not part of the group. So restricted-v2 will always - // result in user-based SCC. So we skip them as the user-based SCCs cause harm - // if they need a higher privileged than restricted. - // We watch for any restricted version above the first one. We might introduce - // restricted-v3 for user namespaces. + // If the SCC evaluation is restricted-v*, it shouldn't be possible + // to violate as a user-based SCC. continue } @@ -87,25 +75,28 @@ func (c *PodSecurityReadinessController) isUserViolation( userPods = append(userPods, pod) } } - if len(userPods) == 0 { - return false, nil // No user pods = violation is from service accounts + return false // No user pods = violation is based upon service accounts } - enforcementVersion := psapi.LatestVersion() + enforcement := psapi.LevelVersion{ + Level: enforcementLevel, + Version: psapi.LatestVersion(), + } for _, pod := range userPods { - results := c.psaEvaluator.EvaluatePod( - psapi.LevelVersion{Level: enforcementLevel, Version: enforcementVersion}, + results := psaEvaluator.EvaluatePod( + enforcement, &pod.ObjectMeta, &pod.Spec, ) + // results contains between 1 and 2 elements for _, result := range results { if !result.Allowed { - return true, nil + return true } } } - return false, nil + return false } From b6899d26cfa592d74ee4b55ca6f0f214b0c5ea69 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 16:14:05 +0200 Subject: [PATCH 08/15] podsecurityreadinesscontroller: change type from cu to unclassified --- .../classification.go | 4 +--- .../classification_test.go | 14 +++++++------- .../podsecurityreadinesscontroller/conditions.go | 12 +++++++----- .../conditions_test.go | 14 +++++++------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go index b033cfe845..d5230952f2 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -51,9 +51,7 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace( return nil } - // Historically, we assume that this is a customer issue, but - // actually it means we don't know what the root cause is. - conditions.addViolatingCustomer(ns) + conditions.addUnclassifiedIssue(ns) return nil } diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go index a7a30bf29f..11247f58d0 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -55,7 +55,7 @@ func TestClassifyViolatingNamespaceWithAPIErrors(t *testing.T) { } // Ensure no classifications were made due to the error - if len(conditions.violatingCustomerNamespaces) != 0 || + if len(conditions.violatingUnclassifiedNamespaces) != 0 || len(conditions.violatingUserSCCNamespaces) != 0 || len(conditions.violatingOpenShiftNamespaces) != 0 || len(conditions.violatingRunLevelZeroNamespaces) != 0 || @@ -190,7 +190,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ - violatingCustomerNamespaces: []string{"customer-ns"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -224,7 +224,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ - violatingCustomerNamespaces: []string{"customer-ns"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -238,7 +238,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{}, enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ - violatingCustomerNamespaces: []string{"customer-ns"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -268,7 +268,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ - violatingCustomerNamespaces: []string{"customer-ns"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -284,7 +284,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: psapi.LevelPrivileged, expectedConditions: podSecurityOperatorConditions{ - violatingCustomerNamespaces: []string{"customer-ns"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -453,7 +453,7 @@ func deepEqualPodSecurityOperatorConditions( return slices.Equal(a.violatingOpenShiftNamespaces, b.violatingOpenShiftNamespaces) && slices.Equal(a.violatingRunLevelZeroNamespaces, b.violatingRunLevelZeroNamespaces) && - slices.Equal(a.violatingCustomerNamespaces, b.violatingCustomerNamespaces) && + slices.Equal(a.violatingUnclassifiedNamespaces, b.violatingUnclassifiedNamespaces) && slices.Equal(a.violatingDisabledSyncerNamespaces, b.violatingDisabledSyncerNamespaces) && slices.Equal(a.violatingUserSCCNamespaces, b.violatingUserSCCNamespaces) && slices.Equal(a.inconclusiveNamespaces, b.inconclusiveNamespaces) diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions.go b/pkg/operator/podsecurityreadinesscontroller/conditions.go index 2c9a352feb..817b98fa5f 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions.go @@ -12,7 +12,9 @@ import ( ) const ( - PodSecurityCustomerType = "PodSecurityCustomerEvaluationConditionsDetected" + // Historically, we assume that this is a customer issue, but + // actually it means we don't know what the root cause is. + PodSecurityUnknownType = "PodSecurityCustomerEvaluationConditionsDetected" PodSecurityOpenshiftType = "PodSecurityOpenshiftEvaluationConditionsDetected" PodSecurityRunLevelZeroType = "PodSecurityRunLevelZeroEvaluationConditionsDetected" PodSecurityDisabledSyncerType = "PodSecurityDisabledSyncerEvaluationConditionsDetected" @@ -28,9 +30,9 @@ const ( type podSecurityOperatorConditions struct { violatingOpenShiftNamespaces []string violatingRunLevelZeroNamespaces []string - violatingCustomerNamespaces []string violatingDisabledSyncerNamespaces []string violatingUserSCCNamespaces []string + violatingUnclassifiedNamespaces []string inconclusiveNamespaces []string } @@ -50,8 +52,8 @@ func (c *podSecurityOperatorConditions) addViolatingDisabledSyncer(ns *corev1.Na c.violatingDisabledSyncerNamespaces = append(c.violatingDisabledSyncerNamespaces, ns.Name) } -func (c *podSecurityOperatorConditions) addViolatingCustomer(ns *corev1.Namespace) { - c.violatingCustomerNamespaces = append(c.violatingCustomerNamespaces, ns.Name) +func (c *podSecurityOperatorConditions) addUnclassifiedIssue(ns *corev1.Namespace) { + c.violatingUnclassifiedNamespaces = append(c.violatingUnclassifiedNamespaces, ns.Name) } func (c *podSecurityOperatorConditions) addViolatingUserSCC(ns *corev1.Namespace) { @@ -94,7 +96,7 @@ func makeCondition(conditionType, conditionReason string, namespaces []string) o func (c *podSecurityOperatorConditions) toConditionFuncs() []v1helpers.UpdateStatusFunc { return []v1helpers.UpdateStatusFunc{ - v1helpers.UpdateConditionFn(makeCondition(PodSecurityCustomerType, violationReason, c.violatingCustomerNamespaces)), + v1helpers.UpdateConditionFn(makeCondition(PodSecurityUnknownType, violationReason, c.violatingUnclassifiedNamespaces)), v1helpers.UpdateConditionFn(makeCondition(PodSecurityOpenshiftType, violationReason, c.violatingOpenShiftNamespaces)), v1helpers.UpdateConditionFn(makeCondition(PodSecurityRunLevelZeroType, violationReason, c.violatingRunLevelZeroNamespaces)), v1helpers.UpdateConditionFn(makeCondition(PodSecurityDisabledSyncerType, violationReason, c.violatingDisabledSyncerNamespaces)), diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go index e954002c78..ab9897bdcb 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go @@ -13,13 +13,13 @@ func TestCondition(t *testing.T) { t.Run("with violating namespaces", func(t *testing.T) { namespaces := []string{"namespace1", "namespace2"} expectedCondition := operatorv1.OperatorCondition{ - Type: PodSecurityCustomerType, + Type: PodSecurityUnknownType, Status: operatorv1.ConditionTrue, Reason: "PSViolationsDetected", Message: "Violations detected in namespaces: [namespace1 namespace2]", } - condition := makeCondition(PodSecurityCustomerType, violationReason, namespaces) + condition := makeCondition(PodSecurityUnknownType, violationReason, namespaces) if condition.Type != expectedCondition.Type { t.Errorf("expected condition type %s, got %s", expectedCondition.Type, condition.Type) @@ -41,13 +41,13 @@ func TestCondition(t *testing.T) { t.Run("with inconclusive namespaces", func(t *testing.T) { namespaces := []string{"namespace1", "namespace2"} expectedCondition := operatorv1.OperatorCondition{ - Type: PodSecurityCustomerType, + Type: PodSecurityUnknownType, Status: operatorv1.ConditionTrue, Reason: "PSViolationDecisionInconclusive", Message: "Could not evaluate violations for namespaces: [namespace1 namespace2]", } - condition := makeCondition(PodSecurityCustomerType, inconclusiveReason, namespaces) + condition := makeCondition(PodSecurityUnknownType, inconclusiveReason, namespaces) if condition.Type != expectedCondition.Type { t.Errorf("expected condition type %s, got %s", expectedCondition.Type, condition.Type) @@ -69,12 +69,12 @@ func TestCondition(t *testing.T) { t.Run("without namespaces", func(t *testing.T) { namespaces := []string{} expectedCondition := operatorv1.OperatorCondition{ - Type: PodSecurityCustomerType, + Type: PodSecurityUnknownType, Status: operatorv1.ConditionFalse, Reason: "ExpectedReason", } - condition := makeCondition(PodSecurityCustomerType, violationReason, namespaces) + condition := makeCondition(PodSecurityUnknownType, violationReason, namespaces) if condition.Type != expectedCondition.Type { t.Errorf("expected condition type %s, got %s", expectedCondition.Type, condition.Type) @@ -298,7 +298,7 @@ func TestOperatorStatus(t *testing.T) { cond.addViolatingDisabledSyncer(ns) } else { // Default to customer violation for test purposes - cond.addViolatingCustomer(ns) + cond.addUnclassifiedIssue(ns) } } if tt.addInconclusive { From ca7b9ab6b725baf001392cda08c2d3d53b2a4c7a Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 16:16:45 +0200 Subject: [PATCH 09/15] podsecurityreadinesscontroller: change actual CFE name --- pkg/operator/podsecurityreadinesscontroller/conditions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions.go b/pkg/operator/podsecurityreadinesscontroller/conditions.go index 817b98fa5f..ac3c8a3819 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions.go @@ -14,7 +14,7 @@ import ( const ( // Historically, we assume that this is a customer issue, but // actually it means we don't know what the root cause is. - PodSecurityUnknownType = "PodSecurityCustomerEvaluationConditionsDetected" + PodSecurityUnknownType = "PodSecurityUnknownEvaluationConditionsDetected" PodSecurityOpenshiftType = "PodSecurityOpenshiftEvaluationConditionsDetected" PodSecurityRunLevelZeroType = "PodSecurityRunLevelZeroEvaluationConditionsDetected" PodSecurityDisabledSyncerType = "PodSecurityDisabledSyncerEvaluationConditionsDetected" From 177afc1379f330200bc92e452acbfad8d70f13ba Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 17:17:48 +0200 Subject: [PATCH 10/15] podsecurityreadinesscontroller: fix tests after renaming --- .../classification_test.go | 4 ++-- .../conditions_test.go | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go index 11247f58d0..3b152d16bc 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -195,8 +195,8 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectError: false, }, { - // TODO: Ideally we would not drop the "customer" condition. - name: "customer namespace with mixed pods - user violates", + // TODO: Ideally we would not drop the "unknown" condition. + name: "customer namespace with mixed pods - unknown violation included", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "customer-ns", diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go index ab9897bdcb..901e8b8cdd 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go @@ -114,7 +114,7 @@ func TestOperatorStatus(t *testing.T) { addViolation: true, addInconclusive: false, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionTrue, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, @@ -136,7 +136,7 @@ func TestOperatorStatus(t *testing.T) { }, addViolation: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionTrue, @@ -158,7 +158,7 @@ func TestOperatorStatus(t *testing.T) { }, addViolation: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionTrue, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, @@ -177,7 +177,7 @@ func TestOperatorStatus(t *testing.T) { }, addViolation: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, @@ -196,7 +196,7 @@ func TestOperatorStatus(t *testing.T) { }, addViolation: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, @@ -223,7 +223,7 @@ func TestOperatorStatus(t *testing.T) { }, addViolation: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionTrue, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionTrue, @@ -254,7 +254,7 @@ func TestOperatorStatus(t *testing.T) { }, addViolation: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionTrue, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, @@ -274,7 +274,7 @@ func TestOperatorStatus(t *testing.T) { addViolation: false, addInconclusive: true, expected: map[string]operatorv1.ConditionStatus{ - "PodSecurityCustomerEvaluationConditionsDetected": operatorv1.ConditionFalse, + "PodSecurityUnknownEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityOpenshiftEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityRunLevelZeroEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityDisabledSyncerEvaluationConditionsDetected": operatorv1.ConditionFalse, From 13a3228f062b64fc2cb291c06f9e0d24e27a1128 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 17:45:18 +0200 Subject: [PATCH 11/15] podsecurityreadinesscontroller: add fine-grained classification --- .../classification.go | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go index d5230952f2..69f150b3cd 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -41,60 +41,58 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace( return nil } + // Evaluate by individual pod. allPods, err := c.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) if err != nil { klog.V(2).ErrorS(err, "Failed to list pods in namespace", "namespace", ns.Name) return err } - if hasUserSCCViolatingPod(c.psaEvaluator, enforceLevel, allPods.Items) { - conditions.addViolatingUserSCC(ns) - return nil - } - conditions.addUnclassifiedIssue(ns) - - return nil -} - -func hasUserSCCViolatingPod( - psaEvaluator policy.Evaluator, - enforcementLevel psapi.Level, - pods []corev1.Pod, -) bool { - var userPods []corev1.Pod - for _, pod := range pods { - if strings.HasPrefix(pod.Annotations[securityv1.ValidatedSCCAnnotation], "restricted-v") { - // If the SCC evaluation is restricted-v*, it shouldn't be possible - // to violate as a user-based SCC. - continue + isViolating := createPodViolationEvaluator(c.psaEvaluator, enforceLevel) + violatingPods := []corev1.Pod{} + for _, pod := range allPods.Items { + if isViolating(pod) { + violatingPods = append(violatingPods, pod) } + } + if len(violatingPods) == 0 { + conditions.addInconclusive(ns) + klog.V(2).InfoS("no violating pods found in namespace, marking as inconclusive", "namespace", ns.Name) + return nil + } + violatingUserSCCPods := []corev1.Pod{} + for _, pod := range violatingPods { if pod.Annotations[securityv1.ValidatedSCCSubjectTypeAnnotation] == "user" { - userPods = append(userPods, pod) + violatingUserSCCPods = append(violatingUserSCCPods, pod) } } - if len(userPods) == 0 { - return false // No user pods = violation is based upon service accounts + if len(violatingUserSCCPods) > 0 { + conditions.addViolatingUserSCC(ns) + } + if len(violatingUserSCCPods) != len(violatingPods) { + conditions.addUnclassifiedIssue(ns) } - enforcement := psapi.LevelVersion{ - Level: enforcementLevel, - Version: psapi.LatestVersion(), - } - for _, pod := range userPods { - results := psaEvaluator.EvaluatePod( - enforcement, + return nil +} + +func createPodViolationEvaluator(evaluator policy.Evaluator, enforcement psapi.Level) func(pod corev1.Pod) bool { + return func(pod corev1.Pod) bool { + results := evaluator.EvaluatePod( + psapi.LevelVersion{ + Level: enforcement, + Version: psapi.LatestVersion(), + }, &pod.ObjectMeta, &pod.Spec, ) - // results contains between 1 and 2 elements for _, result := range results { if !result.Allowed { return true } } + return false } - - return false } From 7fb29f9dbe3046397b82e6ed5090bd1863af80d1 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 31 Jul 2025 19:00:10 +0200 Subject: [PATCH 12/15] podsecurityreadinesscontroller: fix tests to match new logic --- .../classification_test.go | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go index 3b152d16bc..b0af5437b7 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -115,6 +115,20 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, expectError: false, }, + { + name: "run-level zero namespace - kube-node-lease", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-node-lease", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + violatingRunLevelZeroNamespaces: []string{"kube-node-lease"}, + }, + expectError: false, + }, { name: "openshift namespace", namespace: &corev1.Namespace{ @@ -179,7 +193,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectError: false, }, { - name: "customer namespace without user SCC violation", + name: "customer namespace with a pod that passed SA-based SCC, but not PSA", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "customer-ns", @@ -195,7 +209,6 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectError: false, }, { - // TODO: Ideally we would not drop the "unknown" condition. name: "customer namespace with mixed pods - unknown violation included", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -208,7 +221,8 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: "restricted", expectedConditions: podSecurityOperatorConditions{ - violatingUserSCCNamespaces: []string{"customer-ns"}, + violatingUserSCCNamespaces: []string{"customer-ns"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -224,7 +238,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ - violatingUnclassifiedNamespaces: []string{"customer-ns"}, + inconclusiveNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -238,7 +252,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { pods: []corev1.Pod{}, enforceLevel: psapi.LevelRestricted, expectedConditions: podSecurityOperatorConditions{ - violatingUnclassifiedNamespaces: []string{"customer-ns"}, + inconclusiveNamespaces: []string{"customer-ns"}, }, expectError: false, }, @@ -284,7 +298,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { }, enforceLevel: psapi.LevelPrivileged, expectedConditions: podSecurityOperatorConditions{ - violatingUnclassifiedNamespaces: []string{"customer-ns"}, + inconclusiveNamespaces: []string{"customer-ns"}, }, expectError: false, }, From 0f47326b0f3efdcfa354a7d6d136c3af3b2cd3a6 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Fri, 1 Aug 2025 17:48:18 +0200 Subject: [PATCH 13/15] podsecurityreadinesscontroller: remove tautological condition --- pkg/operator/podsecurityreadinesscontroller/violation.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/violation.go b/pkg/operator/podsecurityreadinesscontroller/violation.go index 5722d3e7fd..fe661aae47 100644 --- a/pkg/operator/podsecurityreadinesscontroller/violation.go +++ b/pkg/operator/podsecurityreadinesscontroller/violation.go @@ -63,9 +63,8 @@ func determineEnforceLabelForNamespace(ns *applyconfiguration.NamespaceApplyConf if err == nil { return level } - if err != nil { - klog.V(2).InfoS("invalid level in scc annotation", "value", level) - } + + klog.V(2).InfoS("invalid level in scc annotation", "value", level) } var targetLevel psapi.Level From fa2281e2fc701460e70584afdb9c7ad29e0ab1f8 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Fri, 1 Aug 2025 21:09:24 +0200 Subject: [PATCH 14/15] podsecurityreadinesscontroller: return err on no violating pod Returning an error will put the namespace into inconclusive condition. --- .../podsecurityreadinesscontroller/classification.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification.go b/pkg/operator/podsecurityreadinesscontroller/classification.go index 69f150b3cd..b9f227f197 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -2,6 +2,7 @@ package podsecurityreadinesscontroller import ( "context" + "errors" "strings" securityv1 "github.com/openshift/api/security/v1" @@ -20,6 +21,7 @@ var ( "kube-public", "kube-node-lease", ) + errNoViolatingPods = errors.New("no violating pods in violating namespace") ) func (c *PodSecurityReadinessController) classifyViolatingNamespace( @@ -44,6 +46,8 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace( // Evaluate by individual pod. allPods, err := c.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) if err != nil { + // Will end up in inconclusive as we couldn't diagnose the violation root + // cause. klog.V(2).ErrorS(err, "Failed to list pods in namespace", "namespace", ns.Name) return err } @@ -56,9 +60,8 @@ func (c *PodSecurityReadinessController) classifyViolatingNamespace( } } if len(violatingPods) == 0 { - conditions.addInconclusive(ns) - klog.V(2).InfoS("no violating pods found in namespace, marking as inconclusive", "namespace", ns.Name) - return nil + klog.V(2).ErrorS(errNoViolatingPods, "failed to find violating pod", "namespace", ns.Name) + return errNoViolatingPods } violatingUserSCCPods := []corev1.Pod{} From 6c0f461dc853a7c939b85ae6053723496069524a Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Fri, 1 Aug 2025 21:12:07 +0200 Subject: [PATCH 15/15] podsecurityreadinesscontroller: adjust unit tests to reflect change --- .../podsecurityreadinesscontroller/classification_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go index b0af5437b7..a2dcaf0142 100644 --- a/pkg/operator/podsecurityreadinesscontroller/classification_test.go +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -227,7 +227,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectError: false, }, { - name: "customer namespace with user pods that pass PSA", + name: "customer namespace with non violating user pod", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "customer-ns", @@ -240,7 +240,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectedConditions: podSecurityOperatorConditions{ inconclusiveNamespaces: []string{"customer-ns"}, }, - expectError: false, + expectError: true, }, { name: "customer namespace with no pods", @@ -254,7 +254,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectedConditions: podSecurityOperatorConditions{ inconclusiveNamespaces: []string{"customer-ns"}, }, - expectError: false, + expectError: true, }, { name: "customer namespace with pods without SCC annotation", @@ -300,7 +300,7 @@ func TestClassifyViolatingNamespace(t *testing.T) { expectedConditions: podSecurityOperatorConditions{ inconclusiveNamespaces: []string{"customer-ns"}, }, - expectError: false, + expectError: true, }, } { t.Run(tt.name, func(t *testing.T) {