courses

Lab — Week 4: Container Security II — Scanning, Supply Chain, and Monitoring
Due: 31 July 2026
Submission: GitLab repo secdevops-s26-<CECS>

Overview

Look inside the containers — scanning for known CVEs in OS packages and dependencies — and bring the Wazuh agent online so everything from here on is observed.

Prerequisites


Part 1: Image Scanning and Supply Chain Security

Every container image inherits vulnerabilities from every layer below it. This part scans your hardened images for known CVEs, makes evidence-based remediation decisions, generates an SBOM, and produces a scanning script suitable for use in a CI pipeline.

Initial Scan

  1. Scan each image for HIGH and CRITICAL vulnerabilities:
    trivy image --severity HIGH,CRITICAL dvwa:hardened
    trivy image --severity HIGH,CRITICAL nginx-proxy:hardened
    trivy image --severity HIGH,CRITICAL mariadb:11
    
  2. Save the reports in JSON format:
    trivy image --format json --output lab07/trivy-dvwa-before.json dvwa:hardened
    trivy image --format json --output lab07/trivy-nginx-before.json nginx-proxy:hardened
    

Triage and Remediation

  1. For every HIGH and CRITICAL finding across all images, complete a triage entry in lab07/triage.md using this format:

    CVE Severity Package Image Classification Rationale Action
    CVE-XXXX-XXXX CRITICAL libssl3 dvwa True Positive — fix Remotely exploitable in network service Rebuild with updated base

    Classification must be one of:

    • True Positive — fix: exploitable in your environment; rebuild
    • True Positive — accept: exploitable but mitigated elsewhere; document rationale
    • False Positive: scanner error or not applicable; document reasoning
  2. For every True Positive — fix finding: update the relevant Dockerfile to use a newer base image or explicitly upgrade the affected package, rebuild the image, and re-scan to confirm the CVE no longer appears.

  3. Save post-remediation scans:

    trivy image --format json --output lab07/trivy-dvwa-after.json dvwa:hardened
    trivy image --format json --output lab07/trivy-nginx-after.json nginx-proxy:hardened
    

SBOM Generation

  1. Generate a CycloneDX SBOM for the hardened DVWA image:
    trivy image --format cyclonedx --output lab07/sbom-dvwa.json dvwa:hardened
    

    Note which subcommand does what — this trips people up. trivy image generates an SBOM from an image; trivy sbom consumes one, taking a path to an existing SBOM file and scanning it for vulnerabilities. So once you have the file above, you can scan it later without the image being present:

    trivy sbom lab07/sbom-dvwa.json
    

    That second command is the whole point of keeping an SBOM: you can answer “am I affected?” from the inventory alone, months after the image itself is gone.

  2. Answer in lab07/lab07.md: if CVE-2021-44228 (Log4Shell) were disclosed today, how would you use this SBOM to determine whether your DVWA image is affected? What command would you run?

CI Gate Script

  1. Write scripts/scan-images.sh that:
    • Scans dvwa:hardened and nginx-proxy:hardened
    • Exits non-zero if any CRITICAL CVE is found in either image
    • Prints a summary line for each image (e.g., dvwa:hardened — 0 CRITICAL, 2 HIGH)

    The exit code is the whole deliverable here — it is the only part a CI pipeline reads. Printing FAILED is not failing. A shell script that runs off the end exits with the status of its last command, which for most scripts is the final echo, so a script that reports a CRITICAL and then prints a summary will hand CI a clean 0 and the build goes green. End the script with an explicit exit on the value you accumulated.

  2. Run the script and confirm the exit code, rather than reading the output:
    bash scripts/scan-images.sh; echo "exit=$?"
    

    With your remediated images this must print exit=0.

  3. Now prove the gate can actually fail — a gate that has only ever passed is indistinguishable from a gate that always passes. Scan an image you know is bad and confirm the exit code is non-zero:
    docker pull vulnerables/web-dvwa
    bash scripts/scan-images.sh vulnerables/web-dvwa; echo "exit=$?"
    

    If your script hardcodes the two image names, temporarily point it at this image instead. Capture both exit codes in your writeup.

Deliverables

Create lab07/ in your repo containing:


Part 2: Container Networking, Secrets Integration, and Wazuh Agent

This part connects three threads: secrets from OpenBao reach containers without appearing in environment variables, network segmentation is verified by attempting forbidden connections, and the Wazuh agent is registered so that everything you do from this point forward is observed by the SIEM.

Part 1 and Week 2 Part 2 must be complete — hardened stack running and OpenBao running and unsealed; the Wazuh manager must be running on 10.10.10.30.

