Lab — Week 4: Container Security II — Scanning, Supply Chain, and Monitoring
Due: 31 July 2026
Submission: GitLab repo secdevops-s26-<CECS>
- Overview
- Prerequisites
- Part 1: Image Scanning and Supply Chain Security
- Part 2: Container Networking, Secrets Integration, and Wazuh Agent
- Submission
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.
- Part 1 — Image Scanning and Supply Chain Security scans your hardened images for known CVEs, makes evidence-based remediation decisions, generates an SBOM, and produces a CI-ready scanning script.
- Part 2 — Container Networking, Secrets Integration, and Wazuh Agent delivers secrets to containers without exposing them, verifies network segmentation, and registers the Wazuh agent so everything from here on is observed by the SIEM.
Prerequisites
- Week 3 Part 2 complete — hardened images built and stack running
- Week 2 Part 2 complete — OpenBao running and unsealed on ubuntu-server (for Part 2)
- Wazuh manager running on 10.10.10.30 (for Part 2)
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
- 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 - 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
-
For every HIGH and CRITICAL finding across all images, complete a triage entry in
lab07/triage.mdusing 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
-
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.
-
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
- Generate a CycloneDX SBOM for the hardened DVWA image:
trivy image --format cyclonedx --output lab07/sbom-dvwa.json dvwa:hardenedNote which subcommand does what — this trips people up.
trivy imagegenerates an SBOM from an image;trivy sbomconsumes 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.jsonThat 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.
- 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
- Write
scripts/scan-images.shthat:- Scans
dvwa:hardenedandnginx-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
FAILEDis not failing. A shell script that runs off the end exits with the status of its last command, which for most scripts is the finalecho, so a script that reports a CRITICAL and then prints a summary will hand CI a clean0and the build goes green. End the script with an explicitexiton the value you accumulated. - Scans
- 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. - 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:
trivy-dvwa-before.jsonandtrivy-nginx-before.json— pre-remediation scanstriage.md— triage table for all HIGH/CRITICAL findings- Updated Dockerfiles — showing the remediation changes
trivy-dvwa-after.jsonandtrivy-nginx-after.json— post-remediation scanssbom-dvwa.json— CycloneDX SBOM for DVWAscripts/scan-images.sh— CI gate script, with both exit codes captured:0against your remediated images and non-zero against a known-bad imagelab07/lab07.md— SBOM Log4Shell answer and a brief reflection on CVE count before vs. after
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
-
Update
docker-compose.ymlto 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 ruleBe 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-agentcontainer that writes the secret to a shared volume mounted into thedvwacontainer. 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
mariadbservice needs the same password to create the account that DVWA authenticates with, so leavingMARIADB_PASSWORD=...in the Compose environment leaves the credential in the repository — and thedocker inspectcheck in the next step only looks at the dvwa container, so it will still pass. -
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
- 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 dvwauserThe 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 - 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-zeroDo not run this as
docker compose exec nginx mariadb-admin .... A hardened nginx image has no MariaDB client, so the command fails withexecutable file not foundwhether 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.mdwith an explanation of which network policy enforces it, and say explicitly which ofexecutable file not found, DNS resolution failure, connection refused, or timeout you observed. Only the last two are evidence about the network.
Wazuh Agent Registration
- 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-agentThe relevant stanza is
<ossec_config><client><server><address>. Set<agent_name>under<client>too if you want the agent to register asubuntu-serverrather than the hostname.The
WAZUH_MANAGER=... apt-get install wazuh-agentdeployment variables documented by Wazuh are applied by the package’s post-install script and only take effect on a first install, when noossec.confexists yet. They will not reconfigure an agent that Ansible has already installed and configured. - On the Wazuh manager (10.10.10.30), confirm the agent appears:
sudo /var/ossec/bin/agent_control -l -
In the Wazuh dashboard (
https://10.10.10.30), navigate to Agents and take a screenshot showingubuntu-serverwith status Active. - Enable the Docker Wodle in
/var/ossec/etc/ossec.confon 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 - From the Wazuh dashboard, confirm at least one Docker event appears in Security Events after restarting or creating a container.
Threat Model Update
- 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:
- Updated
docker-compose.yml— with secrets integration applied docker inspectoutput — confirming no password in the container environment, for both the dvwa and mariadb containers- Network path tests — paste of every command from steps 3 and 4 with its output: the two permitted paths succeeding and the forbidden one failing
- Wazuh agent list —
agent_control -loutput showing ubuntu-server active - Wazuh dashboard screenshot — ubuntu-server agent status Active
- Docker event in Wazuh — screenshot of at least one Docker lifecycle event in the dashboard
- Updated
lab02/threat-model.md lab08/lab08.md— explanation of secrets approach chosen and network policy analysis
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.