Lab — Week 3: Container Security I — Threat Model and Image Hardening
Due: 24 July 2026
Submission: GitLab repo secdevops-s26-<CECS>
- Overview
- Prerequisites
- Part 1: Container Threat Model and Image Hardening
- Part 2: Container Runtime Hardening
- Submission
Overview
Understand what containers actually isolate (and what they don’t), harden the DVWA and nginx images, and apply runtime constraints that limit what a compromised container can do.
- Part 1 — Container Threat Model and Image Hardening applies your threat modeling skills to the containerized workload, then builds hardened Docker images for DVWA and nginx.
- Part 2 — Container Runtime Hardening applies seccomp profiles, capability restrictions, read-only filesystems, and network segmentation, then composes the stack into a single hardened
docker-compose.yml.
Prerequisites
- Week 1 Part 1 complete — ubuntu-server running with Docker installed
- Week 1 Part 2 complete — threat model exists in
lab02/threat-model.md
Part 1: Container Threat Model and Image Hardening
Containers are not a security boundary — they share the host kernel. This part applies the threat modeling skills from Week 1 Part 2 to the containerized workload, then builds hardened Docker images for DVWA and nginx that implement as many of those mitigations as possible at the image layer.
Container Threat Model Extension
- In
lab02/threat-model.md, add at least three new STRIDE threats that are specific to the containerized workload. Consider: container escape, secrets in image layers, network reachability between containers, and privilege escalation via root-in-container.
DVWA Dockerfile
- Write
Dockerfile.dvwaon ubuntu-server (commit to your repo). Requirements:- Use a minimal base image (
debian:bookworm-slimorubuntu:26.04) - Install only the packages DVWA requires
- Create a non-root system user and switch to it with
USER - Do not include any credentials, API keys, or passwords in any layer
- Set
WORKDIRappropriately
- Use a minimal base image (
-
Build the image:
docker build -t dvwa:hardened -f Dockerfile.dvwa . - Verify the container runs as non-root:
docker run --rm dvwa:hardened whoami # Must NOT print "root" - Inspect layers for accidental secrets:
docker history --no-trunc dvwa:hardened docker inspect dvwa:hardened | jq '.[0].Config.Env'
nginx Dockerfile
- Write
Dockerfile.nginxfor an nginx reverse proxy that will front DVWA. Requirements:- Minimal base image
- Non-root user
- Copy a basic
nginx.confthat proxies/ → http://dvwa:80/ - Expose only port 80 (TLS termination comes in a later lab)
- Build:
docker build -t nginx-proxy:hardened -f Dockerfile.nginx .
Container Check Script
- Write
scripts/container-check.shthat accepts a container name as its first argument and checks:- The process inside is not running as
root(UID 0) - The container is not running with
--privileged - No environment variables match
password|secret|token|key(case-insensitive)
Exit 0 if all checks pass; exit non-zero otherwise.
- The process inside is not running as
- Start both containers and run the script against each:
docker run -d --name dvwa_test dvwa:hardened sleep 3600 docker run -d --name nginx_test nginx-proxy:hardened bash scripts/container-check.sh dvwa_test bash scripts/container-check.sh nginx_test docker rm -f dvwa_test nginx_test
Threat Model Update
- In
lab02/threat-model.md, mark any threats now addressed by image hardening. Update status and note the specific control.
Deliverables
Create lab05/ in your repo containing:
Dockerfile.dvwa— hardened DVWA imageDockerfile.nginx— hardened nginx reverse proxy imagescripts/container-check.sh— the check script- Non-root verification —
docker run --rm dvwa:hardened whoamioutput (not root) - Layer inspection —
docker historyoutput showing no secrets in any layer - Check script output — both containers passing all checks
- Updated
lab02/threat-model.md— container threats added and any newly mitigated entries updated lab05/lab05.md— for each Dockerfile decision (base image choice, USER, packages), one sentence explaining the security rationale
Part 2: Container Runtime Hardening
Dockerfile hardening controls what is in an image. Runtime hardening controls what a running container is allowed to do. This part applies seccomp profiles, capability restrictions, read-only filesystems, and network segmentation to the lab stack, then composes it into a single hardened docker-compose.yml.
Part 1 must be complete — dvwa:hardened and nginx-proxy:hardened images built.
Hardened docker-compose.yml
- Write
docker-compose.ymlon ubuntu-server that brings up three services:nginx,dvwa, andmariadb. For every service, apply:read_only: truecap_drop: [ALL]security_opt: [no-new-privileges:true]- Only add back capabilities that are actually required (document each one)
tmpfsmounts or named volumes for any paths that must be writable
- Use the hardened images from Part 1 for
nginxanddvwa. Usemariadb:11for the database (DVWA speaks the MySQL/MariaDB protocol, not PostgreSQL).
Network Segmentation
- Define two Compose networks:
frontendandbackend.nginxis onfrontendonlydvwais on both (it bridges the two)mariadbis onbackendonly
-
Bring the stack up:
docker compose up -d -
Verify that DVWA is reachable through nginx:
curl -s -o /dev/null -w "%{http_code}" http://localhost - Verify that nginx cannot directly reach mariadb:
docker compose exec nginx mariadb -h mariadb -u dvwauser 2>&1 # Must fail — mariadb not reachable from frontend network
seccomp Profile
-
Create
seccomp-dvwa.json— a custom seccomp profile based on the Docker default that additionally blocksptraceandreboot. Apply it to thedvwaservice indocker-compose.yml. -
Verify the profile is applied:
docker inspect $(docker compose ps -q dvwa) \ | jq '.[0].HostConfig.SecurityOpt'
Verify the Full Stack
- Run
scripts/container-check.shfrom Part 1 against all three running containers and confirm all checks pass.
Threat Model Update
- In
lab02/threat-model.md, mark any threats now mitigated by runtime hardening. For each, name the specific control (cap_drop,read_only, network segmentation, seccomp).
Deliverables
Create lab06/ in your repo containing:
docker-compose.yml— fully hardened, with inline comments explaining each security optionseccomp-dvwa.json— custom seccomp profile- DVWA reachability —
curloutput showing HTTP 200 (or redirect) through nginx - Network isolation test — output showing nginx cannot reach mariadb
- seccomp verification —
docker inspectoutput showing the profile is applied - Container check — all three containers passing
scripts/container-check.sh - Updated
lab02/threat-model.md lab06/lab06.md— for each dropped capability, explain what it would allow and why the service doesn’t need it
Submission
Commit all files for both parts — lab05/ and lab06/ — and push to your GitLab repo. The lab is considered submitted when the commit appears in GitLab before the due date.