Lab — Week 2: OS Hardening and Secrets Management
Due: 17 July 2026
Submission: GitLab repo secdevops-s26-<CECS>
Overview
Reduce the attack surface of the freshly provisioned Ubuntu server — removing what isn’t needed, locking down what remains — then replace plaintext credentials with a proper secrets manager.
- Part 1 — OS Hardening reduces the attack surface of
ubuntu-serverby removing unnecessary services, hardening SSH, auditing file permissions, and deploying fail2ban. Every change should be traceable to a threat in your threat model. - Part 2 — Secrets Management with OpenBao replaces plaintext credentials with an OpenBao instance, demonstrates the policy model that enforces least-privilege access to secrets, and verifies that no credentials exist in plaintext anywhere on the system.
Prerequisites
- Week 1 Part 1 complete — ubuntu-server running and reachable
- Week 1 Part 2 complete — threat model exists in
lab02/threat-model.md - OpenBao installed on ubuntu-server (by Ansible) — for Part 2
Part 1: OS Hardening
A freshly installed Ubuntu 26.04 server is configured for convenience, not security. This part reduces the attack surface of ubuntu-server by removing unnecessary services, hardening SSH, auditing file permissions, and deploying fail2ban. Every change you make here should be traceable to a threat in your threat model.
SSH Hardening
- Back up the stock file first — the diff you submit is taken against it:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.origThen edit
/etc/ssh/sshd_configon ubuntu-server to implement at minimum:PasswordAuthentication noPermitRootLogin noAllowUsers dmcgrathMaxAuthTries 3LoginGraceTime 30X11Forwarding no
Two things about
AllowUsersthat will otherwise lock you out of your own VM:- It is a whitespace-separated list, not comma-separated.
AllowUsers dmcgrath, studentcreates a permitted username ofdmcgrath,— with the comma — and nobody can log in asdmcgrath. The correct form isAllowUsers dmcgrath student. - The list above is a minimum. Add your own account, or you will lock yourself out the moment you reload sshd.
Note also that several of these directives already appear in the stock file, some uncommented. sshd uses the first value it sees for most keywords, so appending
X11Forwarding noat the end while an uncommentedX11Forwarding yesremains higher up leaves forwarding enabled. Change the existing line or comment it out. -
Test the configuration before reloading:
sudo sshd -tKeep your current SSH session open while you test, and confirm you can open a second session before closing the first. A bad
sshd_configthat passes-tcan still lock you out, and the open session is your way back in. -
Reload sshd:
sudo systemctl reload sshd - Verify from Kali that password authentication is rejected:
ssh -o PreferredAuthentications=password dmcgrath@10.10.10.20 # Must fail with: Permission denied (publickey)
Service Minimization
-
List all running services:
systemctl list-units --type=service --state=running -
Identify and disable at least three services that are not needed for the course workload (nginx, MariaDB, Docker are needed — everything else is a candidate). Document each with: service name, why it was disabled, and what attack surface it removes.
-
Mask any services that should never start:
sudo systemctl mask <service>
Audit Script
- Write
scripts/harden-audit.shon ubuntu-server (commit the script to your repo). The script must check all of the following and print PASS or FAIL for each:PermitRootLoginisnoPasswordAuthenticationisno- No world-writable files exist in
/etc - No unexpected SUID binaries exist in
/tmpor/var/tmp - The
dmcgrathaccount has a valid SSH key in~/.ssh/authorized_keys
- Run the script and fix all FAIL results. Re-run to confirm all checks pass.
fail2ban
-
Configure
/etc/fail2ban/jail.localto protect SSH with:maxretry = 3,findtime = 600,bantime = 3600. Use thenftables-multiportbanaction.Every setting must live inside a section.
jail.localis INI-format and fail2ban’s parser rejects anykey = valuethat appears before the first[section]header — the whole file then fails to load and you silently fall back to the packaged defaults injail.conf, which is easy to mistake for success because bans still happen (atjail.conf’s thresholds, not yours):[DEFAULT] bantime = 3600 findtime = 600 maxretry = 3 [sshd] enabled = true banaction = nftables-multiport -
Enable and start fail2ban:
sudo systemctl enable --now fail2ban - Confirm the running daemon actually agrees with your file, rather than assuming it parsed:
sudo fail2ban-client get sshd maxretry # must print 3 sudo fail2ban-client get sshd banaction # must print nftables-multiport - From Kali, trigger the ban by attempting five failed SSH logins. Confirm the Kali IP appears in fail2ban’s ban list:
sudo fail2ban-client status sshd
Threat Model Update
- In
lab02/threat-model.md, mark any threats that are now mitigated. Update their status fromOPENtoMITIGATEDand add the control applied.
Deliverables
Create lab03/ in your repo containing:
sshd_configdiff —diff /etc/ssh/sshd_config.orig /etc/ssh/sshd_configor equivalent showing your changes- Service audit — table listing each disabled service, the reason, and the attack surface removed
scripts/harden-audit.sh— the script itself- Audit script output — screenshot or paste showing all checks passing
- fail2ban configuration — contents of
/etc/fail2ban/jail.local - fail2ban test — screenshot of
fail2ban-client status sshdshowing the Kali IP banned - Updated
lab02/threat-model.md— with newly mitigated threats marked
All findings and changes must be documented in lab03/lab03.md with a brief explanation of why each change was made, referencing specific threat model entries.
Part 2: Secrets Management with OpenBao
Hardcoded credentials are one of the most common and consequential security failures. This part replaces plaintext credentials on ubuntu-server with an OpenBao instance, demonstrates the policy model that enforces least-privilege access to secrets, and verifies that no credentials exist in plaintext anywhere on the system.
OpenBao is the open-source (Linux Foundation) fork of HashiCorp Vault; its CLI is bao and its API is Vault-compatible.
OpenBao Setup
- Create
/etc/openbao/openbao.hclwith a file storage backend listening on127.0.0.1:8200(TLS disabled for lab use):ui = true api_addr = "http://127.0.0.1:8200" storage "file" { path = "/opt/openbao/data" } listener "tcp" { address = "127.0.0.1:8200" tls_disable = true }HCL has no statement separator — attributes are separated by newlines, and a semicolon is a syntax error. A block written on one line may contain at most one attribute, so single-attribute blocks like
storagecould be collapsed tostorage "file" { path = "/opt/openbao/data" }, but a two-attribute block likelistenercannot. Writing every block multi-line, as above, always works. - Create the storage and audit-log directories and set ownership. OpenBao will not create either one, and it refuses to start if it cannot write to them:
sudo mkdir -p /opt/openbao/data /var/log/openbao sudo chown openbao:openbao /opt/openbao/data /var/log/openbao -
Enable and start OpenBao:
sudo systemctl enable --now openbao - Initialize OpenBao with 5 key shares and a threshold of 3:
export BAO_ADDR='http://127.0.0.1:8200' bao operator init -key-shares=5 -key-threshold=3Save the unseal keys and root token somewhere secure (a local file is fine for the lab).
-
Unseal OpenBao using 3 of the 5 keys:
bao operator unseal <key> - Enable the audit log:
bao audit enable file file_path=/var/log/openbao/audit.log
Secrets and Policies
-
Enable the KV-v2 secrets engine:
bao secrets enable -path=secret kv-v2 - Store the DVWA database credentials:
bao kv put secret/dvwa/db username=dvwauser password=$(openssl rand -base64 16) -
Write a read-only policy file
policy/dvwa-read.hclthat allowsreadonsecret/data/dvwa/*andlistonsecret/metadata/dvwa/*. - Load the policy and create a 24-hour service token scoped to it:
bao policy write dvwa-read policy/dvwa-read.hcl bao token create -policy=dvwa-read -ttl=24h -display-name=dvwa-app - Verify the token can read but not write:
BAO_TOKEN=<service-token> bao kv get secret/dvwa/db # must succeed BAO_TOKEN=<service-token> bao kv put secret/dvwa/db x=y # must failReplace
<service-token>with the literal token string from step 10 and delete the angle brackets. If you paste the placeholder as-is,bashreads the<as an input redirection, tries to open a file namedservice-token, and the command never runs:-bash: service-token: No such file or directoryThe shell then leaves whatever token was already in your environment in place, so the write “succeeds” — because it ran as
root, not asdvwa-app. If your write does not fail, check that the read actually ran under the service token before concluding the policy is wrong. The failure you are looking for ispermission denied.
Python Integration
-
Write
scripts/get-secret.pythat reads thepasswordfield fromsecret/dvwa/dbusing thehvaclibrary and prints it. The script must readBAO_ADDRandBAO_TOKENfrom environment variables — no hardcoded values. -
Run it:
BAO_TOKEN=<service-token> python3 scripts/get-secret.py
Verify No Plaintext Credentials
- Search the system for plaintext credentials:
sudo grep -rn --include="*.conf" --include="*.yml" --include="*.yaml" \ -E '(password|passwd|secret)\s*[:=]\s*\S+' /etc /opt 2>/dev/null \ | grep -vE ':[0-9]+:[[:space:]]*#'grep -rnprefixes every result withpath:lineno:, so a filter anchored at^never sees the start of the matched line — the secondgrephas to skip past the prefix, which is what:[0-9]+:does.Expect hits even on a clean system, and triage them rather than remediating them all. Only a result that contains a real credential needs fixing. Things that will match and are not findings:
/etc/nsswitch.conf(passwd: files systemdis a name-service database), fail2ban filter regexes that match authentication-failure log lines, and shipped placeholders such as/etc/audit/zos-remote.conf. In your writeup, say which hits you classified as noise and why — deciding what a finding is not is most of what secret scanning consists of.
Deliverables
Create lab04/ in your repo containing:
/etc/openbao/openbao.hcl— your OpenBao configuration (strip any sensitive values)policy/dvwa-read.hcl— the policy file- Token verification output — paste showing the service token succeeding on read and failing on write
scripts/get-secret.py— the Python script- Script output — showing successful secret retrieval
- Plaintext credential scan — output of the grep command, with each hit classified as a real credential (and remediated) or as noise (with the reason)
lab04/lab04.md— writeup documenting the secrets architecture: what is stored in OpenBao, which token has access, and how the application retrieves its credentials at runtime
Submission
Commit all files for both parts — lab03/ and lab04/ — and push to your GitLab repo. Do not commit the root OpenBao token, unseal keys, or any actual secret values. The lab is considered submitted when the commit appears in GitLab before the due date.