courses

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.

Prerequisites


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

  1. Back up the stock file first — the diff you submit is taken against it:
    sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig
    

    Then edit /etc/ssh/sshd_config on ubuntu-server to implement at minimum:

    • PasswordAuthentication no
    • PermitRootLogin no
    • AllowUsers dmcgrath
    • MaxAuthTries 3
    • LoginGraceTime 30
    • X11Forwarding no

    Two things about AllowUsers that will otherwise lock you out of your own VM:

    • It is a whitespace-separated list, not comma-separated. AllowUsers dmcgrath, student creates a permitted username of dmcgrath, — with the comma — and nobody can log in as dmcgrath. The correct form is AllowUsers 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 no at the end while an uncommented X11Forwarding yes remains higher up leaves forwarding enabled. Change the existing line or comment it out.

  2. Test the configuration before reloading: sudo sshd -t

    Keep your current SSH session open while you test, and confirm you can open a second session before closing the first. A bad sshd_config that passes -t can still lock you out, and the open session is your way back in.

  3. Reload sshd: sudo systemctl reload sshd

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

  1. List all running services: systemctl list-units --type=service --state=running

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

  3. Mask any services that should never start:

    sudo systemctl mask <service>
    

Audit Script

  1. Write scripts/harden-audit.sh on ubuntu-server (commit the script to your repo). The script must check all of the following and print PASS or FAIL for each:
    • PermitRootLogin is no
    • PasswordAuthentication is no
    • No world-writable files exist in /etc
    • No unexpected SUID binaries exist in /tmp or /var/tmp
    • The dmcgrath account has a valid SSH key in ~/.ssh/authorized_keys
  2. Run the script and fix all FAIL results. Re-run to confirm all checks pass.

fail2ban

  1. Configure /etc/fail2ban/jail.local to protect SSH with: maxretry = 3, findtime = 600, bantime = 3600. Use the nftables-multiport banaction.

    Every setting must live inside a section. jail.local is INI-format and fail2ban’s parser rejects any key = value that appears before the first [section] header — the whole file then fails to load and you silently fall back to the packaged defaults in jail.conf, which is easy to mistake for success because bans still happen (at jail.conf’s thresholds, not yours):

    [DEFAULT]
    bantime  = 3600
    findtime = 600
    maxretry = 3
    
    [sshd]
    enabled   = true
    banaction = nftables-multiport
    
  2. Enable and start fail2ban: sudo systemctl enable --now fail2ban

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

  1. In lab02/threat-model.md, mark any threats that are now mitigated. Update their status from OPEN to MITIGATED and add the control applied.

Deliverables

Create lab03/ in your repo containing:

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

  1. Create /etc/openbao/openbao.hcl with a file storage backend listening on 127.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 storage could be collapsed to storage "file" { path = "/opt/openbao/data" }, but a two-attribute block like listener cannot. Writing every block multi-line, as above, always works.

  2. 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
    
  3. Enable and start OpenBao: sudo systemctl enable --now openbao

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

    Save the unseal keys and root token somewhere secure (a local file is fine for the lab).

  5. Unseal OpenBao using 3 of the 5 keys: bao operator unseal <key>

  6. Enable the audit log: bao audit enable file file_path=/var/log/openbao/audit.log

Secrets and Policies

  1. Enable the KV-v2 secrets engine: bao secrets enable -path=secret kv-v2

  2. Store the DVWA database credentials:
    bao kv put secret/dvwa/db username=dvwauser password=$(openssl rand -base64 16)
    
  3. Write a read-only policy file policy/dvwa-read.hcl that allows read on secret/data/dvwa/* and list on secret/metadata/dvwa/*.

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

    Replace <service-token> with the literal token string from step 10 and delete the angle brackets. If you paste the placeholder as-is, bash reads the < as an input redirection, tries to open a file named service-token, and the command never runs:

    -bash: service-token: No such file or directory
    

    The shell then leaves whatever token was already in your environment in place, so the write “succeeds” — because it ran as root, not as dvwa-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 is permission denied.

Python Integration

  1. Write scripts/get-secret.py that reads the password field from secret/dvwa/db using the hvac library and prints it. The script must read BAO_ADDR and BAO_TOKEN from environment variables — no hardcoded values.

  2. Run it: BAO_TOKEN=<service-token> python3 scripts/get-secret.py

Verify No Plaintext Credentials

  1. 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 -rn prefixes every result with path:lineno:, so a filter anchored at ^ never sees the start of the matched line — the second grep has 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 systemd is 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:


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.