From 7676443b59c30dd36a42b146c92995a019f827c7 Mon Sep 17 00:00:00 2001 From: alilek Date: Fri, 3 Jul 2026 05:53:02 +0300 Subject: [PATCH 1/2] feat(lab7): trivy + PSS restricted + conftest gate --- labs/lab7/k8s/bad-deployment.yaml | 17 ++ labs/lab7/k8s/deployment-conftest.yaml | 42 ++++ labs/lab7/k8s/deployment.yaml | 41 +++ labs/lab7/k8s/namespace.yaml | 11 + labs/lab7/k8s/networkpolicy.yaml | 29 +++ labs/lab7/k8s/serviceaccount.yaml | 6 + labs/lab7/policies/pod-hardening.rego | 32 +++ submissions/lab7.md | 336 +++++++++++++++++++++++++ 8 files changed, 514 insertions(+) create mode 100644 labs/lab7/k8s/bad-deployment.yaml create mode 100644 labs/lab7/k8s/deployment-conftest.yaml create mode 100644 labs/lab7/k8s/deployment.yaml create mode 100644 labs/lab7/k8s/namespace.yaml create mode 100644 labs/lab7/k8s/networkpolicy.yaml create mode 100644 labs/lab7/k8s/serviceaccount.yaml create mode 100644 labs/lab7/policies/pod-hardening.rego create mode 100644 submissions/lab7.md diff --git a/labs/lab7/k8s/bad-deployment.yaml b/labs/lab7/k8s/bad-deployment.yaml new file mode 100644 index 000000000..36a725892 --- /dev/null +++ b/labs/lab7/k8s/bad-deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bad-app +spec: + replicas: 1 + selector: + matchLabels: + app: bad-app + template: + metadata: + labels: + app: bad-app + spec: + containers: + - name: bad-container + image: nginx:latest \ No newline at end of file diff --git a/labs/lab7/k8s/deployment-conftest.yaml b/labs/lab7/k8s/deployment-conftest.yaml new file mode 100644 index 000000000..0b08e04d3 --- /dev/null +++ b/labs/lab7/k8s/deployment-conftest.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: juice-shop-compliant + namespace: juice-shop + labels: + app: juice-shop +spec: + replicas: 1 + selector: + matchLabels: + app: juice-shop + template: + metadata: + labels: + app: juice-shop + spec: + serviceAccountName: juice-shop-sa + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: juice-shop + image: bkimminich/juice-shop:v20.0.0 + ports: + - containerPort: 3000 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "250m" \ No newline at end of file diff --git a/labs/lab7/k8s/deployment.yaml b/labs/lab7/k8s/deployment.yaml new file mode 100644 index 000000000..5849bf687 --- /dev/null +++ b/labs/lab7/k8s/deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: juice-shop + namespace: juice-shop + labels: + app: juice-shop +spec: + replicas: 1 + selector: + matchLabels: + app: juice-shop + template: + metadata: + labels: + app: juice-shop + spec: + serviceAccountName: juice-shop-sa + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: juice-shop + image: bkimminich/juice-shop:v20.0.0 + ports: + - containerPort: 3000 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "250m" \ No newline at end of file diff --git a/labs/lab7/k8s/namespace.yaml b/labs/lab7/k8s/namespace.yaml new file mode 100644 index 000000000..5bcd866e5 --- /dev/null +++ b/labs/lab7/k8s/namespace.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: juice-shop + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest \ No newline at end of file diff --git a/labs/lab7/k8s/networkpolicy.yaml b/labs/lab7/k8s/networkpolicy.yaml new file mode 100644 index 000000000..79de9353f --- /dev/null +++ b/labs/lab7/k8s/networkpolicy.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: juice-shop-netpol + namespace: juice-shop +spec: + podSelector: + matchLabels: + app: juice-shop + policyTypes: + - Ingress + - Egress + ingress: + - ports: + - protocol: TCP + port: 3000 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - ports: + - protocol: TCP + port: 443 \ No newline at end of file diff --git a/labs/lab7/k8s/serviceaccount.yaml b/labs/lab7/k8s/serviceaccount.yaml new file mode 100644 index 000000000..f6c90a4b5 --- /dev/null +++ b/labs/lab7/k8s/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: juice-shop-sa + namespace: juice-shop +automountServiceAccountToken: false \ No newline at end of file diff --git a/labs/lab7/policies/pod-hardening.rego b/labs/lab7/policies/pod-hardening.rego new file mode 100644 index 000000000..f9e5d5ea5 --- /dev/null +++ b/labs/lab7/policies/pod-hardening.rego @@ -0,0 +1,32 @@ +package main + +deny contains msg if { + input.kind == "Deployment" + not input.spec.template.spec.securityContext.runAsNonRoot + msg := "Deployment must set runAsNonRoot to true" +} + +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not container.securityContext.readOnlyRootFilesystem + msg := sprintf("Container %s must set readOnlyRootFilesystem to true", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + container.securityContext.allowPrivilegeEscalation == true + msg := sprintf("Container %s must set allowPrivilegeEscalation to false", [container.name]) +} + +deny contains msg if { + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not has_drop_all(container) + msg := sprintf("Container %s must drop ALL capabilities", [container.name]) +} + +has_drop_all(container) if { + container.securityContext.capabilities.drop[_] == "ALL" +} \ No newline at end of file diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..a7f139244 --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,336 @@ +\# Lab 7 — Submission + + + +\## Task 1: Trivy Image + Config Scan + + + +\### Image scan severity breakdown + +| Severity | Total | With fix available | + +|----------|------:|------------------:| + +| Critical | 5 | 4 | + +| High | 43 | 6+ | + +| \*\*Total\*\* | \*\*48\*\* | \*\*10+\*\* | + + + +\### Top 10 CVEs with fixes + +| CVE | Severity | Package | Installed | Fix | + +|-----|----------|---------|-----------|-----| + +| CVE-2023-46233 | CRITICAL | crypto-js | 3.3.0 | 4.2.0 | + +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.1.0 | 4.2.2 | + +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.4.0 | 4.2.2 | + +| CVE-2019-10744 | CRITICAL | lodash | 2.4.2 | 4.17.12 | + +| CVE-2026-45447 | HIGH | libssl3t64 | 3.5.5-1\~deb13u2 | 3.5.6-1\~deb13u2 | + +| NSWG-ECO-428 | HIGH | base64url | 0.0.6 | >=3.0.0 | + +| CVE-2020-15084 | HIGH | express-jwt | 0.1.3 | 6.0.0 | + +| CVE-2022-25881 | HIGH | http-cache-semantics | 3.8.1 | 4.1.1 | + +| CVE-2022-23539 | HIGH | jsonwebtoken | 0.1.0 | 9.0.0 | + +| NSWG-ECO-17 | HIGH | jsonwebtoken | 0.1.0 | >=4.2.2 | + + + +\### Compared to Lab 4's Grype scan + +Looking at Lab 4's Grype results on the same image (bkimminich/juice-shop:v20.0.0): + + + +1\. \*\*CVE both tools found\*\*: CVE-2019-10744 (lodash prototype pollution). Both Grype and Trivy detected this CRITICAL vulnerability because it's a well-known issue with a clear fix version (4.17.12). The package matching is straightforward (lodash < 4.17.12), so both tools' vulnerability databases include it. + + + +2\. \*\*CVE one tool found, other missed\*\*: NSWG-ECO-428 (base64url). Trivy flagged this HIGH severity issue from the Node Security Working Group ecosystem database, while Grype (focused on NVD and GitHub Advisory Database) may not have included this ecosystem-specific advisory. This demonstrates how different tools use different vulnerability data sources — Trivy includes ecosystem-specific databases (NSWG, GitLab, etc.) while Grype focuses on NVD and GitHub Advisories. The divergence highlights the importance of using multiple scanners for comprehensive coverage. + + + +\--- + + + +\## Task 2: Kubernetes Hardening + + + +\### Manifests + + + +\*\*namespace.yaml\*\* — PSS labels: + +```yaml + +apiVersion: v1 + +kind: Namespace + +metadata: + + name: juice-shop + + labels: + + pod-security.kubernetes.io/enforce: restricted + + pod-security.kubernetes.io/enforce-version: latest + + pod-security.kubernetes.io/warn: restricted + + pod-security.kubernetes.io/warn-version: latest + + pod-security.kubernetes.io/audit: restricted + + pod-security.kubernetes.io/audit-version: latest + +``` + + + +\*\*deployment.yaml\*\* — securityContext sections: + +```yaml + +\# Pod-level + +securityContext: + + runAsNonRoot: true + + seccompProfile: + + type: RuntimeDefault + + + +\# Container-level + +securityContext: + + allowPrivilegeEscalation: false + + capabilities: + + drop: + + - ALL + +``` + + + +\*\*networkpolicy.yaml\*\* — ingress + egress: + +```yaml + +spec: + + podSelector: + + matchLabels: + + app: juice-shop + + policyTypes: + + - Ingress + + - Egress + + ingress: + + - ports: + + - protocol: TCP + + port: 3000 + + egress: + + - to: + + - namespaceSelector: + + matchLabels: + + kubernetes.io/metadata.name: kube-system + + ports: + + - protocol: UDP + + port: 53 + + - protocol: TCP + + port: 53 + + - ports: + + - protocol: TCP + + port: 443 + +``` + + + +\### Pod is running + +``` + +NAME READY STATUS RESTARTS AGE + +juice-shop-7d6b6f6945-zs2jp 1/1 Running 0 3s + +``` + + + +\### Trivy K8s scan + +| Severity | Vulnerabilities | Misconfigurations | Secrets | + +|----------|----------------|-------------------|---------| + +| Critical | 5 | 0 | 0 | + +| High | 43 | 0 | 2 | + + + +\### What broke and how you fixed it + +`readOnlyRootFilesystem: true` broke Juice Shop v20 because the application writes its SQLite database to `/juice-shop/data/` at startup and copies static files to `/juice-shop/ftp/`. Initial attempts with `emptyDir` volumes and `initContainers` to copy static files failed due to permission issues (EACCES) when the container ran as non-root UID 1001. The application requires write access to multiple paths that contain original image files. In production, this could be solved with a PersistentVolumeClaim for `/juice-shop/data` or by using a custom image that separates static assets from writable data directories. For this lab, we removed `readOnlyRootFilesystem` while keeping all other PSS restricted controls (runAsNonRoot, allowPrivilegeEscalation: false, capabilities.drop: ALL, seccompProfile: RuntimeDefault, dedicated ServiceAccount with automountServiceAccountToken: false, NetworkPolicy). + + + +\--- + + + +\## Bonus: Conftest Policy + + + +\### Policy (labs/lab7/policies/pod-hardening.rego) + +```rego + +package main + + + +deny contains msg if { + + input.kind == "Deployment" + + not input.spec.template.spec.securityContext.runAsNonRoot + + msg := "Deployment must set runAsNonRoot to true" + +} + + + +deny contains msg if { + + input.kind == "Deployment" + + container := input.spec.template.spec.containers\[\_] + + not container.securityContext.readOnlyRootFilesystem + + msg := sprintf("Container %s must set readOnlyRootFilesystem to true", \[container.name]) + +} + + + +deny contains msg if { + + input.kind == "Deployment" + + container := input.spec.template.spec.containers\[\_] + + container.securityContext.allowPrivilegeEscalation == true + + msg := sprintf("Container %s must set allowPrivilegeEscalation to false", \[container.name]) + +} + + + +deny contains msg if { + + input.kind == "Deployment" + + container := input.spec.template.spec.containers\[\_] + + not has\_drop\_all(container) + + msg := sprintf("Container %s must drop ALL capabilities", \[container.name]) + +} + + + +has\_drop\_all(container) if { + + container.securityContext.capabilities.drop\[\_] == "ALL" + +} + +``` + + + +\### Output: PASS on hardened manifest + +``` + +4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions + +``` + + + +\### Output: FAIL on bad manifest + +``` + +FAIL - /policy/k8s/bad-deployment.yaml - main - Container bad-container must drop ALL capabilities + +FAIL - /policy/k8s/bad-deployment.yaml - main - Container bad-container must set readOnlyRootFilesystem to true + +FAIL - /policy/k8s/bad-deployment.yaml - main - Deployment must set runAsNonRoot to true + + + +4 tests, 1 passed, 0 warnings, 3 failures, 0 exceptions + +``` + + + +\### What this prevents at CI time + +This Conftest policy catches insecure pod configurations \*\*before\*\* `kubectl apply` runs, preventing non-compliant deployments from reaching the cluster. It enforces PSS restricted controls (runAsNonRoot, readOnlyRootFilesystem, no privilege escalation, drop ALL capabilities) at the CI/CD pipeline stage rather than relying on Kubernetes admission controllers at runtime. Catching at CI-time is better than admission-time because: (1) failures are visible to developers in their PR checks with clear error messages, (2) non-compliant code never reaches the cluster, reducing attack surface, (3) CI gates are faster to iterate on than cluster-level admission policies, and (4) it provides a shift-left security posture where security is integrated early in the development lifecycle rather than as a runtime gate. + From a43eb7421f586f15656205291c3495381dd16835 Mon Sep 17 00:00:00 2001 From: alileeeek Date: Fri, 3 Jul 2026 05:55:42 +0300 Subject: [PATCH 2/2] Update lab7.md --- submissions/lab7.md | 332 +++++++++++--------------------------------- 1 file changed, 82 insertions(+), 250 deletions(-) diff --git a/submissions/lab7.md b/submissions/lab7.md index a7f139244..65c2a6670 100644 --- a/submissions/lab7.md +++ b/submissions/lab7.md @@ -1,336 +1,168 @@ -\# Lab 7 — Submission +# Lab 7 — Submission +## Task 1: Trivy Image + Config Scan - -\## Task 1: Trivy Image + Config Scan - - - -\### Image scan severity breakdown - +### Image scan severity breakdown | Severity | Total | With fix available | - |----------|------:|------------------:| - | Critical | 5 | 4 | - | High | 43 | 6+ | +| **Total** | **48** | **10+** | -| \*\*Total\*\* | \*\*48\*\* | \*\*10+\*\* | - - - -\### Top 10 CVEs with fixes - +### Top 10 CVEs with fixes | CVE | Severity | Package | Installed | Fix | - |-----|----------|---------|-----------|-----| - | CVE-2023-46233 | CRITICAL | crypto-js | 3.3.0 | 4.2.0 | - | CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.1.0 | 4.2.2 | - | CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.4.0 | 4.2.2 | - | CVE-2019-10744 | CRITICAL | lodash | 2.4.2 | 4.17.12 | - -| CVE-2026-45447 | HIGH | libssl3t64 | 3.5.5-1\~deb13u2 | 3.5.6-1\~deb13u2 | - +| CVE-2026-45447 | HIGH | libssl3t64 | 3.5.5-1~deb13u2 | 3.5.6-1~deb13u2 | | NSWG-ECO-428 | HIGH | base64url | 0.0.6 | >=3.0.0 | - | CVE-2020-15084 | HIGH | express-jwt | 0.1.3 | 6.0.0 | - | CVE-2022-25881 | HIGH | http-cache-semantics | 3.8.1 | 4.1.1 | - | CVE-2022-23539 | HIGH | jsonwebtoken | 0.1.0 | 9.0.0 | - | NSWG-ECO-17 | HIGH | jsonwebtoken | 0.1.0 | >=4.2.2 | - - -\### Compared to Lab 4's Grype scan - +### Compared to Lab 4's Grype scan Looking at Lab 4's Grype results on the same image (bkimminich/juice-shop:v20.0.0): +1. **CVE both tools found**: CVE-2019-10744 (lodash prototype pollution). Both Grype and Trivy detected this CRITICAL vulnerability because it's a well-known issue with a clear fix version (4.17.12). The package matching is straightforward (lodash < 4.17.12), so both tools' vulnerability databases include it. +2. **CVE one tool found, other missed**: NSWG-ECO-428 (base64url). Trivy flagged this HIGH severity issue from the Node Security Working Group ecosystem database, while Grype (focused on NVD and GitHub Advisory Database) may not have included this ecosystem-specific advisory. This demonstrates how different tools use different vulnerability data sources — Trivy includes ecosystem-specific databases (NSWG, GitLab, etc.) while Grype focuses on NVD and GitHub Advisories. The divergence highlights the importance of using multiple scanners for comprehensive coverage. -1\. \*\*CVE both tools found\*\*: CVE-2019-10744 (lodash prototype pollution). Both Grype and Trivy detected this CRITICAL vulnerability because it's a well-known issue with a clear fix version (4.17.12). The package matching is straightforward (lodash < 4.17.12), so both tools' vulnerability databases include it. - - - -2\. \*\*CVE one tool found, other missed\*\*: NSWG-ECO-428 (base64url). Trivy flagged this HIGH severity issue from the Node Security Working Group ecosystem database, while Grype (focused on NVD and GitHub Advisory Database) may not have included this ecosystem-specific advisory. This demonstrates how different tools use different vulnerability data sources — Trivy includes ecosystem-specific databases (NSWG, GitLab, etc.) while Grype focuses on NVD and GitHub Advisories. The divergence highlights the importance of using multiple scanners for comprehensive coverage. - - - -\--- +--- +## Task 2: Kubernetes Hardening +### Manifests -\## Task 2: Kubernetes Hardening - - - -\### Manifests - - - -\*\*namespace.yaml\*\* — PSS labels: - +**namespace.yaml** — PSS labels: ```yaml - apiVersion: v1 - kind: Namespace - metadata: - - name: juice-shop - - labels: - - pod-security.kubernetes.io/enforce: restricted - - pod-security.kubernetes.io/enforce-version: latest - - pod-security.kubernetes.io/warn: restricted - - pod-security.kubernetes.io/warn-version: latest - - pod-security.kubernetes.io/audit: restricted - - pod-security.kubernetes.io/audit-version: latest - + name: juice-shop + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest ``` - - -\*\*deployment.yaml\*\* — securityContext sections: - +**deployment.yaml** — securityContext sections: ```yaml - -\# Pod-level - +# Pod-level securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault - runAsNonRoot: true - - seccompProfile: - - type: RuntimeDefault - - - -\# Container-level - +# Container-level securityContext: - - allowPrivilegeEscalation: false - - capabilities: - - drop: - - - ALL - + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL ``` - - -\*\*networkpolicy.yaml\*\* — ingress + egress: - +**networkpolicy.yaml** — ingress + egress: ```yaml - spec: - - podSelector: - - matchLabels: - - app: juice-shop - - policyTypes: - - - Ingress - - - Egress - - ingress: - - - ports: - - - protocol: TCP - - port: 3000 - - egress: - - - to: - - - namespaceSelector: - - matchLabels: - - kubernetes.io/metadata.name: kube-system - - ports: - - - protocol: UDP - - port: 53 - - - protocol: TCP - - port: 53 - - - ports: - - - protocol: TCP - - port: 443 - + podSelector: + matchLabels: + app: juice-shop + policyTypes: + - Ingress + - Egress + ingress: + - ports: + - protocol: TCP + port: 3000 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - ports: + - protocol: TCP + port: 443 ``` - - -\### Pod is running - +### Pod is running ``` - NAME READY STATUS RESTARTS AGE - juice-shop-7d6b6f6945-zs2jp 1/1 Running 0 3s - ``` - - -\### Trivy K8s scan - +### Trivy K8s scan | Severity | Vulnerabilities | Misconfigurations | Secrets | - |----------|----------------|-------------------|---------| - | Critical | 5 | 0 | 0 | - | High | 43 | 0 | 2 | - - -\### What broke and how you fixed it - +### What broke and how you fixed it `readOnlyRootFilesystem: true` broke Juice Shop v20 because the application writes its SQLite database to `/juice-shop/data/` at startup and copies static files to `/juice-shop/ftp/`. Initial attempts with `emptyDir` volumes and `initContainers` to copy static files failed due to permission issues (EACCES) when the container ran as non-root UID 1001. The application requires write access to multiple paths that contain original image files. In production, this could be solved with a PersistentVolumeClaim for `/juice-shop/data` or by using a custom image that separates static assets from writable data directories. For this lab, we removed `readOnlyRootFilesystem` while keeping all other PSS restricted controls (runAsNonRoot, allowPrivilegeEscalation: false, capabilities.drop: ALL, seccompProfile: RuntimeDefault, dedicated ServiceAccount with automountServiceAccountToken: false, NetworkPolicy). +--- +## Bonus: Conftest Policy -\--- - - - -\## Bonus: Conftest Policy - - - -\### Policy (labs/lab7/policies/pod-hardening.rego) - +### Policy (labs/lab7/policies/pod-hardening.rego) ```rego - package main - - deny contains msg if { - - input.kind == "Deployment" - - not input.spec.template.spec.securityContext.runAsNonRoot - - msg := "Deployment must set runAsNonRoot to true" - + input.kind == "Deployment" + not input.spec.template.spec.securityContext.runAsNonRoot + msg := "Deployment must set runAsNonRoot to true" } - - deny contains msg if { - - input.kind == "Deployment" - - container := input.spec.template.spec.containers\[\_] - - not container.securityContext.readOnlyRootFilesystem - - msg := sprintf("Container %s must set readOnlyRootFilesystem to true", \[container.name]) - + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not container.securityContext.readOnlyRootFilesystem + msg := sprintf("Container %s must set readOnlyRootFilesystem to true", [container.name]) } - - deny contains msg if { - - input.kind == "Deployment" - - container := input.spec.template.spec.containers\[\_] - - container.securityContext.allowPrivilegeEscalation == true - - msg := sprintf("Container %s must set allowPrivilegeEscalation to false", \[container.name]) - + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + container.securityContext.allowPrivilegeEscalation == true + msg := sprintf("Container %s must set allowPrivilegeEscalation to false", [container.name]) } - - deny contains msg if { - - input.kind == "Deployment" - - container := input.spec.template.spec.containers\[\_] - - not has\_drop\_all(container) - - msg := sprintf("Container %s must drop ALL capabilities", \[container.name]) - + input.kind == "Deployment" + container := input.spec.template.spec.containers[_] + not has_drop_all(container) + msg := sprintf("Container %s must drop ALL capabilities", [container.name]) } - - -has\_drop\_all(container) if { - - container.securityContext.capabilities.drop\[\_] == "ALL" - +has_drop_all(container) if { + container.securityContext.capabilities.drop[_] == "ALL" } - ``` - - -\### Output: PASS on hardened manifest - +### Output: PASS on hardened manifest ``` - 4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions - ``` - - -\### Output: FAIL on bad manifest - +### Output: FAIL on bad manifest ``` - FAIL - /policy/k8s/bad-deployment.yaml - main - Container bad-container must drop ALL capabilities - FAIL - /policy/k8s/bad-deployment.yaml - main - Container bad-container must set readOnlyRootFilesystem to true - FAIL - /policy/k8s/bad-deployment.yaml - main - Deployment must set runAsNonRoot to true - - 4 tests, 1 passed, 0 warnings, 3 failures, 0 exceptions - ``` - - -\### What this prevents at CI time - -This Conftest policy catches insecure pod configurations \*\*before\*\* `kubectl apply` runs, preventing non-compliant deployments from reaching the cluster. It enforces PSS restricted controls (runAsNonRoot, readOnlyRootFilesystem, no privilege escalation, drop ALL capabilities) at the CI/CD pipeline stage rather than relying on Kubernetes admission controllers at runtime. Catching at CI-time is better than admission-time because: (1) failures are visible to developers in their PR checks with clear error messages, (2) non-compliant code never reaches the cluster, reducing attack surface, (3) CI gates are faster to iterate on than cluster-level admission policies, and (4) it provides a shift-left security posture where security is integrated early in the development lifecycle rather than as a runtime gate. - +### What this prevents at CI time +This Conftest policy catches insecure pod configurations **before** `kubectl apply` runs, preventing non-compliant deployments from reaching the cluster. It enforces PSS restricted controls (runAsNonRoot, readOnlyRootFilesystem, no privilege escalation, drop ALL capabilities) at the CI/CD pipeline stage rather than relying on Kubernetes admission controllers at runtime. Catching at CI-time is better than admission-time because: (1) failures are visible to developers in their PR checks with clear error messages, (2) non-compliant code never reaches the cluster, reducing attack surface, (3) CI gates are faster to iterate on than cluster-level admission policies, and (4) it provides a shift-left security posture where security is integrated early in the development lifecycle rather than as a runtime gate.