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:
| Outcome | Detail |
|---|---|
| Re-tested | Every attack from Modules 08-12 against the hardened system |
| Confirmed | All five categories of attacks are now blocked |
| Documented | Before/after security posture for each vulnerability |
| Reflected | On 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, $2placeholders, nofmt.Sprintffor 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 statusshows 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/32has access, not the entire/24subnet - UFW firewall active:
sudo ufw statusshows 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-Controlshows restrictive headers - UFW firewall active:
sudo ufw statusshows 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
| Vulnerability | Module | Before (Phase 1) | After (Phase 2) |
|---|---|---|---|
| SQL Injection | 08 | fmt.Sprintf builds SQL with user input | Parameterized queries ($1, $2) separate code from data |
| Credential Sniffing | 09 | HTTP plaintext — passwords visible on the wire | HTTPS via Tailscale Funnel encrypts external traffic |
| Brute Force Login | 10 | Unlimited attempts, plaintext password storage | bcrypt hashing + IP-based rate limiting (5 attempts / 5 min) |
| Direct DB Access | 11 | Any VM can reach any port on any other VM | UFW firewalls restrict each server to minimum required access |
| CORS Exploitation | 12 | No CORS headers — browser default behavior | Explicit 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:
| Topic | What it adds |
|---|---|
| Docker / Containers | Package each service in a container for consistent deployment |
| Infrastructure as Code | Define VMs, networks, and firewalls as code (Terraform, Ansible) |
| CI/CD Pipelines | Automate testing and deployment (GitHub Actions, Jenkins) |
| Monitoring & Alerting | Track application health and detect attacks (Prometheus, Grafana) |
| Log Aggregation | Centralize logs from all services (ELK stack, Loki) |
| Container Orchestration | Manage containers at scale (Kubernetes) |
| Internal TLS | Encrypt traffic between all internal services (mutual TLS, service mesh) |
| Secret Management | Replace 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.