Know which door
you are opening.
Trace a request to your Ubuntu server, move the demo page inside an SSH tunnel, and close its direct internet HTTP port.
These are sample names, not our infrastructure. Before copying commands, read Customize the examples: paths, keys, IPs and names. Substitute your own authorized account and resources. Existing lab? Retain its original resource names, usernames and state; renamed samples can cause replacement. See the existing-lab warning before applying updated recipes.
Reuse one disposable lab
Complete one of the server guides above, including Ansible configuration and host-fingerprint verification. Use only its code-managed sandbox. You do not need three running servers to complete this lesson.
A route is a road sign. A firewall is the doorman. A listening application is someone inside who can answer. A working website needs all three—not just an open port.
Use Ubuntu/WSL on Windows, as in the server lessons. All terminal commands below run in that environment, except commands explicitly marked “inside the server.”
1. Trace the request through five layers
- Address: use the server's current public IP, not its private subnet IP. An IP can change after stop/start.
- Route: AWS's subnet routes to an internet gateway. Azure uses system routes and the explicit NIC public IP. Google uses the custom VPC's internet route and external IP.
- Firewall: verify the source /32, TCP port and target. Azure may have subnet and NIC NSGs; Google rules apply to tagged targets and can be affected by higher-level policies.
- Guest: an OS firewall or application binding can still block the request. A cloud allow does not override them.
- Application: Nginx must be running and listening on port 80. SSH on 22 is a different service.
sudo ss -lntp
systemctl status nginx --no-pager
curl --max-time 5 -I http://127.0.0.1/A local HTTP 200 response proves the application works locally. It does not prove the external firewall is correct. A timeout can mean several different failures; collect evidence before changing access.
| Address/range | Meaning | Not the same as |
|---|---|---|
| 127.0.0.1 | This computer's loopback interface | Your remote server unless the command runs there |
| Your public IPv4 /32 | One source address | One human—other users may share the same NAT/VPN address |
| 0.0.0.0/0 route | Default route for IPv4 destinations | Authorization to log in |
| 0.0.0.0/0 inbound source | Any IPv4 source | A safe SSH troubleshooting shortcut |
In every new laptop terminal, set LAB_SSH_KEY to your actual private-key file, not its .pub partner. For example, LAB_SSH_KEY="$HOME/.ssh/contoso_aws" works only if you created that AWS key there. Choose your Azure or Google key for those servers. See key paths on Windows, WSL and Linux; never copy a Windows path into WSL unchanged.
2. Put HTTP inside an encrypted SSH tunnel
Instead of opening the front web door, send the page down the locked SSH hallway you already verified.
In a new Ubuntu/WSL terminal, choose your cloud's command, replace the server IP, and unlock your passphrase if asked. The tunnel binds only your laptop's loopback address—not every device on your home network.
ssh -N -o ExitOnForwardFailure=yes \
-L 127.0.0.1:8080:127.0.0.1:80 \
-i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" ubuntu@YOUR-SERVER-IPssh -N -o ExitOnForwardFailure=yes \
-L 127.0.0.1:8080:127.0.0.1:80 \
-i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" contoso@YOUR-SERVER-IPssh -N -o ExitOnForwardFailure=yes \
-L 127.0.0.1:8080:127.0.0.1:80 \
-i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" contoso@YOUR-SERVER-IPThe command stays open without a shell prompt; that is expected. The first loopback address is on your laptop. The second is interpreted from the cloud server. Open http://127.0.0.1:8080/ or use another terminal:
curl --max-time 5 -I http://127.0.0.1:8080/On Windows, localhost forwarding usually lets the Windows browser reach the WSL tunnel. If your WSL networking differs, use curl inside the same Ubuntu environment; do not rebind the tunnel to 0.0.0.0. If port 8080 is occupied, consistently choose another unused local port, such as 8081.
3. Remove only the direct HTTP allowance
Work in the original Terraform folder for the chosen cloud. Keep the current /32 SSH rule intact. Make the change in code so a later apply does not accidentally restore it.
- AWS: delete only the entire
aws_vpc_security_group_ingress_rule "http"resource block. Keep the SSH resource, outbound rule, routes and server. - Azure: remove only the
security_ruleblock namedLearnerHTTPfrom the NSG. KeepLearnerSSHandDenyOtherInbound. - Google: in the learner firewall resource change its allow block to the one below. Keep its source /32 and target tag.
allow {
protocol = "tcp"
ports = ["22"]
}terraform fmt
terraform validate
terraform plan -out=network.tfplan
# Expect only removal of port 80 access, not VM replacement or new services.
terraform apply network.tfplanIf the plan proposes replacing the VM because a newer image was discovered, stop. Review/pin the approved image and regenerate the plan instead of combining changes. Also stop for unexpected billing, account, firewall or deletion changes. Saved plans apply without another confirmation.
4. Compare a blocked path with a working path
# Direct HTTP should no longer succeed in this isolated lab.
curl --max-time 5 -I http://YOUR-SERVER-IP/
# The SSH tunnel should still return HTTP 200.
curl --max-time 5 -I http://127.0.0.1:8080/Test a fresh connection and inspect the actual rules. Stateful cloud firewalls may let previously tracked connections continue after a rule change, and browsers can cache content. A single timeout alone is not proof of correct policy.
If direct access still works, check other attached security groups, NIC/subnet rules, target tags and inherited policies. Do not disable a shared policy to make the result fit this lab. Ask the administrator to help interpret effective rules.
AWS tracked connectionsAzure effective traffic rulesGoogle firewall evaluation
5. A tunnel is not a private subnet
The VM still has a public IP and source-restricted SSH. Removing that IP without another access and outbound path can lock you out and break package downloads.
| Cloud | Private administration option | Requirements to plan first |
|---|---|---|
| AWS | Systems Manager Session Manager | Agent, authorized instance role, user session permissions and service connectivity via approved egress or endpoints. |
| Azure | Azure Bastion or an approved private connection | Supported topology/SKU, permissions, subnet/rules and cost. Do not assume a Bastion deployment is free. |
| Google Cloud | IAP TCP forwarding with approved OS access | Tunnel IAM, SSH/OS Login permissions and the required IAP source rule. It is a different source path from your laptop /32. |
Private servers can still need paid NAT, endpoints or another egress design for updates. NAT is outbound connectivity, not an administrator login service. A private address does not automatically grant identity or protect an insecure application.
Session Manager prerequisitesAzure BastionBastion pricingGoogle IAP setup
These are architecture planning options, not an implemented migration in this lesson. Keep your working lab access until a replacement path is deliberately built and tested.
6. Close the tunnel and finish the lab
Press Ctrl+C in the tunnel terminal. Confirm localhost:8080 no longer serves the cloud page. Closing a tunnel does not stop or delete the VM.
Keep the port-80 removal in your recipe, or deliberately restore that lab-only rule through a reviewed plan if repeating the original demo. Use the chosen server guide's full cleanup when finished: