Skip to main content

Module 13 — Final Security Re-test

Objective

Re-run every exploit from Modules 08-12 against the now-secured application. Confirm all attacks fail. Document the security posture of the complete system.

By the end of this module you will have:

OutcomeDetail
Re-testedEvery attack from Modules 08-12 against the hardened system
ConfirmedAll five categories of attacks are now blocked
DocumentedBefore/after security posture for each vulnerability
ReflectedOn the difference between "it works" and "it's secure"

Prerequisites

  • Modules 08-12 complete (all fixes applied)
  • App running and accessible via Nginx and Tailscale Funnel
  • SSH access to all three VMs

1. Security Checklist — Before Testing

Before running the re-tests, confirm every fix from Phase 2 is in place. SSH into each VM and verify.

On app-server

ssh app-server
  • Parameterized SQL queries (Module 08): Review main.go — all queries use $1, $2 placeholders, no fmt.Sprintf for SQL
  • bcrypt password hashing (Module 10): Login handler uses bcrypt.CompareHashAndPassword
  • Rate limiting (Module 10): Login handler checks isRateLimited(ip) before processing
  • UFW firewall active: sudo ufw status shows rules, port 8080 only from 192.168.56.13

On db-server

ssh db-server
  • Passwords are hashed (Module 10): psql -U appuser -d customerdb -h localhost -c "SELECT username, left(password, 7) FROM users;" shows $2a$10$ prefixes
  • pg_hba.conf tightened (Module 11): Only 192.168.56.12/32 has access, not the entire /24 subnet
  • UFW firewall active: sudo ufw status shows rules, port 5432 only from 192.168.56.12

On web-server

ssh web-server
  • HTTPS via Tailscale Funnel (Module 09): Funnel is running with HTTPS
  • CORS headers (Module 12): curl -v http://localhost/health 2>&1 | grep Access-Control shows restrictive headers
  • UFW firewall active: sudo ufw status shows rules, only ports 80, 443, and 22

If any check fails, go back to the relevant module and apply the fix before continuing.


2. Re-test: SQL Injection (Module 08)

Get a session cookie first:

curl -c cookies.txt -X POST http://192.168.56.13/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'

Test 1 — OR 1=1

curl -b cookies.txt "http://192.168.56.13/api/customers/1%20OR%201=1"

Expected: Failed to fetch customer or Customer not found. The parameterized query treats the entire string as a literal ID value.

Test 2 — UNION SELECT to extract users

curl -b cookies.txt "http://192.168.56.13/api/customers/0%20UNION%20SELECT%20id,username,password,username,password,created_at,created_at%20FROM%20users%20LIMIT%201"

Expected: Error response. No user data exposed.

Test 3 — Login bypass

curl -v -X POST http://192.168.56.13/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin'\''--","password":"anything"}'

Expected: HTTP 401 Invalid username or password.

Test 4 — CREATE injection

curl -b cookies.txt -X POST http://192.168.56.13/api/customers \
-H "Content-Type: application/json" \
-d '{"name":"hacker","email":"x'\''),(SELECT username FROM users LIMIT 1),(SELECT password FROM users LIMIT 1),'\''gotcha","phone":"","address":""}'

Expected: Customer created with the literal string as the email — SQL is not executed.

Result

  • PASS — All SQL injection attacks return errors or store input as literal data
  • FAIL — One or more attacks still succeeded (go back to Module 08)

3. Re-test: Credential Sniffing (Module 09)

Test 1 — Sniff external HTTPS traffic

Terminal 1 — Capture on web-server:

ssh web-server
sudo tcpdump -i enp0s8 -A port 80 2>/dev/null | grep -E "password|username"

Terminal 2 — Login via Tailscale Funnel HTTPS URL:

curl -X POST https://YOUR-TUNNEL-DOMAIN/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'

Expected: No plaintext credentials in the tcpdump output. Traffic arrives through the encrypted Tailscale Funnel via localhost, not over the Host-Only network interface.

Test 2 — Verify HTTPS enforcement

curl -v https://YOUR-TUNNEL-DOMAIN/ 2>&1 | grep "< HTTP"

Expected: HTTP/2 200 (or HTTP/1.1 200). The connection uses HTTPS.

Result

  • PASS — No plaintext credentials visible in network captures; HTTPS enforced
  • FAIL — Credentials still visible (go back to Module 09)

4. Re-test: Brute Force Login (Module 10)

Test 1 — Run the brute-force script

If you still have the script from Module 10:

./bruteforce.sh

Or recreate it:

cat > wordlist.txt << 'EOF'
password
123456
admin123
letmein
welcome
operator456
EOF

for pw in $(cat wordlist.txt); do
code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST http://192.168.56.13/api/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"operator\",\"password\":\"$pw\"}")
echo "$pw -> HTTP $code"
done

Expected: After 5 failed attempts, all subsequent attempts return HTTP 429 (Too Many Requests).

Test 2 — Verify passwords are hashed

ssh db-server
psql -U appuser -d customerdb -h localhost -c "SELECT username, length(password), left(password, 7) FROM users;"

Expected:

 username | length |  left
----------+--------+---------
admin | 60 | $2a$10$
operator | 60 | $2a$10$

Passwords are 60-character bcrypt hashes, not plaintext.

Result

  • PASS — Brute force blocked after 5 attempts; passwords stored as bcrypt hashes
  • FAIL — Unlimited attempts still allowed or plaintext passwords found (go back to Module 10)

5. Re-test: Direct Database Access (Module 11)

