Introduction
Corner Shop uses an order application to read and create records in a private PostgreSQL database. Its page works, but a Visitor can also reach the database directly. A working application therefore does not prove that the database boundary is safe.
You will compare real connections, restrict forwarding at the prepared Gateway, and preserve both orders and a public status service. Only basic terminal use is assumed. All people and records are synthetic. The three terminal nodes are containers with separate network namespaces sharing one instance kernel; they are not separate VMs.
Follow an Order and Its Network Path
In this step, you will create a real order and compare two paths to its database. An application is the customer-facing program; its database is the record store. Open Order Page, find seed: Green tea, enter practice-1 in Order reference, and click Create tea order. The new row is stored in PostgreSQL, not just displayed in the page.
The network map lists each node's IP address, which identifies a network destination. A port identifies a service at that address. PostgreSQL listens at 172.28.20.10:5432; the separate public status service uses port 8001. App Client (172.28.10.20) needs the database. Visitor (172.28.10.30) needs only public status.
Switch to App Client. The prepared tcp-probe opens a real TCP connection and waits at most three seconds. Its two arguments are the destination address and port:
tcp-probe 172.28.20.10 5432
Expect TCP OPEN. This proves reachability, not permission to read records. psql is PostgreSQL's query client. The following command supplies the training password only to this command. The quoted connection string groups server, port, user, database and a three-second connection timeout. -c runs the quoted SQL; SELECT reads the named columns from orders, and ORDER BY sorts them. The semicolon ends the SQL statement.
PGPASSWORD=training-only psql "host=172.28.20.10 port=5432 user=shopapp dbname=shop connect_timeout=3" -c "SELECT id, item FROM orders ORDER BY id;"
Find practice-1 and Green tea in the result. The deliberately known credential will stay constant so we can isolate the network change.
Switch to Visitor and repeat both commands:
tcp-probe 172.28.20.10 5432
PGPASSWORD=training-only psql "host=172.28.20.10 port=5432 user=shopapp dbname=shop connect_timeout=3" -c "SELECT id, item FROM orders ORDER BY id;"
Both succeed: the unintended client can connect and read the synthetic orders. An invalid password would produce an authentication failure after reaching PostgreSQL; that would leave the network port exposed.
A route tells a node where to send packets. Inspect Visitor's prepared route to the database:
ip route get 172.28.20.10
The important fields are via 172.28.10.254 (Gateway) and src 172.28.10.30 (Visitor). App Client uses the same gateway with a different source. The database's return route points to Gateway's other interface, 172.28.20.254. Neither client has a direct database interface. These routes are prepared infrastructure; leave them unchanged. The order record is this step's checkpoint; you do not submit a report.
Permit Only the Application Database Path
In this step, you will filter packets that cross Gateway. Switch to Gateway. iptables edits the kernel's packet-filter rules. A chain is an ordered list: the first matching decision wins. FORWARD handles traffic passing through this node, while INPUT concerns traffic addressed to the node itself. Setup connects FORWARD to the lab-owned LAB_FORWARD chain; its initial permissive rule allows the exposure.
Inspect the current rules. -n keeps numeric addresses, -v includes packet counters, and -L lists the named chain:
iptables -nvL LAB_FORWARD
Change only LAB_FORWARD in this terminal. Leave host rules, routes, credentials and services unchanged. Rules apply immediately and last for this lab session. We will replace the permissive rule with four decisions. The prepared parent chain denies anything left unmatched.
-F flushes this named chain. This briefly blocks forwarded services while you add rules; the terminal and Order Page interface remain accessible. If you make a typo, start again at this flush and re-enter all four rules below.
iptables -F LAB_FORWARD
Responses must return too. For example, a database reply travels back toward App Client, so its destination is no longer the database port. -A appends a rule, -m conntrack uses connection tracking, and --ctstate ESTABLISHED,RELATED identifies replies or related traffic for already permitted connections. -j ACCEPT permits a matching packet:
iptables -A LAB_FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Now allow new application connections. -s matches the source, -d the destination, -p tcp selects TCP, and --dport matches the destination port. A single address here means that exact node:
iptables -A LAB_FORWARD -s 172.28.10.20 -d 172.28.20.10 -p tcp --dport 5432 -j ACCEPT
Place the general database rejection after the application exception. Without -s, it covers other sources. REJECT --reject-with tcp-reset explicitly refuses a TCP connection instead of silently waiting for a timeout:
iptables -A LAB_FORWARD -d 172.28.20.10 -p tcp --dport 5432 -j REJECT --reject-with tcp-reset
Finally preserve the separate public status port for both clients. Allowing this service does not expose the database port:
iptables -A LAB_FORWARD -d 172.28.20.10 -p tcp --dport 8001 -j ACCEPT
List the result:
iptables -nvL LAB_FORWARD
Read the order: replies, application database access, general database rejection, then public status access. A rejection above the application exception would block the shop too. Packet counters increase only when matching traffic actually crosses Gateway; a rule's presence alone is not proof. Refresh Order Page: App Client should show OPEN, Visitor BLOCKED, public status PUBLIC STATUS OK, and the orders should still load.
Retest Security and the Shop
In this step, you will confirm the connection matrix and complete another real order. Switch to App Client and make a new connection:
tcp-probe 172.28.20.10 5432
Expect TCP OPEN. Repeat the read with the same credential:
PGPASSWORD=training-only psql "host=172.28.20.10 port=5432 user=shopapp dbname=shop connect_timeout=3" -c "SELECT id, item FROM orders ORDER BY id;"
Switch to Visitor:
tcp-probe 172.28.20.10 5432
Expect TCP BLOCKED and a connection-refused explanation. This command intentionally returns a failure status. A silent drop instead yields a timeout; both prevent a new connection, but a timeout alone does not identify the cause. If App Client also fails, return to Gateway and check rule order and reply handling.
curl retrieves a web resource. --max-time 4 bounds the entire request to four seconds; -f treats HTTP errors as failures, -s hides progress and -S still shows errors. From Visitor, check the different public service:
curl --max-time 4 -fsS http://172.28.20.10:8001/status.txt
Expect PUBLIC STATUS OK. Blocking every forwarded packet would fail this requirement even if it closed the database path.
Open Order Page, create practice-2, and confirm both practice orders appear. This exercises the application's real database connection after the repair. Refresh once more: Visitor must remain blocked. Gateway counters can help diagnose mismatches, but a successful order plus the allowed/blocked connection tests provides the actual evidence. Do not stop PostgreSQL or change its password to make a failed visitor query look secure.
Summary
You followed an order into PostgreSQL, compared two routed clients, applied ordered forwarding rules, and tested both security and availability. A password error is different from a blocked TCP connection. Preserve the intended application and unrelated services when closing an exposed database path.



