courses

Lab — Week 3: Container Security I — Threat Model and Image Hardening
Due: 24 July 2026
Submission: GitLab repo secdevops-s26-<CECS>

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.

Prerequisites


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

  1. 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

  1. Write Dockerfile.dvwa on ubuntu-server (commit to your repo). Requirements:
    • Use a minimal base image (debian:bookworm-slim or ubuntu: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 WORKDIR appropriately
  2. Build the image: docker build -t dvwa:hardened -f Dockerfile.dvwa .

  3. Verify the container runs as non-root:
    docker run --rm dvwa:hardened whoami
    # Must NOT print "root"
    
  4. Inspect layers for accidental secrets:
    docker history --no-trunc dvwa:hardened
    docker inspect dvwa:hardened | jq '.[0].Config.Env'
    

nginx Dockerfile

  1. Write Dockerfile.nginx for an nginx reverse proxy that will front DVWA. Requirements:
    • Minimal base image
    • Non-root user
    • Copy a basic nginx.conf that proxies / → http://dvwa:80/
    • Expose only port 80 (TLS termination comes in a later lab)
  2. Build: docker build -t nginx-proxy:hardened -f Dockerfile.nginx .

Container Check Script

  1. Write scripts/container-check.sh that 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.

  2. 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

  1. 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:


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

  1. Write docker-compose.yml on ubuntu-server that brings up three services: nginx, dvwa, and mariadb. For every service, apply:
    • read_only: true
    • cap_drop: [ALL]
    • security_opt: [no-new-privileges:true]
    • Only add back capabilities that are actually required (document each one)
    • tmpfs mounts or named volumes for any paths that must be writable
  2. Use the hardened images from Part 1 for nginx and dvwa. Use mariadb:11 for the database (DVWA speaks the MySQL/MariaDB protocol, not PostgreSQL).

Network Segmentation

  1. Define two Compose networks: frontend and backend.
    • nginx is on frontend only
    • dvwa is on both (it bridges the two)
    • mariadb is on backend only
  2. Bring the stack up: docker compose up -d

  3. Verify that DVWA is reachable through nginx: curl -s -o /dev/null -w "%{http_code}" http://localhost

  4. 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

  1. Create seccomp-dvwa.json — a custom seccomp profile based on the Docker default that additionally blocks ptrace and reboot. Apply it to the dvwa service in docker-compose.yml.

  2. Verify the profile is applied:

    docker inspect $(docker compose ps -q dvwa) \
      | jq '.[0].HostConfig.SecurityOpt'
    

Verify the Full Stack

  1. Run scripts/container-check.sh from Part 1 against all three running containers and confirm all checks pass.

Threat Model Update

  1. 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:


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.