Secrets Integration

  1. Update docker-compose.yml to pass the DVWA database password from OpenBao rather than hardcoding it. Use Docker Compose secrets or an OpenBao Agent sidecar — your choice. Document your approach.

    Option A — Docker Compose secrets:

    secrets:
      db_password:
        file: ./secrets/db_password.txt   # populated from OpenBao at deploy time
    services:
      dvwa:
        secrets: [db_password]
        # reads /run/secrets/db_password at runtime
    

    “Populated from OpenBao at deploy time” means exactly that — use the script you wrote in Week 2, and keep the resulting file out of git:

    mkdir -p secrets && chmod 700 secrets
    printf 'secrets/\n' >> .gitignore
    BAO_TOKEN=<service-token> python3 scripts/get-secret.py > secrets/db_password.txt
    chmod 600 secrets/db_password.txt
    git check-ignore -v secrets/db_password.txt    # must print a matching rule
    

    Be clear-eyed about what this buys you: the credential is out of the container’s environment and out of docker inspect, but it now sits in plaintext on the host filesystem and has to be repopulated whenever it rotates. That tradeoff is what your writeup should discuss.

    Option B — OpenBao Agent sidecar: configure an openbao-agent container that writes the secret to a shared volume mounted into the dvwa container. More moving parts, but the credential never lands on the host filesystem and the agent renews it on its own.

    Whichever you choose, apply it to both sides of the connection. The mariadb service needs the same password to create the account that DVWA authenticates with, so leaving MARIADB_PASSWORD=... in the Compose environment leaves the credential in the repository — and the docker inspect check in the next step only looks at the dvwa container, so it will still pass.

  2. Verify the password is not visible in docker inspect:

    docker inspect $(docker compose ps -q dvwa) \
      | jq '.[0].Config.Env[] | select(test("password|secret"; "i"))'
    # Must return nothing
    

Network Isolation Verification

  1. Confirm that all expected paths work:
    # nginx → dvwa: must succeed
    docker compose exec nginx curl -s -o /dev/null -w "%{http_code}" http://dvwa/
    # dvwa → mariadb: must succeed
    docker compose exec dvwa mariadb-admin ping -h mariadb -u dvwauser
    

    The second command needs a MariaDB client inside the dvwa image. If you left it out — reasonably, since DVWA talks to the database through PHP’s mysqli, not the CLI — use the network-level equivalent instead, which does not depend on what is installed in the image:

    docker run --rm --network <project>_backend busybox \
      sh -c 'nc -z -w 5 mariadb 3306; echo "exit=$?"'    # must be 0
    
  2. Confirm that forbidden paths fail:
    # nginx → mariadb: must fail
    docker run --rm --network <project>_frontend busybox \
      sh -c 'nc -z -w 5 mariadb 3306; echo "exit=$?"'    # must be non-zero
    

    Do not run this as docker compose exec nginx mariadb-admin .... A hardened nginx image has no MariaDB client, so the command fails with executable file not found whether or not the networks are segmented — it tests the image, not the network, and it passes for the wrong reason.

    Document each result in lab08/lab08.md with an explanation of which network policy enforces it, and say explicitly which of executable file not found, DNS resolution failure, connection refused, or timeout you observed. Only the last two are evidence about the network.

Wazuh Agent Registration

  1. Configure the Wazuh agent on ubuntu-server to point to the manager. The agent is already installed by the Week 1 Ansible playbook, so set the manager address in the configuration file directly:
    sudo sed -i 's|<address>.*</address>|<address>10.10.10.30</address>|' \
        /var/ossec/etc/ossec.conf
    sudo grep -A3 '<server>' /var/ossec/etc/ossec.conf     # confirm before restarting
    sudo systemctl enable --now wazuh-agent
    

    The relevant stanza is <ossec_config><client><server><address>. Set <agent_name> under <client> too if you want the agent to register as ubuntu-server rather than the hostname.

    The WAZUH_MANAGER=... apt-get install wazuh-agent deployment variables documented by Wazuh are applied by the package’s post-install script and only take effect on a first install, when no ossec.conf exists yet. They will not reconfigure an agent that Ansible has already installed and configured.

  2. On the Wazuh manager (10.10.10.30), confirm the agent appears:
    sudo /var/ossec/bin/agent_control -l
    
  3. In the Wazuh dashboard (https://10.10.10.30), navigate to Agents and take a screenshot showing ubuntu-server with status Active.

  4. Enable the Docker Wodle in /var/ossec/etc/ossec.conf on ubuntu-server so Wazuh monitors container lifecycle events:
    <wodle name="docker-listener">
      <interval>10m</interval>
      <attempts>5</attempts>
      <run_on_start>yes</run_on_start>
      <disabled>no</disabled>
    </wodle>
    

    Restart the agent: sudo systemctl restart wazuh-agent

  5. From the Wazuh dashboard, confirm at least one Docker event appears in Security Events after restarting or creating a container.

Threat Model Update

  1. In lab02/threat-model.md, mark threats mitigated by secrets integration and Wazuh monitoring. Add new threats surfaced by having an agent in place (e.g., what if the agent itself is compromised?).

Deliverables

Create lab08/ in your repo containing:


Submission

Commit all files for both parts — lab07/ and lab08/ — and push to your GitLab repo. The lab is considered submitted when the commit appears in GitLab before the due date.