Test 1 — From web-server to PostgreSQL

ssh web-server
psql -U appuser -d customerdb -h 192.168.56.11

Expected: Connection hangs then times out, or returns "connection refused". The UFW firewall on db-server blocks port 5432 from web-server.

Test 2 — From Mac to Go backend directly

curl --connect-timeout 5 http://192.168.56.12:8080/health

Expected: Connection timeout. The UFW firewall on app-server blocks port 8080 from the Mac (only web-server is allowed).

Test 3 — Legitimate path still works

curl http://192.168.56.13/health

Expected: {"status":"healthy","database":"connected"}. The Nginx → Go → PostgreSQL chain works because each hop is explicitly allowed.

Test 4 — Verify firewall rules

ssh db-server  'sudo ufw status' 2>/dev/null
ssh app-server 'sudo ufw status' 2>/dev/null
ssh web-server 'sudo ufw status' 2>/dev/null

Expected: All three VMs show Status: active with the rules configured in Module 11.

Result

  • PASS — Direct database access blocked; direct backend access blocked; app works through Nginx
  • FAIL — Unauthorized connections still succeed (go back to Module 11)

6. Re-test: CORS Exploitation (Module 12)

Test 1 — Cross-origin request from malicious page

Create and serve the malicious page:

mkdir -p /tmp/cors-test
cat > /tmp/cors-test/index.html << 'EOF'
<html><body>
<h1>CORS Re-test</h1>
<div id="result">Testing...</div>
<script>
fetch('http://192.168.56.13/api/customers', { credentials: 'include' })
.then(r => r.json())
.then(d => document.getElementById('result').textContent = 'FAIL: Data stolen: ' + JSON.stringify(d))
.catch(e => document.getElementById('result').textContent = 'PASS: Blocked by CORS: ' + e);
</script>
</body></html>
EOF
cd /tmp/cors-test && python3 -m http.server 9999 &

Open http://localhost:9999 in your browser.

Expected: The page shows "PASS: Blocked by CORS" and the browser console shows a CORS policy error.

Stop the test server:

kill %1 2>/dev/null
rm -rf /tmp/cors-test

Test 2 — Verify CORS headers

curl -v http://192.168.56.13/health 2>&1 | grep -i "access-control-allow-origin"

Expected: Shows your Tailscale Funnel URL, not *.

Result

  • PASS — Cross-origin requests blocked; CORS headers set to specific domain
  • FAIL — Cross-origin requests succeed or headers too permissive (go back to Module 12)

7. Summary — Before and After

VulnerabilityModuleBefore (Phase 1)After (Phase 2)
SQL Injection08fmt.Sprintf builds SQL with user inputParameterized queries ($1, $2) separate code from data
Credential Sniffing09HTTP plaintext — passwords visible on the wireHTTPS via Tailscale Funnel encrypts external traffic
Brute Force Login10Unlimited attempts, plaintext password storagebcrypt hashing + IP-based rate limiting (5 attempts / 5 min)
Direct DB Access11Any VM can reach any port on any other VMUFW firewalls restrict each server to minimum required access
CORS Exploitation12No CORS headers — browser default behaviorExplicit Access-Control-Allow-Origin for the app domain only

8. What You Have Built

Over the course of this training, you built and secured a three-tier web application:

Internet
|
Tailscale Funnel (HTTPS)
|
web-server (192.168.56.13)
Nginx reverse proxy (:80)
UFW: allow 80, 443, SSH
CORS headers configured
|
app-server (192.168.56.12)
Go backend (:8080)
UFW: allow 8080 from web-server only, SSH
Parameterized queries
bcrypt passwords
Rate limiting
|
db-server (192.168.56.11)
PostgreSQL (:5432)
UFW: allow 5432 from app-server only, SSH
pg_hba.conf restricted to app-server

Each layer has its own security controls. Compromising one layer does not automatically give access to the others. This is defense in depth.


9. Reflection — "It works" is not enough

At the end of Phase 1, the application was fully functional. Users could log in, manage customers, and access the app from the internet. Every feature worked correctly.

It was also completely insecure. An attacker could:

  • Steal every password in the database with a single curl command
  • Log in as any user without knowing their password
  • Read every piece of customer data by sniffing the network
  • Brute-force any account in seconds
  • Access the database directly from any machine on the network

"It works" and "it's secure" are different requirements. Security is not a feature you add at the end — it is a constraint that shapes how you build from the start. In this training, you built it insecurely first to understand why each protection matters. In production, you would build it securely from day one.


10. What's Next

This training covered the fundamentals. Here are areas to explore next:

TopicWhat it adds
Docker / ContainersPackage each service in a container for consistent deployment
Infrastructure as CodeDefine VMs, networks, and firewalls as code (Terraform, Ansible)
CI/CD PipelinesAutomate testing and deployment (GitHub Actions, Jenkins)
Monitoring & AlertingTrack application health and detect attacks (Prometheus, Grafana)
Log AggregationCentralize logs from all services (ELK stack, Loki)
Container OrchestrationManage containers at scale (Kubernetes)
Internal TLSEncrypt traffic between all internal services (mutual TLS, service mesh)
Secret ManagementReplace hardcoded credentials with a vault (HashiCorp Vault)

Final Checklist

  • All 5 re-tests passed
  • Understood why each vulnerability existed
  • Understood why each fix works
  • Can explain the before/after for each vulnerability
  • Understands defense in depth

Congratulations — you have completed the Platform Engineer Training.