|
| 1 | +# Lab 7 — Submission |
| 2 | + |
| 3 | +## Task 1: Trivy Image + Config Scan |
| 4 | + |
| 5 | +### Image scan severity breakdown |
| 6 | +| Severity | Total | With fix available | |
| 7 | +|----------|------:|------------------:| |
| 8 | +| Critical | 5 | 4 | |
| 9 | +| High | 43 | 42 | |
| 10 | +| **Total** | 48 | 46 | |
| 11 | + |
| 12 | +### Top 10 CVEs with fixes |
| 13 | +| CVE | Severity | Package | Installed | Fix | |
| 14 | +|-----|----------|---------|-----------|-----| |
| 15 | +| CVE-2023-46233 | CRITICAL | crypto-js | 3.3.0 | 4.2.0 | |
| 16 | +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.1.0 | 4.2.2 | |
| 17 | +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 0.4.0 | 4.2.2 | |
| 18 | +| CVE-2019-10744 | CRITICAL | lodash | 2.4.2 | 4.17.12 | |
| 19 | +| CVE-2026-45447 | HIGH | libssl3t64 | 3.5.5-1~deb13u2 | 3.5.6-1~deb13u2 | |
| 20 | +| NSWG-ECO-428 | HIGH | base64url | 0.0.6 | >=3.0.0 | |
| 21 | +| CVE-2020-15084 | HIGH | express-jwt | 0.1.3 | 6.0.0 | |
| 22 | +| CVE-2022-25881 | HIGH | http-cache-semantics | 3.8.1 | 4.1.1 | |
| 23 | +| CVE-2022-23539 | HIGH | jsonwebtoken | 0.1.0 | 9.0.0 | |
| 24 | +| NSWG-ECO-17 | HIGH | jsonwebtoken | 0.1.0 | >=4.2.2 | |
| 25 | + |
| 26 | +> **Note regarding lab instructions error:** |
| 27 | +> The `jq` command provided in step 7.3 of the lab instructions (`{cve: .VulnerabilityID, severity: .Severity, pkg: .PkgName, fix: .FixedVersion}`) omits the `InstalledVersion` field, making it impossible to fill out the 'Installed' column in the table above using only the provided command. The command had to be manually modified to include `installed: .InstalledVersion` to complete the assignment properly. |
| 28 | +
|
| 29 | + |
| 30 | +### Compared to Lab 4's Grype scan |
| 31 | +1. **Found by both tools (CVE-2015-9235 / GHSA-c7hr-j4mj-j2w6 in `jsonwebtoken`)**: Both tools identified the vulnerability, but they represent it differently. Trivy outputs the standard `CVE-2015-9235`, whereas Grype prefers ecosystem-specific identifiers and outputs `GHSA-c7hr-j4mj-j2w6`. This illustrates that the tools use different identifier preferences and package matching rules (Grype favors GitHub Security Advisories for npm). |
| 32 | +2. **Found by Trivy, missed by Grype (CVE-2025-57349 in `messageformat`)**: This recent vulnerability was flagged by Trivy but missed by Grype. This difference is primarily due to DB freshness and update cadence. Trivy's vulnerability database is likely updated more frequently, allowing it to ingest newer CVEs faster than the default Anchore snapshot used by Grype. |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | +## Task 2: Kubernetes Hardening |
| 38 | + |
| 39 | +### Manifests (paste relevant snippets) |
| 40 | +- `namespace.yaml` PSS labels: |
| 41 | +```yaml |
| 42 | + pod-security.kubernetes.io/enforce: restricted |
| 43 | + pod-security.kubernetes.io/warn: restricted |
| 44 | + pod-security.kubernetes.io/audit: restricted |
| 45 | +``` |
| 46 | +- `deployment.yaml` securityContext sections (pod + container): |
| 47 | +```yaml |
| 48 | + securityContext: |
| 49 | + runAsNonRoot: true |
| 50 | + runAsUser: 1000 |
| 51 | + fsGroup: 1000 |
| 52 | + seccompProfile: |
| 53 | + type: RuntimeDefault |
| 54 | +... |
| 55 | + securityContext: |
| 56 | + allowPrivilegeEscalation: false |
| 57 | + readOnlyRootFilesystem: true |
| 58 | + capabilities: |
| 59 | + drop: |
| 60 | + - ALL |
| 61 | +``` |
| 62 | +- `networkpolicy.yaml` ingress + egress: |
| 63 | +```yaml |
| 64 | + ingress: |
| 65 | + - {} # Allows incoming connections (e.g. for kubectl port-forward) |
| 66 | + egress: |
| 67 | + - to: |
| 68 | + - namespaceSelector: |
| 69 | + matchLabels: |
| 70 | + kubernetes.io/metadata.name: kube-system |
| 71 | + ports: |
| 72 | + - protocol: UDP |
| 73 | + port: 53 |
| 74 | + - ports: |
| 75 | + - protocol: TCP |
| 76 | + port: 443 |
| 77 | +``` |
| 78 | + |
| 79 | +### Pod is running |
| 80 | +Output of `kubectl get pod -n juice-shop -l app=juice-shop`: |
| 81 | +``` |
| 82 | +NAME READY STATUS RESTARTS AGE |
| 83 | +juice-shop-654c7d67fb-2hzs4 1/1 Running 0 42s |
| 84 | +``` |
| 85 | +> **Note:** The pod successfully passes the `kubectl wait` condition because it initially starts without crashing, but shortly after goes into a `CrashLoopBackOff` state. |
| 86 | + |
| 87 | +### Trivy K8s scan |
| 88 | +> **Note regarding lab instructions error:** The command `trivy k8s --namespace juice-shop` is deprecated. I used `trivy k8s --include-namespaces juice-shop`. |
| 89 | + |
| 90 | +| Severity | Count | |
| 91 | +|----------|------:| |
| 92 | +| Critical | 5 | |
| 93 | +| High | 43 | |
| 94 | + |
| 95 | +### What broke and how you fixed it (2-3 sentences) |
| 96 | +`readOnlyRootFilesystem: true` breaks Juice Shop because it needs to write to `/tmp` and `/juice-shop/logs`, as well as its SQLite database and FTP directory. I fixed the initial crash by mounting `emptyDir` volumes at `/tmp` and `/juice-shop/logs` (although v20.0.0 also requires writable access to `/juice-shop/data`, but simply mounting an `emptyDir` there hides the required `static` directory unless advanced K8s patterns like initContainers are used). |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +## Bonus: Conftest Policy |
| 101 | + |
| 102 | +### Policy (paste labs/lab7/policies/pod-hardening.rego) |
| 103 | +```rego |
| 104 | +package main |
| 105 | +import rego.v1 |
| 106 | +
|
| 107 | +# 1. spec.securityContext.runAsNonRoot != true |
| 108 | +deny contains msg if { |
| 109 | + input.kind == "Deployment" |
| 110 | + pod_spec := input.spec.template.spec |
| 111 | + not pod_spec.securityContext.runAsNonRoot == true |
| 112 | + msg := "Pod securityContext must have runAsNonRoot set to true" |
| 113 | +} |
| 114 | +
|
| 115 | +# 2. (any container) spec.containers[_].securityContext.readOnlyRootFilesystem != true |
| 116 | +deny contains msg if { |
| 117 | + input.kind == "Deployment" |
| 118 | + container := input.spec.template.spec.containers[_] |
| 119 | + not container.securityContext.readOnlyRootFilesystem == true |
| 120 | + msg := sprintf("Container '%v' must have readOnlyRootFilesystem set to true", [container.name]) |
| 121 | +} |
| 122 | +
|
| 123 | +# 3. (any container) spec.containers[_].securityContext.allowPrivilegeEscalation != false |
| 124 | +deny contains msg if { |
| 125 | + input.kind == "Deployment" |
| 126 | + container := input.spec.template.spec.containers[_] |
| 127 | + not container.securityContext.allowPrivilegeEscalation == false |
| 128 | + msg := sprintf("Container '%v' must have allowPrivilegeEscalation set to false", [container.name]) |
| 129 | +} |
| 130 | +
|
| 131 | +# 4. (any container) spec.containers[_].securityContext.capabilities.drop missing "ALL" |
| 132 | +deny contains msg if { |
| 133 | + input.kind == "Deployment" |
| 134 | + container := input.spec.template.spec.containers[_] |
| 135 | + not has_drop_all(container) |
| 136 | + msg := sprintf("Container '%v' must drop ALL capabilities", [container.name]) |
| 137 | +} |
| 138 | +
|
| 139 | +has_drop_all(container) if { |
| 140 | + "ALL" in container.securityContext.capabilities.drop |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +### Output: PASS on hardened manifest |
| 145 | +``` |
| 146 | +4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions |
| 147 | +``` |
| 148 | +
|
| 149 | +### Output: FAIL on bad manifest |
| 150 | +``` |
| 151 | +FAIL - /tmp/bad-pod.yaml - main - Container 'app' must drop ALL capabilities |
| 152 | +FAIL - /tmp/bad-pod.yaml - main - Container 'app' must have allowPrivilegeEscalation set to false |
| 153 | +FAIL - /tmp/bad-pod.yaml - main - Container 'app' must have readOnlyRootFilesystem set to true |
| 154 | +FAIL - /tmp/bad-pod.yaml - main - Pod securityContext must have runAsNonRoot set to true |
| 155 | + |
| 156 | +4 tests, 0 passed, 0 warnings, 4 failures, 0 exceptions |
| 157 | +``` |
| 158 | +
|
| 159 | +### What this prevents at CI time (2-3 sentences) |
| 160 | +This policy catches insecure configuration bugs (like running as root or allowing privilege escalation) natively in the CI pipeline BEFORE `kubectl apply` is ever executed. By failing the pipeline early, developers get immediate feedback in their Pull Requests without waiting for the cluster's admission controller (like PSS) to reject the deployment. This represents true "shift-left" security, reducing friction and keeping the cluster entirely unaware of bad manifests. |
0 commit comments