Several websites and several servers, all behind the one internet address you already have โ the main machine stays hidden, and you keep a way back in if a firewall change locks you out.
What you are actually building
Your provider gives you one address. You want to run more than one thing behind it.
the internet sees ONE address
โ
๐งฑ OPNsense โโโ free firewall. Decides what gets in,
โ and which machine it goes to.
โโโโโโโโโโโโผโโโโโโโโโโโ
๐ LIVE ๐งช DEV ๐ ALT
the real the copy the spare that
site you break takes over
ssh :2201 ssh :2202 ssh :2203 โ one door number per machine
Proxmox โ the box that runs all three as software โ is never visible from the internet. Only the firewall is.
The two traps this saves you from
- Locking yourself out. One wrong firewall rule, made from the internet side, and your own server is gone with no undo. Everyone does it once. Section 8 makes you set up the way back in before you touch a rule.
- SSH will not listen to names. Three websites can share one address because the browser says which site it wants. SSH (your terminal login) cannot say that โ so it gets a door number instead. Section 5 ranks four ways to do it, best to worst.
Every step has a verified backup before it and a written rollback after it.
The 9 sections, and where you are standing in each
โ PLAN โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1 ๐ design rules โ
โ 2 ๐ source-of-truth worksheet โ CTID ยท host ยท IP โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ BACKUP โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3 ๐๏ธ Proxmox shell โ vzdump + zstd -t + sha256 โ
โ ๐งฑ OPNsense UI โ export config, checksum offline โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ADDRESS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4 ๐๏ธ + ๐ฆ inside container โ verify IP at every layer โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PUBLISH โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 5 ๐งฑ OPNsense UI โ aliases โ NAT โ WAN rule โ apply ONCE โ
โ 6 ๐ฆ Nginx container โ vhost, nginx -t before reload โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PROVE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 7 ๐๏ธ private nc + ๐ from OUTSIDE the LAN โ curl, TLS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ RECOVER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 8 ๐ the way back in, decided before you need it โ
โ 9 โ
final proof, 14 lines โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Two different things are both called an alias below: an OPNsense alias (a name for an IP, used in firewall rules) and an SSH alias (a shortcut in ~/.ssh/config). They never touch each other.
Every CTID, IP and hostname is a placeholder. Fill the worksheet in section 2 first.
๐ 1. What we are building
Internet
|
Public IP / firewall
|
OPNsense VM
|
`-- Private LAN / management bridge
|-- Nginx LIVE container
|-- Nginx DEV container
`-- Nginx ALT container
Core design rules:
- Keep the Proxmox management plane private whenever practical.
- Publish services through OPNsense instead of exposing the hypervisor.
- Give every container a documented static private address.
- Keep CTIDs, hostnames, and final IP octets aligned when practical.
- Publish only required ports.
- Back up before making network changes.
- Validate configuration syntax before reloads.
- Keep a console path that does not depend on the network you are changing.
- Never assume that a remembered topology is still correct; verify it live.
๐ 2. Build a source-of-truth worksheet first
ROLE CTID HOSTNAME PRIVATE IP BRIDGE
LIVE <110> <nginx-live> <10.20.30.110/24> <vmbr-lan>
DEV <120> <nginx-dev> <10.20.30.120/24> <vmbr-lan>
ALT <130> <nginx-alt> <10.20.30.130/24> <vmbr-lan>
LAN gateway: <10.20.30.1>
DNS resolver: <10.20.30.1>
Public hostname: <example.net>
DEV hostname: <dev.example.net>
ALT hostname: <alt.example.net>
Do not proceed until Proxmox configuration, guest configuration, OPNsense aliases, NAT rules, DNS, and documentation agree.
๐๏ธ๐งฑ 3. Capture the baseline and back up
Run read-only discovery on the Proxmox host:
hostname
pveversion
pct list
qm list
pvesm status
cat /etc/pve/storage.cfg
ip -br address
ip route
bridge link
df -h
for id in <LIVE_CTID> <DEV_CTID> <ALT_CTID>; do
pct config "$id"
pct listsnapshot "$id"
done
Before changing a container, create a verified backup using your established backup system.
Cold backup example:
vzdump <CTID> --mode stop --storage <BACKUP_STORAGE> --compress zstd
Do not stop at โthe file exists.โ Verify it:
test -s <BACKUP_ARCHIVE>
zstd -t <BACKUP_ARCHIVE>
zstdcat <BACKUP_ARCHIVE> | tar -tf - >/dev/null
md5sum <BACKUP_ARCHIVE> > <BACKUP_ARCHIVE>.md5
sha256sum <BACKUP_ARCHIVE> > <BACKUP_ARCHIVE>.sha256
md5sum -c <BACKUP_ARCHIVE>.md5
sha256sum -c <BACKUP_ARCHIVE>.sha256
Export the OPNsense configuration before changing aliases, NAT, or firewall policy:
System -> Configuration -> Backups
Then checksum the exported file offline:
sha256sum <OPNSENSE-CONFIG-BACKUP.xml>
Do not publish the OPNsense XML. It may contain sensitive network and credential material.
๐๏ธ๐ฆ 4. Verify addressing at every layer
From Proxmox:
pct config <CTID>
pct exec <CTID> -- ip -br address
pct exec <CTID> -- ip route
pct exec <CTID> -- cat /etc/hosts
Search documentation and configuration for stale addresses:
rg -n '<OLD_PRIVATE_IP>|<EXPECTED_PRIVATE_IP>' <DOCUMENTATION_DIRECTORY>
Classify every match. Never perform a blind global replacement.
A failed ping does not prove an address is unused. Check DHCP leases, ARP/neighbour state, firewall aliases, DNS, documentation, and the intended addressing plan.
Before changing a guest network file:
cp -a <NETWORK_CONFIG> <NETWORK_CONFIG>.before-$(date +%Y%m%d-%H%M%S)
Use the Proxmox console during network-address changes. Verify locally before touching NAT:
ip -br address
ip route
ping -c 3 <LAN_GATEWAY>
๐งฑ 5. OPNsense aliases, NAT, and firewall order
Use aliases instead of scattering literal private IPs across rules:
Alias: NGINX_LIVE -> <LIVE_PRIVATE_IP>
Alias: NGINX_DEV -> <DEV_PRIVATE_IP>
Alias: NGINX_ALT -> <ALT_PRIVATE_IP>
For each published service:
- Back up OPNsense.
- Update or create the alias.
- Create the port forward.
- Create the associated WAN pass rule.
- Apply once.
- Test from an actual external network.
- Roll back immediately if management or LIVE service becomes unhealthy.
Typical mappings:
WAN TCP/80 -> selected Nginx TCP/80
WAN TCP/443 -> selected Nginx TCP/443
If several websites share one public IP, normally forward 80/443 to one reverse proxy and route by HTTP Host / TLS SNI there.
SSH is different
SSH is raw TCP. It does not provide HTTP Host routing.
If multiple DNS names resolve to the same public IP, a firewall cannot choose a different TCP/22 backend based on the typed hostname.
Use one of these patterns:
Best: VPN or hardened SSH jump host
Good: separate public IP per SSH endpoint
Acceptable: unique external ports, such as 2201/2202/2203
Avoid: broadly exposed password-enabled root SSH
Client aliases:
Host live-admin
HostName <PUBLIC_IP_OR_NAME>
Port <LIVE_EXTERNAL_SSH_PORT>
User <ADMIN_USER>
Host dev-admin
HostName <PUBLIC_IP_OR_NAME>
Port <DEV_EXTERNAL_SSH_PORT>
User <ADMIN_USER>
Host alt-admin
HostName <PUBLIC_IP_OR_NAME>
Port <ALT_EXTERNAL_SSH_PORT>
User <ADMIN_USER>
Prefer keys, source restrictions, rate limiting, and private/VPN management.
๐ฆ 6. Nginx virtual hosts and safe reloads
Preserve a known-good configuration:
cp -a /etc/nginx /root/nginx-backup-$(date +%Y%m%d-%H%M%S)
nginx -T > /root/nginx-before-$(date +%Y%m%d-%H%M%S).txt
Minimal HTTP virtual host:
server {
listen 80;
listen [::]:80;
server_name <HOSTNAME>;
root /var/www/<SITE>;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Validate before every reload:
nginx -t
systemctl reload nginx
systemctl --no-pager --full status nginx
ss -lntp | grep -E ':(80|443)\b'
Obtain certificates only after DNS and the selected ACME challenge path are working. Never copy private keys into documentation.
๐๏ธ๐ 7. Test private and public paths independently
Private tests:
for ip in <LIVE_PRIVATE_IP> <DEV_PRIVATE_IP> <ALT_PRIVATE_IP>; do
for port in 22 80 443; do
nc -vz -w 5 "$ip" "$port"
done
done
External DNS and service tests:
for host in <LIVE_HOST> <DEV_HOST> <ALT_HOST>; do
echo "=== $host ==="
getent ahostsv4 "$host"
curl -sSIL --max-time 10 "https://$host/"
done
TLS inspection:
openssl s_client \
-connect <HOSTNAME>:443 \
-servername <HOSTNAME> </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
Always test from a network that is genuinely outside the private LAN.
๐ 8. Management-plane recovery
Before risky edge changes, know how you will recover if SSH disappears.
Keep:
- Proxmox console access.
- OPNsense console access.
- A current OPNsense configuration backup.
- A known-good Nginx configuration.
- A written rollback sequence.
- The exact management IP/route needed to get back in.
If a firewall or NAT change unexpectedly breaks access, stop making additional changes. Recover from the console or restore the known-good configuration first.
โ 9. Final proof checklist
Do not call the work complete until you can prove:
[ ] Proxmox management still reachable
[ ] OPNsense healthy
[ ] LIVE private path works
[ ] DEV private path works
[ ] ALT private path works
[ ] Public DNS correct
[ ] Public HTTP/HTTPS correct
[ ] TLS certificate correct
[ ] Nginx configuration passes nginx -t
[ ] Expected listeners present
[ ] Backups verified
[ ] Rollback path documented
[ ] Documentation updated
[ ] Remaining unknowns recorded
BCBC field rule: evidence beats memory
Do not write โworkingโ as a conclusion by itself.
Record:
- what was tested;
- when it was tested;
- what result was observed;
- where the evidence is stored;
- what rollback is available; and
- what remains unknown.
Useful states:
DOCUMENTED present in an approved record
LIVE-VERIFIED tested against the running system with evidence
INFERRED plausible but not yet proven
PROPOSED intended future state
UNKNOWN insufficient evidence
Never recover operational state from memory when it can be recovered from evidence.
!