# 🚪 Three Sites, Three SSH Doors, One Public IP — Without Exposing Proxmox

**URL:** <https://onehack.st/t/three-sites-three-ssh-doors-one-public-ip-without-exposing-proxmox/325240>\
**Category:** Tutorials & Methods\
**Tags:** networking, hosting, self-hosted-tools\
**Created:** [September 2, 2026, 8:53am UTC](https://onehack.st/t/three-sites-three-ssh-doors-one-public-ip-without-exposing-proxmox/325240 "2026-09-02T08:53:07Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![BCBC](https://onehack.st/user_avatar/onehack.st/bcbc/32/179606_2.png) [@BCBC](https://onehack.st/u/BCBC)\
**Post date:** [September 2, 2026, 8:53am UTC](https://onehack.st/t/three-sites-three-ssh-doors-one-public-ip-without-exposing-proxmox/325240/1 "2026-09-02T08:53:07Z")

</div>

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

```auto
   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

```auto
 ┌ 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**
>
> ```plaintext
> 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**
>
> ```plaintext
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> vzdump <CTID> --mode stop --storage <BACKUP_STORAGE> --compress zstd
> 
> ```
> 
> Do not stop at “the file exists.” Verify it:
> 
> ```bash
> 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:
> 
> ```plaintext
> System -> Configuration -> Backups
> 
> ```
> 
> Then checksum the exported file offline:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```plaintext
> Alias: NGINX_LIVE -> <LIVE_PRIVATE_IP>
> Alias: NGINX_DEV -> <DEV_PRIVATE_IP>
> Alias: NGINX_ALT -> <ALT_PRIVATE_IP>
> 
> ```
> 
> For each published service:
> 
> 1. Back up OPNsense.
> 2. Update or create the alias.
> 3. Create the port forward.
> 4. Create the associated WAN pass rule.
> 5. Apply once.
> 6. Test from an actual external network.
> 7. Roll back immediately if management or LIVE service becomes unhealthy.
> 
> Typical mappings:
> 
> ```plaintext
> 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:
> 
> ```plaintext
> 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:
> 
> ```sshconfig
> 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:
> 
> ```bash
> 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:
> 
> ```nginx
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```bash
> 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:
> 
> ```plaintext
> [] 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:

```plaintext
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.**

---

<div class="post-metadata">

**Author:** ![chovy](https://onehack.st/user_avatar/onehack.st/chovy/32/177730_2.png) [@chovy](https://onehack.st/u/chovy)\
**Post date:** [September 9, 2026, 6:20pm UTC](https://onehack.st/t/three-sites-three-ssh-doors-one-public-ip-without-exposing-proxmox/325240/2 "2026-09-09T18:20:40Z")

</div>

i still haven’t used proxmox yet. what does it do??
