# Edge proxy `edge-proxy-1` — Nginx Proxy Manager (native install) Dedicated reverse proxy for all LogShip ERP instances, replacing the hand-maintained ISPConfig vhosts on web-erp-1 (192.168.12.51). Built 2026-10-03, **not yet live** for production hostnames. | Item | Value | |------|-------| | VM | 116 `edge-proxy-1` on prox2, Debian 12, 2 vCPU / 2 GB / 16 GB, in the 04:30 PBS backup job | | LAN IP | 192.168.12.52 | | Public IP | **5.9.170.82** (OPNsense VIP; NAT 80/443 → .52 added 2026-10-03, pass-mode rules like production's) | | Admin UI | http://192.168.12.52:81 (nftables: private networks only); credentials `/root/npm-admin.txt` on the VM | | First run | v2.16 seeds **no** default admin: the first `POST /api/users` (unauthenticated) creates the admin; done 2026-10-03 (info@logyou.de), afterwards the endpoint is closed (403) | | Software | Nginx Proxy Manager v2.16.0 native: `/app` (backend+frontend), `/data` (sqlite, generated nginx conf, custom certs), `/etc/letsencrypt`, OpenResty from openresty.org apt repo, certbot venv `/opt/certbot` (+ `certbot-dns-ionos`) | | Services | `openresty.service`, `npm.service` | | State to back up | `/data` + `/etc/letsencrypt` (the VM is in PBS anyway) | ## Files here | File | Purpose | |------|---------| | `install-npm-baremetal.sh` | full native install on a fresh Debian 12 VM; `--upgrade` rebuilds `/app` from the latest release and keeps `/data` | | `npm-api.sh` | login + curl wrappers for the NPM REST API (`source` it) | | `add-instance.sh [--cert …] [--letsencrypt ] [--disabled]` | creates/updates the **standard ERP proxy host** (idempotent per domain) | | `advanced-snippet.conf` | host-level nginx extras (body size, header buffers, SSE no-buffering, chat websocket) | | `location-api.conf`, `location-webui.conf` | per-location extras for the iDempiere REST / ZK UI upstreams | | `selfsigned-wildcard.sh` | 10-year self-signed `*.logship.de, *.blut24.com` as custom cert `selfsigned-wildcard` (pre-switch testing) | ## The standard instance host (what every ERP domain gets) | Path | Upstream | Notes | |------|----------|-------| | `/` | `http://IP:3001` | Nuxt; websockets allowed, `X-Forwarded-Proto`, 64k buffers, Force SSL, HTTP/2, HSTS, block exploits | | `/api/v1` | `https://IP:8443/api/v1` | iDempiere REST (self-signed upstream, `proxy_ssl_verify off`) | | `/webui`, `/webui/serverpush` | `https://IP:8443/…` | ZK UI, websocket upgrade, 300 s timeouts | | `/media-api` | `http://IP:1337/api` | Strapi | | `/files-api` | `http://IP:80/` | instance nginx → uploads | | `/capi/webhooks/fulfillment-leads/` | `http://IP:3001` | landing-page lead webhook (ISPConfig had `https://…:3001`, which cannot work) | | `~ ^/api/.*/stream$` | `http://IP:3001` | SSE: `proxy_buffering off`, 1 h read timeout (new, ISPConfig had nothing) | | `/chat/ws` | `http://IP:3001` | websocket, 1 h read timeout | `/custom-api` (PostgREST) is intentionally not exposed, as before. ## Adding an instance (after `clone-logship-instance.sh … --domain `) ```bash # from your Mac (needs curl + jq; NPM_USER/NPM_PASS or run it on the edge VM where /root/npm-admin.txt is read) export NPM_USER=info@logyou.de NPM_PASS='…' reference/edge-proxy/add-instance.sh app.acme.de 192.168.12.NN --letsencrypt info@logyou.de ``` DNS A record `app.acme.de → 5.9.170.82` must exist first for `--letsencrypt` (http-01). Without DNS yet: `--cert selfsigned-wildcard --disabled`, then edit in the UI later. ## Verified 2026-10-03 (public path 94.x → 5.9.170.82 → NAT → .52 → template VM .67) `/signin` 200 over HTTP/2 with a valid Let's Encrypt cert, http → 301 https, `/api/v1` → iDempiere 8443 200 (JSON), `/webui/` 200, `/webui/serverpush` 404 (no ZK session = routed), `/media-api` 403 (Strapi auth = routed), `/files-api` 403 (autoindex off = routed), lead webhook 404 (routed, no TLS error any more), `/chat/ws` 426 (Nuxt ws handler = routed), HSTS + X-Served-By present, 12 KB cookie header accepted, `/custom-api` 404. Reboot: all services + nftables back, public path 200. Browser login/dashboard test: do it yourself at https://edge.logship.de while VM 115 runs (it must be stopped again afterwards — `clone-logship-instance.sh` refuses to clone a running template). ## Hosts configured 2026-10-03 | Domain | Upstream | Cert | State | |--------|----------|------|-------| | edge.logship.de | 192.168.12.67 (golden template VM 115, boot it for demos) | Let's Encrypt | public test host | | app.logship.de | 192.168.12.72 | selfsigned-wildcard | ready, **DNS still → .81 (ISPConfig)** | | app.blut24.com | 192.168.12.80 | selfsigned-wildcard | ready, DNS still → .81 | | dev-app.logship.de | 192.168.12.71 | selfsigned-wildcard | ready, DNS still → .81 | | dev-blut24.logship.de | 192.168.12.75 | selfsigned-wildcard | ready, DNS still → .81 | ## Switch-over runbook (per hostname, 10 min, reversible) 1. Lower the TTL of the A record at IONOS a day before (300 s). 2. In NPM, on the host: SSL tab → "Request a new SSL certificate" is **not possible before DNS moves** (http-01). So: move the A record `.81 → .82`, wait for propagation (`dig +short`), then request the Let's Encrypt cert on the host (replaces the self-signed one), Force SSL + HTTP/2 + HSTS on. During the few minutes in between, visitors see the self-signed cert → do it off-hours or accept it. Alternative without the gap: IONOS DNS API key → DNS-01 certificate first, then move DNS. 3. Verify: `/signin` 200, login, dashboard SSE, `/api/v1` via token, `/webui/`, `/media-api`, `/files-api`. 4. Only then disable the ISPConfig site for that hostname (keeps .81 as fallback until all moved). 5. Rollback: A record back to .81 (ISPConfig vhost + cert still there). When every hostname has moved, the OPNsense rules for .81 → .51 ports 80/443 can be deleted and the VIP .81 reused. ## Certificates - Now: `selfsigned-wildcard` (custom cert) for the not-yet-live hosts; Let's Encrypt http-01 for `edge.logship.de` (DNS and NAT in place). - Later options: per-host Let's Encrypt at switch time (http-01, zero config), or wildcard `*.logship.de` via DNS-01 with the IONOS plugin (installed in the certbot venv): NPM UI → SSL Certificates → Add → "Use a DNS Challenge" → IONOS, credentials `dns_ionos_prefix=…` / `dns_ionos_secret=…` from developer.hosting.ionos.com. - Renewals are automatic (NPM's internal scheduler, certbot venv). ## Operations - Upgrade NPM: `NPM_RELEASE=v2.x.y bash install-npm-baremetal.sh --upgrade` (rebuilds `/app`, keeps `/data`); OpenResty/Node via apt. Check the upstream release notes for DB migrations (automatic on start). - Logs: `/data/logs/*` (per host), `journalctl -u npm`, `journalctl -u openresty`. - Test config: `nginx -t`; generated host files: `/data/nginx/proxy_host/*.conf`. - Admin UI reachability: nftables allows tcp/81 only from RFC1918 (`/etc/nftables.conf`). - Monitoring: not yet registered in Check_MK (todo: `cmk-agent-ctl register` like the other VMs). ## How it was built (2026-10-03) prox2: `qm clone 9000 116 --name edge-proxy-1 --full --storage Disk1-NVMe`, ipconfig .52, 2 vCPU/2 GB, resize 16 G, added to backup job `backup-0e79c346-9785`; guest agent installed in the guest. OPNsense (root@192.168.12.1, config.xml edited by script, backup `/conf/backup/config-20261003-185859-pre-edge-proxy.xml`): two NAT rules `wan tcp 5.9.170.82:80|443 → 192.168.12.52`, pass mode, VIP .82 description updated. Then `install-npm-baremetal.sh`, first admin via `POST /api/users`, `selfsigned-wildcard.sh`, `add-instance.sh` ×5 (edge.logship.de with `--letsencrypt`, cert issued via http-01 at first try). Gotchas hit: NPM emits `proxy_http_version 1.1` + Upgrade headers itself for custom locations when websockets are on — do not repeat them in a location's advanced config (duplicate directive = nginx test fails = every later save returns 500 until the stale generated file is fixed by hand); `iif lo accept` is needed in nftables or the backend cannot reach its own UI port; on update the script keeps the host's certificate unless `--cert`/`--letsencrypt` is given.