diff --git a/labs/lab7/k8s/deployment.yaml b/labs/lab7/k8s/deployment.yaml new file mode 100644 index 000000000..054fd54ad --- /dev/null +++ b/labs/lab7/k8s/deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: juice-shop + namespace: 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 + runAsUser: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: juice-shop + image: bkimminich/juice-shop:v20.0.0 + ports: + - containerPort: 3000 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" diff --git a/labs/lab7/k8s/namespace.yaml b/labs/lab7/k8s/namespace.yaml new file mode 100644 index 000000000..77c76dbaa --- /dev/null +++ b/labs/lab7/k8s/namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: juice-shop + labels: + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/audit: restricted diff --git a/labs/lab7/k8s/networkpolicy.yaml b/labs/lab7/k8s/networkpolicy.yaml new file mode 100644 index 000000000..0c2a6b511 --- /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: + - from: + - namespaceSelector: {} + ports: + - port: 3000 + protocol: TCP + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - port: 53 + protocol: UDP + - ports: + - port: 443 + protocol: TCP diff --git a/labs/lab7/k8s/serviceaccount.yaml b/labs/lab7/k8s/serviceaccount.yaml new file mode 100644 index 000000000..fc24f5a35 --- /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 diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..d75ddafe7 --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,101 @@ +# Lab 7 — Submission + +## Task 1: Trivy Image + Config Scan + +### Image scan severity breakdown +| Severity | Total | With fix available | +|----------|------:|------------------:| +| Critical | 4 | 4 | +| High | 6 | 6 | +| **Total** | 10 | 10 | + +### Top 10 CVEs with fixes +| CVE | Severity | Package | Fix | +|-----|----------|---------|-----| +| CVE-2023-46233 | CRITICAL | crypto-js | 4.2.0 | +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 4.2.2 | +| CVE-2015-9235 | CRITICAL | jsonwebtoken | 4.2.2 | +| CVE-2019-10744 | CRITICAL | lodash | 4.17.12 | +| CVE-2026-45447 | HIGH | libssl3t64 | 3.5.6-1~deb13u2 | +| NSWG-ECO-428 | HIGH | base64url | >=3.0.0 | +| CVE-2020-15084 | HIGH | express-jwt | 6.0.0 | +| CVE-2022-25881 | HIGH | http-cache-semantics | 4.1.1 | +| CVE-2022-23539 | HIGH | jsonwebtoken | 9.0.0 | +| NSWG-ECO-17 | HIGH | jsonwebtoken | >=4.2.2 | + +### Compared to Lab 4's Grype scan +Look back at your Lab 4 Grype results on the same image. Pick **two CVEs**: +1. One that BOTH Grype and Trivy found + +``CVE-2026-45447`` - detected by both tools, as it is a vulnerability in the OpenSSL system package. Both scanners use up-to-date OS-level databases. + +2. One that ONE tool found and the OTHER missed +For each: explain why the tools differ (DB freshness? Different package matching? +EPSS scoring? Lecture 7 + Lecture 4 give context.) (2-3 sentences per CVE.) + +``NSWG-ECO-428`` - Found by Trivy but missing from Grype. Trivy can inspect the contents of **package-lock.json** and understands all npm dependencies. Grype operates via an SBOM and sees the general list of libraries, but if a vulnerability is recorded in the database as **NSWG-ECO-428**, Grype might not recognize it; it is geared toward system packages and CVE identifiers. If Trivy flagged CVE-2026-45447 with a high EPSS score, while Grype found the same vulnerability without a probability score, then Trivy provides more context for prioritization. + +## Task 2: Kubernetes Hardening + +### Manifests (paste relevant snippets) +- `namespace.yaml` PSS labels: +```yaml + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/audit: restricted +``` +- `deployment.yaml` securityContext sections (pod + container): +```yaml +securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL +``` +- `networkpolicy.yaml` ingress + egress: +```yaml +ingress: + - from: + - namespaceSelector: {} + ports: + - port: 3000 + protocol: TCP + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - port: 53 + protocol: UDP + - ports: + - port: 443 + protocol: TCP +``` + +### Pod is running +Output of `kubectl get pod -n juice-shop -l app=juice-shop`: +``` +NAME READY STATUS RESTARTS AGE +juice-shop-68bc966c84-sqnjt 1/1 Running 0 4s +``` + +### Trivy K8s scan +| Severity | Count | +|----------|------:| +| Critical | 5 | +| High | 43 | + +### What broke and how you fixed it (2-3 sentences) +`readOnlyRootFilesystem: true` likely broke Juice Shop. What paths did it need to write? +How did you fix it (which emptyDir mounts)? + +Setting `readOnlyRootFilesystem: true` broke Juice Shop because the application writes to several directories: `/juice-shop/data/`, `/juice-shop/ftp/`, `/tmp/`, and `/usr/src/app/logs/`. The issue was resolved by mounting `emptyDir` volumes at each of these paths; this provided Juice Shop with writable temporary storage while keeping the root filesystem read-only. However, even with `emptyDir` volumes, SQLite database initialization failed because the original database file inside the image was obscured by the mounted empty volume. Ultimately, it was decided to forgo the `readOnlyRootFilesystem: true` setting. +