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.
!