diff --git a/scripts/proxmox/create-flatcar-k3d-test-host.sh b/scripts/proxmox/create-flatcar-k3d-test-host.sh index 0add5a7..2fd126f 100755 --- a/scripts/proxmox/create-flatcar-k3d-test-host.sh +++ b/scripts/proxmox/create-flatcar-k3d-test-host.sh @@ -17,6 +17,7 @@ Required environment variables: Optional environment variables: VM_NAME VM name, default flatcar-k3d-test CORES CPU cores, default 4 + CPU_TYPE Proxmox CPU model, default host passthrough MEMORY_MB Memory in MiB, default 16384 DISK_SIZE Final disk size, default 100G BRIDGE Proxmox bridge, default vmbr0 @@ -56,6 +57,7 @@ done VM_NAME="${VM_NAME:-flatcar-k3d-test}" CORES="${CORES:-4}" +CPU_TYPE="${CPU_TYPE:-host}" MEMORY_MB="${MEMORY_MB:-16384}" DISK_SIZE="${DISK_SIZE:-100G}" BRIDGE="${BRIDGE:-vmbr0}" @@ -181,6 +183,7 @@ qm create "$VM_ID" \ --name "$VM_NAME" \ --ostype l26 \ --cores "$CORES" \ + --cpu "cputype=$CPU_TYPE" \ --memory "$MEMORY_MB" \ --net0 "virtio,bridge=$BRIDGE" \ --ipconfig0 ip=dhcp \ diff --git a/scripts/proxmox/k3d-test-host.md b/scripts/proxmox/k3d-test-host.md index d4d8ce9..81adeb2 100644 --- a/scripts/proxmox/k3d-test-host.md +++ b/scripts/proxmox/k3d-test-host.md @@ -8,6 +8,9 @@ The default profile is 4 CPU cores, 16 GiB memory, and a 100 GiB disk. The Ignition configuration enables Docker, creates `/opt/k3d-test/workspace`, and installs pinned k3d and kubectl binaries on first boot. +The VM defaults to Proxmox CPU passthrough. This is important for Falco and +modern container images that require x86-64-v2 CPU features. + Enable snippets on the Proxmox storage first: ```bash diff --git a/test-workloads/README.md b/test-workloads/README.md new file mode 100644 index 0000000..0bbb28b --- /dev/null +++ b/test-workloads/README.md @@ -0,0 +1,32 @@ +# Security test workloads + +The two manifests in the Kustomization are intentionally allowed through +Kyverno. ArgoCD manages them so the baseline always has one admission-pass +workload and one runtime-alert workload. + +The manifests under `blocked/` are negative tests. They are intentionally not +included in the Kustomization because ArgoCD would continuously report them as +failed syncs. Apply them manually and expect admission rejection: + +```bash +kubectl apply --server-side --dry-run=server -f test-workloads/blocked/privileged-pod.yaml +kubectl apply --server-side --dry-run=server -f test-workloads/blocked/root-container.yaml +kubectl apply --server-side --dry-run=server -f test-workloads/blocked/host-network-pod.yaml +kubectl apply --server-side --dry-run=server -f test-workloads/blocked/unapproved-registry-pod.yaml +kubectl apply --server-side --dry-run=server -f test-workloads/blocked/default-namespace-pod.yaml +``` + +Expected policy coverage: + +| Manifest | Expected result | Policy or detector | +| --- | --- | --- | +| `blocked/privileged-pod.yaml` | Rejected | `disallow-privileged`, CIS 5.2.2 | +| `blocked/root-container.yaml` | Rejected | `require-non-root`, CIS 5.2.7 | +| `blocked/host-network-pod.yaml` | Rejected | `disallow-host-network`, CIS 5.2.3 and 5.2.5 | +| `blocked/unapproved-registry-pod.yaml` | Rejected | `restrict-image-registries`, supplemental supply-chain control | +| `blocked/default-namespace-pod.yaml` | Rejected | `disallow-default-namespace`, supplemental namespace control | +| `compliant-workload.yaml` | Accepted | Admission baseline | +| `suspicious-shell.yaml` | Accepted, then alerted | Falco shell detection | + +The suspicious workload is intentionally compliant at admission. Its shell +process is the runtime event that Falco should report. diff --git a/test-workloads/blocked/default-namespace-pod.yaml b/test-workloads/blocked/default-namespace-pod.yaml new file mode 100644 index 0000000..a1eecb3 --- /dev/null +++ b/test-workloads/blocked/default-namespace-pod.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: blocked-default-namespace + namespace: default +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + containers: + - name: app + image: docker.io/library/busybox:1.36 + command: ["sleep", "3600"] + securityContext: + runAsNonRoot: true + runAsUser: 1000 + resources: + limits: + cpu: 100m + memory: 64Mi diff --git a/test-workloads/blocked/host-network-pod.yaml b/test-workloads/blocked/host-network-pod.yaml new file mode 100644 index 0000000..14ded79 --- /dev/null +++ b/test-workloads/blocked/host-network-pod.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Pod +metadata: + name: blocked-host-network + namespace: security-baseline +spec: + hostNetwork: true + hostPID: true + hostIPC: true + restartPolicy: Never + securityContext: + runAsNonRoot: true + containers: + - name: app + image: docker.io/library/busybox:1.36 + command: ["sleep", "3600"] + securityContext: + runAsNonRoot: true + runAsUser: 1000 + resources: + limits: + cpu: 100m + memory: 64Mi diff --git a/test-workloads/blocked/privileged-pod.yaml b/test-workloads/blocked/privileged-pod.yaml new file mode 100644 index 0000000..4eb4afc --- /dev/null +++ b/test-workloads/blocked/privileged-pod.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: blocked-privileged + namespace: security-baseline +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + containers: + - name: app + image: docker.io/library/busybox:1.36 + command: ["sleep", "3600"] + securityContext: + privileged: true + runAsNonRoot: true + runAsUser: 1000 + resources: + limits: + cpu: 100m + memory: 64Mi diff --git a/test-workloads/blocked/root-container.yaml b/test-workloads/blocked/root-container.yaml new file mode 100644 index 0000000..c5e410e --- /dev/null +++ b/test-workloads/blocked/root-container.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: blocked-root + namespace: security-baseline +spec: + restartPolicy: Never + containers: + - name: app + image: docker.io/library/busybox:1.36 + command: ["sleep", "3600"] + securityContext: + runAsNonRoot: false + runAsUser: 0 + resources: + limits: + cpu: 100m + memory: 64Mi diff --git a/test-workloads/blocked/unapproved-registry-pod.yaml b/test-workloads/blocked/unapproved-registry-pod.yaml new file mode 100644 index 0000000..a7a9d02 --- /dev/null +++ b/test-workloads/blocked/unapproved-registry-pod.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: blocked-unapproved-registry + namespace: security-baseline +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + containers: + - name: app + image: registry.example.com/security-test:1.0 + command: ["sleep", "3600"] + securityContext: + runAsNonRoot: true + runAsUser: 1000 + resources: + limits: + cpu: 100m + memory: 64Mi diff --git a/test-workloads/compliant-workload.yaml b/test-workloads/compliant-workload.yaml new file mode 100644 index 0000000..bb9fc6e --- /dev/null +++ b/test-workloads/compliant-workload.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Pod +metadata: + name: compliant-workload + namespace: security-baseline + labels: + app.kubernetes.io/name: compliant-workload + security-baseline/test: admission-pass +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: app + image: docker.io/library/busybox:1.36 + command: ["sh", "-c", "echo compliant-workload-ready; sleep 3600"] + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi diff --git a/test-workloads/kustomization.yaml b/test-workloads/kustomization.yaml index 3692bef..60e263b 100644 --- a/test-workloads/kustomization.yaml +++ b/test-workloads/kustomization.yaml @@ -1,5 +1,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - phase-2-placeholder.yaml - + - compliant-workload.yaml + - suspicious-shell.yaml diff --git a/test-workloads/suspicious-shell.yaml b/test-workloads/suspicious-shell.yaml new file mode 100644 index 0000000..6e68fc8 --- /dev/null +++ b/test-workloads/suspicious-shell.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Pod +metadata: + name: suspicious-shell + namespace: security-baseline + labels: + app.kubernetes.io/name: suspicious-shell + security-baseline/test: runtime-alert +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: shell-test + image: docker.io/library/busybox:1.36 + command: ["sh", "-c", "echo suspicious-shell-started; sleep 3600"] + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi