Skip to main content

Module 11 — Firewall Rules (Attack / Understand / Fix)

Objective

Demonstrate that any VM can directly access PostgreSQL and the Go backend, understand why network segmentation matters, then configure UFW firewall rules on all three VMs.

By the end of this module you will have:

OutcomeDetail
ExploitedConnected directly to PostgreSQL from web-server, bypassing the app
UnderstoodWhy unrestricted network access between VMs is dangerous
FixedUFW firewall rules on all 3 VMs restricting port access
Verifiedweb-server can no longer reach PostgreSQL; app still works end-to-end

Prerequisites

  • Module 10 complete (brute force protection added)
  • App running and accessible via Nginx at http://192.168.56.13
  • SSH access to all three VMs: ssh db-server, ssh app-server, ssh web-server

1. Introduction — The network is flat

You have three VMs on the same network (192.168.56.0/24). Right now, every VM can reach every port on every other VM. There are no firewalls, no access controls, and no segmentation.

This means if an attacker compromises one VM — say, the web-server through an Nginx vulnerability — they have direct access to:

  • The Go backend on app-server (port 8080)
  • The PostgreSQL database on db-server (port 5432)
  • SSH on all machines (port 22)

In production, the web-server should only talk to the app-server, and the app-server should only talk to the database. The web-server should never touch the database directly.


2. CTF Challenge

Challenge: From web-server, connect directly to the PostgreSQL database on db-server. You know the database credentials from the Go source code. Can you read, modify, and delete data without going through the application? You have 20 minutes.

Rules:

  • SSH to web-server only
  • Use only tools available on web-server (or that you install)
  • Your goal: read all customer data and all user credentials directly from the database

If you get stuck, reveal the hints below one at a time:

Hint 1

You need the PostgreSQL client. Install it with sudo apt install -y postgresql-client.

Hint 2

The database connection string is in the Go source code. Look for the host, port, username, password, and database name.

Hint 3

psql -U appuser -d customerdb -h 192.168.56.11

Once you have tried the challenge (or after 20 minutes), continue to the guided attack below.


3. Guided Attack — Direct database access from web-server

Step 1 — Install PostgreSQL client on web-server

ssh web-server
sudo apt install -y postgresql-client

Step 2 — Connect to the database directly

The database credentials are hardcoded in the Go source code (main.go). Use them to connect from web-server:

psql -U appuser -d customerdb -h 192.168.56.11

Enter the password when prompted: apppassword123

You are now connected directly to PostgreSQL from a machine that should have no business talking to the database.

Step 3 — Read all data

-- Dump all customers
SELECT * FROM customers;

-- Dump all users (including password hashes)
SELECT * FROM users;

You can see every customer record and every user account. Even though passwords are now bcrypt hashes (from Module 10), an attacker with database access can:

  • Read all customer PII
  • Attempt offline hash cracking
  • Modify data without any audit trail

Step 4 — Modify data directly

-- Create a backdoor admin account
INSERT INTO users (username, password) VALUES ('backdoor', '$2a$10$InvalidHashButItProvesTHePoint');

-- Delete all customer records
DELETE FROM customers;

-- Or worse — drop the entire table
-- DROP TABLE customers; (don't actually run this)

Step 5 — Exit and reflect

\q

You just demonstrated that the web-server — which should only serve HTTP traffic — has full, unrestricted access to the database. A single compromised Nginx module, a misconfigured CGI script, or a reverse shell on the web-server gives an attacker everything.


4. Bonus Attack — Direct backend access

The web-server is supposed to proxy requests through Nginx. But you can also call the Go backend directly:

# From web-server, call the Go backend directly on port 8080
curl http://192.168.56.12:8080/health

And from your Mac, you can bypass Nginx entirely:

# From Mac, access the Go backend directly (skipping Nginx)
curl http://192.168.56.12:8080/health

This means any security controls you add to Nginx (rate limiting, WAF rules, CORS headers) can be bypassed by going straight to the backend.


5. Why This Works

No firewall = no boundaries

SourceDestinationPortShould be allowed?Currently allowed?
web-serverdb-server5432NoYes
web-serverapp-server8080YesYes
Mac (host)db-server5432NoYes
Mac (host)app-server8080NoYes
app-serverdb-server5432YesYes
Any VMAny VM22Yes (SSH)Yes

Without firewalls, PostgreSQL's pg_hba.conf is the only access control — and it allows the entire 192.168.56.0/24 subnet. Any machine on the network can connect.

The principle of least privilege

Each server should only be able to reach the services it needs:

  • web-server needs: Nginx (port 80/443), outbound to app-server (port 8080), SSH
  • app-server needs: Go backend (port 8080 from web-server only), outbound to db-server (port 5432), SSH
  • db-server needs: PostgreSQL (port 5432 from app-server only), SSH

Everything else should be denied.


6. The Fix — UFW firewall rules

UFW (Uncomplicated Firewall) is a frontend for iptables that makes it easy to manage firewall rules on Ubuntu.

Fix db-server — Only app-server can reach PostgreSQL

ssh db-server
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow PostgreSQL ONLY from app-server
sudo ufw allow from 192.168.56.12 to any port 5432 comment 'app-server -> PostgreSQL'

# Allow SSH from the Host-Only network (so you can still manage the VMs)
sudo ufw allow from 192.168.56.0/24 to any port 22 comment 'SSH from internal network'

# Enable the firewall
sudo ufw enable

When prompted "Command may disrupt existing ssh connections. Proceed with operation?" type y.

Verify:

sudo ufw status verbose

Expected output:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To Action From
-- ------ ----
5432 ALLOW IN 192.168.56.12 # app-server -> PostgreSQL
22 ALLOW IN 192.168.56.0/24 # SSH from internal network

Fix app-server — Only web-server can reach the Go backend

ssh app-server
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow Go backend ONLY from web-server
sudo ufw allow from 192.168.56.13 to any port 8080 comment 'web-server -> Go backend'

# Allow SSH from the Host-Only network
sudo ufw allow from 192.168.56.0/24 to any port 22 comment 'SSH from internal network'

sudo ufw enable

Verify:

sudo ufw status verbose

Fix web-server — Accept HTTP/HTTPS from anywhere, SSH from internal

ssh web-server
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow HTTP and HTTPS from anywhere (for Nginx and Tailscale Funnel)
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Allow SSH from the Host-Only network
sudo ufw allow from 192.168.56.0/24 to any port 22 comment 'SSH from internal network'

sudo ufw enable

Verify:

sudo ufw status verbose

Tighten pg_hba.conf — Restrict to app-server only

The firewall blocks network connections, but you should also restrict PostgreSQL's own access control as defense in depth.

ssh db-server
sudo nano /etc/postgresql/16/main/pg_hba.conf

Find the line you added in Module 02:

host    all    all    192.168.56.0/24    md5

Replace it with:

host    all    all    192.168.56.12/32    md5

This restricts PostgreSQL to accept connections only from app-server's exact IP address.

Restart PostgreSQL:

sudo systemctl restart postgresql

7. Verify — Re-run the attacks

Verify direct database access from web-server is blocked

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

Expected: The connection hangs for a few seconds, then fails with a timeout or "connection refused" error. The firewall on db-server drops the packet because it did not come from 192.168.56.12.

Verify direct backend access from Mac is blocked

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

Expected: Connection timeout. The firewall on app-server only allows port 8080 from web-server (192.168.56.13).

Verify the app still works end-to-end

# Access through Nginx (the correct path)
curl http://192.168.56.13/health

Expected: {"status":"healthy","database":"connected"}. The full chain works:

Mac -> web-server:80 (Nginx) -> app-server:8080 (Go) -> db-server:5432 (PostgreSQL)

Each hop is allowed by the firewall rules. Direct shortcuts are blocked.

Verify from app-server to db-server still works

ssh app-server
psql -U appuser -d customerdb -h 192.168.56.11 -c "SELECT 1;"

Expected: Returns 1. app-server is explicitly allowed to reach PostgreSQL.

Check firewall status on all VMs

ssh db-server  'sudo ufw status numbered'
ssh app-server 'sudo ufw status numbered'
ssh web-server 'sudo ufw status numbered'

Troubleshooting

ProblemSolution
Locked out of SSHThe UFW rules allow SSH from 192.168.56.0/24. If locked out, access the VM console directly through VirtualBox and run sudo ufw disable.
App returns 502 Bad GatewayThe firewall on app-server may be blocking Nginx. Verify: sudo ufw status on app-server shows port 8080 allowed from 192.168.56.13.
Database connection fails from app-serverCheck both UFW on db-server (sudo ufw status) and pg_hba.conf (should have 192.168.56.12/32). Restart PostgreSQL after pg_hba.conf changes.
ufw: command not foundInstall it: sudo apt install -y ufw
UFW shows "inactive" after rebootEnable persistence: sudo systemctl enable ufw

Summary

What you didWhat you learned
Connected to PostgreSQL from web-serverWithout firewalls, every VM can reach every service
Read/modified data bypassing the appDirect database access has no application-level security
Called Go backend directly, skipping NginxNginx security controls are useless if the backend is directly accessible
Configured UFW on all 3 VMsEach server now only accepts traffic from authorized sources
Tightened pg_hba.confDefense in depth — multiple layers of access control

The one rule to remember: Every server should accept only the traffic it needs, from only the sources that should send it. Default deny, explicit allow.