diff --git a/README.md b/README.md index 7700a3a..2dafe4a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ It is not needed for the portfolio demo and is not run in CI. - [ ] Phase 2: ArgoCD app-of-apps deployment (scaffolded; runtime verification pending) - [x] Phase 3: kube-bench CI scan - [ ] Architecture revision: Flatcar hosts and Cilium kube-proxy replacement (configured; runtime verification pending) -- [ ] Phase 4: Kyverno policy set and CIS mapping +- [ ] Phase 4: Kyverno policy set and CIS mapping (configured; runtime verification pending) - [ ] Phase 5: Falco rules and webhook alerting - [ ] Phase 6: test workloads and evidence capture - [ ] Phase 7: architecture/design documentation diff --git a/docs/cis-policy-mapping.md b/docs/cis-policy-mapping.md new file mode 100644 index 0000000..c01c8f9 --- /dev/null +++ b/docs/cis-policy-mapping.md @@ -0,0 +1,19 @@ +# CIS control to Kyverno mapping + +The policy set is scoped to namespaces carrying the +`security-baseline/open: "true"` label. This makes the local demo predictable +and avoids applying workload admission rules to platform namespaces. + +| CIS control | Policy | What the demo proves | Boundary | +| --- | --- | --- | --- | +| 5.2.2 | `disallow-privileged` | A Pod with `privileged: true` is rejected. | Does not replace host hardening or Linux capability review. | +| 5.2.3 | `disallow-host-network` | A Pod with `hostPID: true` is rejected. | Does not cover every host namespace or hostPath risk. | +| 5.2.5 | `disallow-host-network` | A Pod with `hostNetwork: true` is rejected. | Does not replace network policy. | +| 5.2.7 | `require-non-root` | A Pod without explicit non-root execution is rejected. | Does not prove the image cannot switch users internally. | +| 5.5.1 | `restrict-image-registries` | Images outside the approved registry set are rejected. | Full provenance needs signing, verification, and trusted build controls. | +| 5.7.3 supplemental | `require-resource-limits` | Pods must declare CPU and memory limits. | Resource limits are a defense-in-depth control, not a direct CIS test. | + +The Kubernetes CIS Benchmark is broader than admission policy. kube-bench +covers node, control-plane, RBAC, audit, network policy, and manual review +areas that these Kyverno policies do not implement. + diff --git a/policies/README.md b/policies/README.md new file mode 100644 index 0000000..b0ffe88 --- /dev/null +++ b/policies/README.md @@ -0,0 +1,24 @@ +# Kyverno policy baseline + +These policies are `ClusterPolicy` resources, but they intentionally select +only namespaces labeled `security-baseline/open: "true"`. That keeps the +portfolio demo enforceable without blocking Kyverno, Falco, ArgoCD, or other +platform components. Expand the label scope deliberately for a real cluster. + +All five policies use `validationFailureAction: Enforce`, so a matching Pod is +rejected at admission time. The policies use `background: false` because the +goal is to demonstrate admission enforcement, not retroactively evaluate +existing objects. + +| Policy | CIS alignment | Enforcement | +| --- | --- | --- | +| `disallow-privileged` | 5.2.2 | Rejects privileged containers and init containers. | +| `require-non-root` | 5.2.7 | Requires `runAsNonRoot: true` on containers and init containers. | +| `require-resource-limits` | 5.7.3 supplemental | Requires CPU and memory limits. This is a supplemental control, not a one-to-one CIS test. | +| `restrict-image-registries` | 5.5.1 supplemental | Allows only approved registries. Full image provenance requires signing and verification as well. | +| `disallow-host-network` | 5.2.3 and 5.2.5 | Rejects host PID, host network, and host IPC access. | + +The CIS benchmark includes manual checks and platform-specific exceptions. +The mapping above describes the control objective each policy supports, not a +claim that one Kyverno rule implements the entire benchmark control. + diff --git a/policies/kyverno/disallow-host-network.yaml b/policies/kyverno/disallow-host-network.yaml new file mode 100644 index 0000000..c8ab77d --- /dev/null +++ b/policies/kyverno/disallow-host-network.yaml @@ -0,0 +1,32 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-host-network + annotations: + policies.kyverno.io/title: Disallow host network and host PID + policies.kyverno.io/category: Pod Security Standards + policies.kyverno.io/severity: high + # CIS 5.2.3: minimize hostPID; CIS 5.2.5: minimize hostNetwork. + policies.kyverno.io/cis-control: "5.2.3, 5.2.5" +spec: + validationFailureAction: Enforce + background: false + failurePolicy: Fail + rules: + - name: no-host-network-or-pid + match: + any: + - resources: + kinds: + - Pod + namespaceSelector: + matchLabels: + security-baseline/open: "true" + validate: + message: hostNetwork, hostPID, and hostIPC must be false or omitted. + pattern: + spec: + =(hostNetwork): false + =(hostPID): false + =(hostIPC): false + diff --git a/policies/kyverno/disallow-privileged.yaml b/policies/kyverno/disallow-privileged.yaml new file mode 100644 index 0000000..21f6cf2 --- /dev/null +++ b/policies/kyverno/disallow-privileged.yaml @@ -0,0 +1,42 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-privileged + annotations: + policies.kyverno.io/title: Disallow privileged containers + policies.kyverno.io/category: Pod Security Standards + policies.kyverno.io/severity: high + # CIS 5.2.2: minimize admission of privileged containers. + policies.kyverno.io/cis-control: "5.2.2" +spec: + validationFailureAction: Enforce + background: false + failurePolicy: Fail + rules: + - name: privileged-containers + match: + any: + - resources: + kinds: + - Pod + namespaceSelector: + matchLabels: + security-baseline/open: "true" + validate: + message: Privileged containers are not allowed in security-baseline namespaces. + foreach: + - list: request.object.spec.containers[] + deny: + conditions: + any: + - key: "{{ element.securityContext.privileged || false }}" + operator: Equals + value: true + - list: request.object.spec.initContainers[] + deny: + conditions: + any: + - key: "{{ element.securityContext.privileged || false }}" + operator: Equals + value: true + diff --git a/policies/kyverno/kustomization.yaml b/policies/kyverno/kustomization.yaml index 076d7f1..c98c436 100644 --- a/policies/kyverno/kustomization.yaml +++ b/policies/kyverno/kustomization.yaml @@ -2,5 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - namespace.yaml - - phase-2-placeholder.yaml - + - disallow-privileged.yaml + - require-non-root.yaml + - require-resource-limits.yaml + - restrict-image-registries.yaml + - disallow-host-network.yaml diff --git a/policies/kyverno/phase-2-placeholder.yaml b/policies/kyverno/phase-2-placeholder.yaml deleted file mode 100644 index ea868e1..0000000 --- a/policies/kyverno/phase-2-placeholder.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: policy-baseline-status - namespace: security-baseline -data: - phase: "2" - status: "GitOps target established; Kyverno policies arrive in phase 4" - diff --git a/policies/kyverno/require-non-root.yaml b/policies/kyverno/require-non-root.yaml new file mode 100644 index 0000000..c37af75 --- /dev/null +++ b/policies/kyverno/require-non-root.yaml @@ -0,0 +1,42 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-non-root + annotations: + policies.kyverno.io/title: Require non-root containers + policies.kyverno.io/category: Pod Security Standards + policies.kyverno.io/severity: high + # CIS 5.2.7: minimize admission of root containers. + policies.kyverno.io/cis-control: "5.2.7" +spec: + validationFailureAction: Enforce + background: false + failurePolicy: Fail + rules: + - name: containers-run-as-non-root + match: + any: + - resources: + kinds: + - Pod + namespaceSelector: + matchLabels: + security-baseline/open: "true" + validate: + message: Containers must explicitly set runAsNonRoot=true. + foreach: + - list: request.object.spec.containers[] + deny: + conditions: + any: + - key: "{{ element.securityContext.runAsNonRoot || false }}" + operator: Equals + value: false + - list: request.object.spec.initContainers[] + deny: + conditions: + any: + - key: "{{ element.securityContext.runAsNonRoot || false }}" + operator: Equals + value: false + diff --git a/policies/kyverno/require-resource-limits.yaml b/policies/kyverno/require-resource-limits.yaml new file mode 100644 index 0000000..17ffa53 --- /dev/null +++ b/policies/kyverno/require-resource-limits.yaml @@ -0,0 +1,49 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-resource-limits + annotations: + policies.kyverno.io/title: Require CPU and memory limits + policies.kyverno.io/category: Resource governance + policies.kyverno.io/severity: medium + # Supplemental hardening aligned with CIS 5.7.3 workload controls. The + # CIS benchmark does not define a one-to-one resource-limit test. + policies.kyverno.io/cis-control: "5.7.3 supplemental" +spec: + validationFailureAction: Enforce + background: false + failurePolicy: Fail + rules: + - name: containers-have-resource-limits + match: + any: + - resources: + kinds: + - Pod + namespaceSelector: + matchLabels: + security-baseline/open: "true" + validate: + message: Every container must define CPU and memory limits. + foreach: + - list: request.object.spec.containers[] + deny: + conditions: + any: + - key: "{{ element.resources.limits.cpu || '' }}" + operator: Equals + value: "" + - key: "{{ element.resources.limits.memory || '' }}" + operator: Equals + value: "" + - list: request.object.spec.initContainers[] + deny: + conditions: + any: + - key: "{{ element.resources.limits.cpu || '' }}" + operator: Equals + value: "" + - key: "{{ element.resources.limits.memory || '' }}" + operator: Equals + value: "" + diff --git a/policies/kyverno/restrict-image-registries.yaml b/policies/kyverno/restrict-image-registries.yaml new file mode 100644 index 0000000..cdab9a8 --- /dev/null +++ b/policies/kyverno/restrict-image-registries.yaml @@ -0,0 +1,39 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: restrict-image-registries + annotations: + policies.kyverno.io/title: Restrict image registries + policies.kyverno.io/category: Supply chain + policies.kyverno.io/severity: high + # CIS 5.5.1: image provenance. This is a concrete admission control that + # supports the broader provenance objective but is not the full CIS test. + policies.kyverno.io/cis-control: "5.5.1 supplemental" +spec: + validationFailureAction: Enforce + background: false + failurePolicy: Fail + rules: + - name: images-from-approved-registries + match: + any: + - resources: + kinds: + - Pod + namespaceSelector: + matchLabels: + security-baseline/open: "true" + validate: + message: Images must come from ghcr.io, quay.io, or docker.io/library. + foreach: + - list: request.object.spec.containers[] + anyPattern: + - image: ghcr.io/* + - image: quay.io/* + - image: docker.io/library/* + - list: request.object.spec.initContainers[] + anyPattern: + - image: ghcr.io/* + - image: quay.io/* + - image: docker.io/library/* +