- ποΈ August 2018 onward β Aqua Security's research team begins tracking misconfigured Docker daemons and exposed Kubernetes API servers
- πͺ By 2023 they had documented >1.6 million publicly accessible containers running cryptominers; the average compromise lifecycle: 15 minutes from misconfig to miner
- π― The pattern: a developer exposes
2375/tcpon a cloud VM "just for debugging", or a fresh K8s cluster ships withkubectl proxyreachable; scanner bots find it in minutes; miners deploy by the thousand - π§ Every one of these is a misconfiguration finding that Trivy + a Pod Security Standard policy would have flagged β the rules existed; they weren't gating
π€ Think: Lecture 6 scanned the IaC text that creates the cluster. This lecture scans the image you run inside it and the cluster config it lives in. Three layers of static checks before any code runs.
| # | π Outcome |
|---|---|
| 1 | β Write a secure Dockerfile (non-root, minimal base, no embedded secrets) |
| 2 | β Run Trivy image scan and read its CVE + misconfig output |
| 3 | β Apply Pod Security Standards (Privileged / Baseline / Restricted) to a namespace |
| 4 | β Choose between distroless, Alpine, and full-distro base images by use case |
| 5 | β Identify the three Kubernetes objects that most often grant excessive privilege (Role/ClusterRole, ServiceAccount, NetworkPolicy gaps) |
graph LR
L4["π L4 SBOM/SCA"] --> L7
L5["π§ͺ L5 SAST/DAST"] --> L7
L6["ποΈ L6 IaC scan"] --> L7["π¦ L7 Container<br/>(here)"]
L7 --> L8["π L8 Sign<br/>+ verify"]
L7 -.feeds.-> L9["π L9 Runtime<br/>Falco"]
style L7 fill:#FF9800,color:#fff
- πͺ Building on prior labs:
- L4 generated the SBOM β we'll scan its CVEs here
- L6 scanned the IaC that creates clusters β we now scan the images running inside them
- π― Lab 7 alignment: Task 1 (Trivy image scan on Juice Shop + Docker Bench), Task 2 (K8s hardening via PSS), Bonus (small Policy-as-Code gate for insecure pods)
- π£οΈ Setting up L8: the image we harden here will be signed in Lab 8; runtime behavior monitored in Lab 9
π¬ "A container is just a Linux process with a particularly fancy set of namespaces and cgroups." β Liz Rice, Container Security (O'Reilly, 2020)
graph TB
Container[π³ Container] --> NS[Linux namespaces<br/>PID, NET, MNT, UTS, IPC, USER]
Container --> CG[cgroups<br/>CPU, memory, devices]
Container --> RFS[Layered filesystem<br/>OverlayFS]
Container --> CAPS[Linux capabilities<br/>retained subset of root]
style Container fill:#2196F3,color:#fff
- πͺ A container is not a VM. It shares the kernel with the host. Kernel CVE = container escape risk (see Dirty Pipe CVE-2022-0847, runc CVE-2024-21626)
- π§ The image is just a tarball of filesystem layers. Scanning the image = scanning those layer tarballs for known-CVE packages + secrets + misconfig
- πͺ Pictographic mental model: image = recipe; container = cooked meal; runtime = stove. Each has different vulnerabilities
| π¨ Class | π₯ Example | πͺ Layer to fix |
|---|---|---|
| Vulnerable base image | Alpine with patched glibc CVE | Update / rebuild |
| Vulnerable app deps | Log4j 2 in your Java image (Lecture 1) | SBOM + SCA (L4) |
| Embedded secrets | Build arg ARG DB_PASSWORD=... baked into a layer |
gitleaks + Trivy secrets |
| Excessive privileges | USER root, no --cap-drop |
Dockerfile + runtime flags |
| Insecure runtime | --privileged, host PID, host network |
K8s SecurityContext / PSS |
| Kernel CVE | runc/containerd escape | OS patch cadence |
- πͺ Most of these layers are scannable β Trivy hits all of them in one pass
- π§ The runtime classes (privileged, host PID) are NOT scannable on the image β they live in your K8s manifest or
docker runflags. That's where Lab 7 Task 2 lives
# β
Hardened Dockerfile
FROM node:22-alpine AS build # 1. Pinned, minimal base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM gcr.io/distroless/nodejs22-debian12 # 2. Distroless runtime
WORKDIR /app
COPY --from=build /app .
COPY src/ ./src/
USER 65532 # 3. Non-root user (nobody)
ENV NODE_ENV=production # 4. No debug paths
EXPOSE 3000 # 5. Explicit ports only
HEALTHCHECK CMD ["node", "src/healthcheck.js"] # 6. Built-in healthcheck
ENTRYPOINT ["/nodejs/bin/node", "src/server.js"]| # | πͺ Rule | π― Why |
|---|---|---|
| 1 | Pin base by tag (or digest for max safety) | Same as L4 action pinning β immutability |
| 2 | Use multi-stage + distroless or -alpine |
Smaller attack surface; no shell to drop into |
| 3 | USER non-root (numeric UID best for K8s) |
runAsNonRoot policy compliance |
| 4 | No build args for secrets | Embedded in image history; scrape-able with docker history |
| 5 | Explicit EXPOSE β even if K8s doesn't read it | Documentation + IDE hints |
| 6 | Built-in healthcheck | K8s readiness/liveness probes have something honest to call |
- π§ Trivy's
--config(misconfig) mode reads your Dockerfile and flags violations of these rules β you'll see this in Lab 7 Task 1
| π·οΈ Base | πͺΆ Size | π Shell | πͺ Use when |
|---|---|---|---|
ubuntu:24.04 |
~80MB | bash |
Legacy apps, debugging tools needed |
node:22-alpine |
~50MB | sh (busybox) |
Default for most apps; small CVE surface |
gcr.io/distroless/nodejs22 |
~30MB | None | Production; no debug interaction needed |
cgr.dev/chainguard/node (Chainguard, paid) |
~25MB | None | Production with signed-by-default images |
- πͺ Distroless was Google's 2018 contribution β base images with only your runtime + dependencies. No shell = no
nsenterfrom a compromised process; no package manager = noapk addexfiltration - π¨ The cost: you can't
kubectl exec -it -- sh. Debugging needskubectl debug(k8s 1.23+) with an ephemeral container - π§ The right choice depends on operational maturity. For this course: distroless or alpine. Avoid
latestof anything
- π’ By Aqua Security; first release 2019; Go binary, single statically-linked tool
- π’ Latest: Trivy v0.69.x (April 2026)
- π― Six targets Trivy scans:
- Image (
trivy image ...) β CVEs + misconfig + secrets in container images - Filesystem (
trivy fs ...) β local directories - Repository (
trivy repo ...) β Git repos for misconfig + secrets - Kubernetes (
trivy k8s ...) β live cluster scan; uses RBAC to enumerate workloads - AWS / Azure / GCP (
trivy aws ...) β live cloud config scan - SBOM (
trivy sbom ...) β scan an existing CycloneDX/SPDX for CVEs
- Image (
# Lab 7 starts here
trivy image bkimminich/juice-shop:v19.0.0 \
--severity HIGH,CRITICAL \
--format json --output juice-shop-scan.json- π§ Same Trivy that absorbed tfsec (L6). Same Trivy that consumes the SBOM from L4. One tool, integrated outputs.
juice-shop (alpine 3.18.4)
==========================
Total: 23 (HIGH: 18, CRITICAL: 5)
ββββββββββββββββββββ¬ββββββββββββββββ¬βββββββββββ¬ββββββββββββββ¬ββββββββββββββββ
β Library β Vulnerability β Severity β Installed β Fixed Version β
ββββββββββββββββββββΌββββββββββββββββΌβββββββββββΌββββββββββββββΌββββββββββββββββ€
β libcrypto3 β CVE-2023-5363 β HIGH β 3.1.4-r0 β 3.1.4-r1 β
β libssl3 β CVE-2023-5363 β HIGH β 3.1.4-r0 β 3.1.4-r1 β
β openssl β CVE-2024-0727 β CRITICAL β 3.1.4-r0 β 3.1.4-r4 β
β glib β CVE-2024-34397β HIGH β 2.76.6-r0 β 2.76.6-r1 β
ββββββββββββββββββββ΄ββββββββββββββββ΄βββββββββββ΄ββββββββββββββ΄ββββββββββββββββ
| π·οΈ Column | π― Meaning |
|---|---|
| Library | Package as found in image (deb/apk/rpm/jar/npm/...) |
| Vulnerability | CVE ID β track in NVD or CIRCL |
| Severity | NVD CVSS-based, also reflected in vendor advisories |
| Fixed Version | If non-empty β a fix exists; not bumping is a choice |
- πͺ The triage shortcut: sort by Fixed Version is not empty AND severity β₯ HIGH. That's your weekly remediation queue
- π§ CVEs with no fix yet still matter for risk acceptance β but they're a different conversation (Lecture 10)
graph TB
P[π€ Pod / Workload] -->|uses| SA[βοΈ ServiceAccount]
SA -->|bound by| RB[π RoleBinding / ClusterRoleBinding]
RB -->|grants| R[πͺͺ Role / ClusterRole]
R -->|allows verbs on| Res[π¦ Resources]
P -->|sends/recvs traffic| NP[π NetworkPolicy]
style P fill:#2196F3,color:#fff
style R fill:#F44336,color:#fff
style NP fill:#4CAF50,color:#fff
-
πͺͺ RBAC = Role-Based Access Control. The right model for cluster authz since K8s 1.6 (2017)
-
π¨ Three common excessive-privilege patterns:
- Wildcard ClusterRoles β
verbs: ["*"],resources: ["*"]β‘ root-of-cluster - Default ServiceAccount used by app pods β every pod can talk to the K8s API
- No NetworkPolicy β every pod can connect to every other pod (the "flat network" failure mode)
- Wildcard ClusterRoles β
-
πͺ In Lab 7 Task 2 you'll harden a manifest against all three
- ποΈ PodSecurityPolicy (PSP) β first K8s admission control, deprecated in 1.21, removed in 1.25. Replaced by:
- ποΈ Pod Security Admission (PSA) + Pod Security Standards (PSS) β stable in K8s 1.25 (August 2022)
| ποΈ Level | π¦ Allows | πͺ Use for |
|---|---|---|
privileged |
Everything (host PID, host network, all capabilities) | Sysadmin/CNI namespaces only |
baseline |
No host namespaces, no runAsRoot, drops dangerous capabilities |
App workloads (default) |
restricted |
Full hardening: read-only root FS, capabilities ALL dropped, runAsNonRoot enforced | Production app workloads |
- πͺ Apply by namespace label:
apiVersion: v1
kind: Namespace
metadata:
name: juice-shop
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted- π§
enforceblocks creation;warnlogs to kubectl;auditlogs to audit log. In dev, start withwarn; in prod, escalate toenforce
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
automountServiceAccountToken: false # π¦ Default-deny K8s API
securityContext:
runAsNonRoot: true # π¦ Enforced by PSS restricted
runAsUser: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault # π¦ Block dangerous syscalls
containers:
- name: app
image: ghcr.io/me/juice-shop@sha256:...
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false # π¦ No setuid binaries
readOnlyRootFilesystem: true # π¦ Tampering protection
capabilities:
drop: ["ALL"] # π¦ Drop every Linux capability
resources:
limits: { memory: "256Mi", cpu: "200m" } # π¦ DoS protection
requests: { memory: "128Mi", cpu: "50m" }- πͺ Every line here is a Trivy misconfig rule. Run
trivy k8sagainst your cluster; missing items show up as findings - π§
readOnlyRootFilesystem: trueis the single most effective runtime hardening β and the one most often skipped because apps write temp files. UseemptyDirvolumes for those
- ποΈ April 25, 2019 β Docker discloses unauthorized access to a database holding 190,000 accounts with hashed passwords, GitHub/Bitbucket tokens, and Docker registry tokens
- π§ The breach wasn't a container escape β it was Docker Hub's own infrastructure breached via API. But the impact propagated through every customer
- πͺ DevSecOps lessons:
- πͺͺ Rotate registry tokens regularly (the long-lived-token failure mode again, from Lecture 4)
- π Pin and sign images (Lab 8) β even if the registry is compromised, verified signatures help
- π‘οΈ Image scanning catches only known CVEs; registry compromises need orthogonal controls
π¬ "Container security is application security, plus host security, plus orchestrator security, plus registry security." β Liz Rice paraphrased; container security is not a layer, it's an intersection
- ποΈ January 31, 2024 β Snyk discloses CVE-2024-21626 in runc: a working-directory race condition lets a malicious image escape its container to the host filesystem
- π runc powers Docker, containerd, Kubernetes, Podman β essentially every container runtime
- π‘οΈ What protected secure deployments:
- π οΈ Patched runc within hours of release (operational discipline)
- πͺ Read-only root filesystem on the container β limits the attacker's write scope after escape
- π Runtime detection (Falco, Lab 9) β flags the unusual chdir + filesystem activity
- π§ The takeaway: even with perfect image hygiene, runtime CVEs happen. Defense in depth at the runtime + kernel layers (L9) is non-negotiable
- πͺ By default in K8s, every pod can reach every other pod. This is the "flat network" failure mode
- π― NetworkPolicy lets you write firewall-style rules at the pod label level (works with most CNIs: Calico, Cilium, Antrea, Weave)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: juice-shop-default-deny
namespace: juice-shop
spec:
podSelector: {} # all pods
policyTypes: [Ingress, Egress]
egress:
- to:
- namespaceSelector:
matchLabels: { name: monitoring } # allow β monitoring NS only- πͺ Pattern that scales: default-deny at the namespace level + explicit allows. Same philosophy as IAM (L6) β default-deny + targeted-allow
- π§ Lab 7 Task 2 includes adding NetworkPolicy; Conftest (introduced in this lecture as bonus, deep in L9) can gate that every new pod is covered by at least one policy
sequenceDiagram
participant K as kubectl
participant A as kube-apiserver
participant P as Pod Security Admission
participant G as Gatekeeper / Kyverno
K->>A: kubectl apply -f deploy.yaml
A->>P: validate against PSS
P-->>A: β
or β
A->>G: validate against custom policies (Rego/Kyverno)
G-->>A: β
or β
A-->>K: created / rejected
| π οΈ Admission control | π― What it does | πͺ Stage |
|---|---|---|
| Pod Security Admission | Built-in; enforces PSS levels | K8s 1.25+ |
| Gatekeeper (OPA) | Rego policies as CRDs | Custom rules |
| Kyverno | YAML policies as CRDs (no DSL learning) | Custom rules |
- πͺ Gatekeeper uses Rego β same language as Conftest (L9) and KICS (L6). Skills compound across the program
- πͺ In Lab 7 Bonus you'll preview Conftest as a CI-side gate (before apply), then Lab 9 covers full runtime admission
| π¨ Mistake | π οΈ Fix |
|---|---|
FROM node:latest (or :18, but no minor pin) |
Pin to specific tag (e.g. node:22.10.0-alpine3.20); update via Renovate/Dependabot |
USER root (or no USER line) |
USER 65532 (numeric β K8s runAsNonRoot reads numeric only) |
Embedded secrets via ARG/ENV |
Use external secret mount; never bake into image |
default ServiceAccount used by app pods |
Create a dedicated SA per workload; automountServiceAccountToken: false unless needed |
privileged: true "just to make it work" |
Diagnose what specific capability is needed; use capabilities.add: [NET_BIND_SERVICE] (or whatever) instead |
image: myapp:latest in K8s |
Pin by digest @sha256:... β same lesson as L4 GHA pinning |
- π§ Almost every container-security audit report ends with these six items. Get them right and you're ahead of 80% of orgs
# .github/workflows/container-sec.yml (extends L4 + L5 + L6 patterns)
jobs:
build-and-scan:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # for OIDC if signing happens here (L8 preview)
packages: write
steps:
- uses: actions/checkout@b4ffde6...
- name: Build image
run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- name: Scan image (Trivy)
uses: aquasecurity/trivy-action@v0.28.0
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: 1 # fail PR on findings
format: sarif
output: trivy.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: trivy.sarif }
# K8s manifest scan
- name: Scan manifests (Trivy config)
run: trivy config ./k8s/ --severity HIGH,CRITICAL --exit-code 1- πͺ The L4 lessons all apply: pin by SHA, OIDC, least-privilege
permissions:. The new part is the Trivy step - π§ Note: separate image scan (CVEs) and manifest scan (misconfig) β different output classes, both gated
- π§ͺ Lab 7 (this week):
- Task 1 (6 pts): Trivy image scan + Docker Bench on Juice Shop image; analyze CVE + misconfig findings
- Task 2 (4 pts): Harden a K8s deployment with PSS (
restricted) + securityContext + NetworkPolicy - Bonus (2 pts): Write a small Policy-as-Code rule (Conftest or Kyverno) that rejects pods missing
runAsNonRoot: true
- π Lecture 8 (next week): Software Supply Chain Security β Cosign signs the Juice Shop image you've now hardened; we add provenance attestations and verify them at deploy
Books:
| π Book | βοΈ Why |
|---|---|
| Container Security β Liz Rice (O'Reilly, 2020) | The canonical book; every chapter is gold; ch. 4β6 on isolation are critical |
| Hacking Kubernetes β Andrew Martin & Michael Hausenblas (O'Reilly, 2021) | Attacker's perspective; the chapter on RBAC abuse is the strongest |
| Kubernetes Security and Observability β Brendan Burns et al. (O'Reilly, 2021) | Defender's perspective; pairs perfectly with Liz Rice's book |
Talks & specs:
- π₯ "Container Security: It's All About Trust" β Liz Rice, KubeCon EU 2020
- π₯ "Leaky Vessels: runc Container Escape" β Snyk team, RSA 2024
- π NIST SP 800-190 β Container Security Guide
- π Pod Security Standards
- π Trivy Documentation
- π CIS Kubernetes Benchmark
Takeaways:
| # | π§ Insight |
|---|---|
| 1 | A container is a Linux process. Treat it like one: minimal privilege, no shell, read-only root. |
| 2 | Trivy is your one-tool answer for image + config + secrets + cluster scanning. Wire it everywhere. |
| 3 | PSS restricted should be the default for app namespaces. If restricted fails, the pod has questions to answer. |
| 4 | Default-deny NetworkPolicy is to K8s what default-deny IAM is to AWS β the foundational discipline. |
| 5 | runc-style runtime CVEs happen even with perfect images. Defense in depth at runtime (L9) is mandatory. |
| 6 | One image, multiple scans (CVE + misconfig + secret). Don't conflate them. |
π¬ "Containers don't contain. That's the most useful sentence in container security." β Daniel J. Walsh (Red Hat), Linux Conf 2014.