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..b9f227f197 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/classification.go @@ -0,0 +1,101 @@ +package podsecurityreadinesscontroller + +import ( + "context" + "errors" + "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" + "k8s.io/pod-security-admission/policy" +) + +var ( + runLevelZeroNamespaces = sets.New[string]( + "default", + "kube-system", + "kube-public", + "kube-node-lease", + ) + errNoViolatingPods = errors.New("no violating pods in violating namespace") +) + +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 + } + if strings.HasPrefix(ns.Name, "openshift") { + conditions.addViolatingOpenShift(ns) + return nil + } + if ns.Labels[labelSyncControlLabel] == "false" { + conditions.addViolatingDisabledSyncer(ns) + return nil + } + + // 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 + } + + isViolating := createPodViolationEvaluator(c.psaEvaluator, enforceLevel) + violatingPods := []corev1.Pod{} + for _, pod := range allPods.Items { + if isViolating(pod) { + violatingPods = append(violatingPods, pod) + } + } + if len(violatingPods) == 0 { + klog.V(2).ErrorS(errNoViolatingPods, "failed to find violating pod", "namespace", ns.Name) + return errNoViolatingPods + } + + violatingUserSCCPods := []corev1.Pod{} + for _, pod := range violatingPods { + if pod.Annotations[securityv1.ValidatedSCCSubjectTypeAnnotation] == "user" { + violatingUserSCCPods = append(violatingUserSCCPods, pod) + } + } + if len(violatingUserSCCPods) > 0 { + conditions.addViolatingUserSCC(ns) + } + if len(violatingUserSCCPods) != len(violatingPods) { + conditions.addUnclassifiedIssue(ns) + } + + 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, + ) + + for _, result := range results { + if !result.Allowed { + return true + } + } + return false + } +} diff --git a/pkg/operator/podsecurityreadinesscontroller/classification_test.go b/pkg/operator/podsecurityreadinesscontroller/classification_test.go new file mode 100644 index 0000000000..a2dcaf0142 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/classification_test.go @@ -0,0 +1,474 @@ +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" + psapi "k8s.io/pod-security-admission/api" + "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.violatingUnclassifiedNamespaces) != 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 psapi.Level + expectedConditions podSecurityOperatorConditions + expectError bool + }{ + { + name: "run-level zero namespace - kube-system", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: psapi.LevelRestricted, + 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: psapi.LevelRestricted, + 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: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + violatingRunLevelZeroNamespaces: []string{"kube-public"}, + }, + 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{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-test", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: psapi.LevelRestricted, + 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: psapi.LevelRestricted, + 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: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + violatingUserSCCNamespaces: []string{"user-scc-violation-test"}, + }, + expectError: false, + }, + { + name: "customer namespace with a pod that passed SA-based SCC, but not PSA", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newServiceAccountPod("sa-pod", "customer-ns"), + }, + enforceLevel: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + violatingUnclassifiedNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "customer namespace with mixed pods - unknown violation included", + 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"}, + violatingUnclassifiedNamespaces: []string{"customer-ns"}, + }, + expectError: false, + }, + { + name: "customer namespace with non violating user pod", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{ + newUserSCCPodRestricted("user-pod", "customer-ns"), + }, + enforceLevel: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + inconclusiveNamespaces: []string{"customer-ns"}, + }, + expectError: true, + }, + { + name: "customer namespace with no pods", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "customer-ns", + }, + }, + pods: []corev1.Pod{}, + enforceLevel: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + inconclusiveNamespaces: []string{"customer-ns"}, + }, + expectError: true, + }, + { + 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: psapi.LevelRestricted, + expectedConditions: podSecurityOperatorConditions{ + violatingUnclassifiedNamespaces: []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: psapi.LevelPrivileged, + expectedConditions: podSecurityOperatorConditions{ + inconclusiveNamespaces: []string{"customer-ns"}, + }, + 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.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 7d8f09f536..ac3c8a3819 100644 --- a/pkg/operator/podsecurityreadinesscontroller/conditions.go +++ b/pkg/operator/podsecurityreadinesscontroller/conditions.go @@ -3,22 +3,23 @@ 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" ) 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 = "PodSecurityUnknownEvaluationConditionsDetected" PodSecurityOpenshiftType = "PodSecurityOpenshiftEvaluationConditionsDetected" PodSecurityRunLevelZeroType = "PodSecurityRunLevelZeroEvaluationConditionsDetected" PodSecurityDisabledSyncerType = "PodSecurityDisabledSyncerEvaluationConditionsDetected" PodSecurityInconclusiveType = "PodSecurityInconclusiveEvaluationConditionsDetected" + PodSecurityUserSCCType = "PodSecurityUserSCCEvaluationConditionsDetected" labelSyncControlLabel = "security.openshift.io/scc.podSecurityLabelSync" @@ -26,46 +27,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 + violatingUnclassifiedNamespaces []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) +} - c.violatingCustomerNamespaces = append(c.violatingCustomerNamespaces, ns.Name) +func (c *podSecurityOperatorConditions) addViolatingDisabledSyncer(ns *corev1.Namespace) { + c.violatingDisabledSyncerNamespaces = append(c.violatingDisabledSyncerNamespaces, ns.Name) } -func (c *podSecurityOperatorConditions) addInconclusive(ns *corev1.Namespace) { - c.inconclusiveNamespaces = append(c.inconclusiveNamespaces, ns.Name) +func (c *podSecurityOperatorConditions) addUnclassifiedIssue(ns *corev1.Namespace) { + c.violatingUnclassifiedNamespaces = append(c.violatingUnclassifiedNamespaces, 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 { @@ -104,10 +96,11 @@ 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)), + 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..901e8b8cdd 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" @@ -12,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) @@ -40,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) @@ -68,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) @@ -113,10 +114,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -134,10 +136,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -155,10 +158,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -173,10 +177,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -191,10 +196,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -217,10 +223,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -247,10 +254,11 @@ 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, + "PodSecurityUserSCCEvaluationConditionsDetected": operatorv1.ConditionFalse, "PodSecurityInconclusiveEvaluationConditionsDetected": operatorv1.ConditionFalse, }, }, @@ -266,10 +274,11 @@ 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, + "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.addUnclassifiedIssue(ns) + } } if tt.addInconclusive { cond.addInconclusive(ns) diff --git a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go index 080546e4dd..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) 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) } diff --git a/pkg/operator/podsecurityreadinesscontroller/violation.go b/pkg/operator/podsecurityreadinesscontroller/violation.go index 97a0ed3fe7..fe661aae47 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,19 +20,18 @@ 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, psapi.Level, 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, + psapi.EnforceLevelLabel: string(enforceLabel), }) _, err = c.kubeClient.CoreV1(). @@ -43,41 +41,39 @@ 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) 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 label, nil - } - - viableLabels := map[string]string{} - - for alertLabel := range alertLabels { - if value, ok := ns.Labels[alertLabel]; ok { - viableLabels[alertLabel] = value + level, err := psapi.ParseLevel(pssAnnotation) + if err == nil { + return level } - } - 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") + klog.V(2).InfoS("invalid level in scc annotation", "value", level) } - return pickStrictest(viableLabels), nil -} + var targetLevel psapi.Level + for label := range alertLabels { + value, ok := ns.Labels[label] + if !ok { + continue + } -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) @@ -85,18 +81,18 @@ func pickStrictest(viableLabels map[string]string) string { } 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 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) 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