The IONOS netbird VPS held two credentials in plaintext that were the last
holdouts from the homelab's sops+age secrets pattern:
- /root/turn-pass.txt (coturn long-term-credential password)
- PKCEAuthorizationFlow.ProviderConfig.ClientSecret inline in
/opt/management.json (Authentik OIDC client secret)
This commit moves both into sops-encrypted YAML in the repo and adds a render
command that recreates the VPS config files from templates + decrypted secrets:
* secrets/turn-shared-secret.yaml — encrypted `password: <coturn pwd>`
* secrets/netbird-authentik-oidc.yaml — encrypted `client_secret: <...>`
Both recipients = hubris + apps + republic-laptop (same 3 as hello.yaml).
* vps/turnserver.conf.tmpl + vps/management.json.tmpl — templates with
{{TURN_PASSWORD}} + {{AUTHENTIK_CLIENT_SECRET}} placeholders.
* bin/homelab new subcommand `render-vps-configs`:
- Decrypts both secrets locally (works on any recipient).
- Substitutes placeholders into templates.
- Diffs against current VPS state via ssh, prompts, applies atomically
(write `.new` then mv), restarts coturn + netbird-mgmt.
- --dry-run + -y flags. Hops through hubris when not running on hubris
itself, since VPS sshd is locked to hubris's pubkey.
* inventory.yaml adds the VPS as `kind: external` (new kind; reserved for
ssh-managed hosts that aren't homelab clients themselves — no age key,
no /opt/homelab-context). hosts/netbird-vps.yaml regenerated.
* SHARED_SECRETS list includes both new secrets so re-keys on enrollment
changes pick them up automatically.
After this lands + the 5-min sync propagates to hubris, run from hubris PVE
shell (or any client; hubris just skips the extra ssh hop):
homelab render-vps-configs --dry-run # see plan, no changes
homelab render-vps-configs -y # apply + restart services
Once verified working, the plaintext `/root/turn-pass.txt` should be deleted
on the VPS (the rendered /etc/turnserver.conf no longer needs it as a
reference).
6.1 KiB
VPS hardening — 82.165.190.79 / 100.122.165.149
IONOS VPS that runs the Netbird control plane and the public ingress traefik. Hardened 2026-04-23 from its stock-Plesk state.
At a glance
- Hostname:
inspiring-ramanujan.82-165-190-79.plesk.page - OS: Debian 13
- Mesh: netbird
100.122.165.149(peer of the lab mesh; routes192.168.8.0/24via hubris). - Public:
82.165.190.79(ens6). - Public DNS: IONOS wildcard
*.hubris.network → 82.165.190.79. - Docker stack at
/opt/docker-compose.yml:traefik(TLS/ACME) +dashboard+mgmt+signal+relay+proxy— netbird-mgmt 0.71.3 vanilla deploy since 2026-05-21 (see mesh.md changelog). - Host services (outside docker):
coturn(TURN-TCP on :3478, long-term creds rendered into/etc/turnserver.confbyhomelab render-vps-configsfrom sops-encryptedsecrets/turn-shared-secret.yaml). - Config rendering:
/etc/turnserver.conf+/opt/management.jsonare generated from templates invps/*.tmplon this repo byhomelab render-vps-configs. Secret placeholders ({{TURN_PASSWORD}},{{AUTHENTIK_CLIENT_SECRET}}) are substituted from sops-encrypted secrets decrypted on hubris and pushed over ssh. Do not hand-edit those two files on the VPS — the next render will overwrite them.
SSH
- Key-only (
PasswordAuthentication no,PermitRootLogin prohibit-password) via drop-in at/etc/ssh/sshd_config.d/10-hubris-hardening.conf. Original config backed up at/etc/ssh/sshd_config.bak.<ts>. - Mesh-only: public
:22is dropped by the nftables firewall. SSH reaches the VPS only overwt0.ListenAddressitself is still0.0.0.0— gating is firewall-layer. - Authorized root keys: PVE (
root@hubris), Mac Mini (d.toro.v@pm.me). Add a new device withssh-copy-id root@100.122.165.149from a mesh peer before disabling its access paths.
Firewall — nftables (inet hubris-fw)
Config at /etc/nftables.conf, service enabled.
- Public iface
ens6. Wireguard ifacewt0. - INPUT on
ens6allow-list: DHCP (67→68), rate-limited ICMP/ICMPv6, TCP 3478 (coturn TURN-TCP, added 2026-05-21). Everything else drops. wt0fully accepted in INPUT.loaccepted.- IONOS upstream firewall also gates inbound traffic before it reaches
ens6. Open ports today: TCP 80/443 (traefik), UDP 51820 (netbird-proxy), TCP 3478 (coturn, added 2026-05-21). UDP 3478 is dropped by IONOS upstream regardless of local nftables. See mesh.md ICE/STUN for the STUN/TURN port matrix. - FORWARD chain at priority
filter-10(runs before Docker's FORWARD) hosts the fail2ban ban enforcement — see below. - Set
banned4(typedipv4_addr, flagtimeout) holds fail2ban's drops. - Coexists with Docker's
ip nat/ip filtertables (iptables-nft compat). Do NOTflush rulesetin this config — it'll wipe Docker's state too.
fail2ban
- Jail
traefik-4xxtails/var/log/traefik/access.log(bind-mounted from container). Filter at/etc/fail2ban/filter.d/traefik-4xx.confmatches 401/403/404/429 fromblog-public@fileorartifacto-public@filerouters only — netbird-grpc traffic isn't considered. - Tunables:
findtime=600, maxretry=30, bantime=3600. - Action at
/etc/fail2ban/action.d/nft-hubris.confadds/removes elements frominet hubris-fw banned4with per-element timeout.
CRITICAL invariant — wireguard / fail2ban
Bans must never affect wt0 or wireguard UDP. The FORWARD chain explicitly accepts the following before the ban check:
udp 51820(wireguard)udp 3478(STUN)ct state established,related
The INPUT ban rule is scoped to iifname "ens6".
Violating this takes the mesh down for every home device (they share one public IP) and the only recovery is IONOS console → nft flush set inet hubris-fw banned4.
Traefik access log
- Written to
/var/log/traefik/access.logon the host via a bind mount added to/opt/docker-compose.yml(traefik volumes include/var/log/traefik:/logs) plus--accesslog.filepath=/logs/access.log. - CLF format. Real client IP arrives correctly because docker userland-proxy is off — see public ingress.
Plesk / mail / FTP / Dr.Web
Stopped and disabled (not uninstalled). All of:
dovecot, dovecot.socket, postfix, postfix@-, pc-remote, xinetd, plesk-task-manager, plesk-web-socket, sw-cp-server, sw-engine, plesk-repaird, plesk-repaird.socket, drwebd.
psa.service is masked (was a one-shot boot bootstrap). /etc/cron.d/plesk-backup-manager-task renamed to .disabled.
Reverse: systemctl unmask psa; systemctl enable --now <svc>.
Auto-patching
unattended-upgradesenabled (stock).- Drop-in at
/etc/apt/apt.conf.d/52hubris-reboot.confsets auto-reboot at 04:00 UTC when/var/run/reboot-requiredis set. - Runs inside the stock
apt-daily-upgrade.timer.
Recovery paths
Ordered by preference:
- SSH via mesh — primary. Any mesh peer with an authorized key.
- IONOS web console (my.ionos.com → VPS → Console) — uses the system password, not SSH keys. Bypasses any firewall misconfig.
- IONOS rescue mode — boot rescue, mount rootfs, edit
/etc/nftables.confor/etc/ssh/sshd_config.d/10-hubris-hardening.confto a known-good state, reboot.
Related
- Public ingress (VPS traefik)
- Mesh migration — VPS as a mesh peer
- SSH access
Changelog
2026-05-21 — netbird stack migrated combined → vanilla; coturn added
Replaced the netbirdio/netbird-server combined image with the canonical mgmt + signal + relay + dashboard containers (0.71.3). Added host-side coturn for external TURN, with nftables rule iifname "ens6" tcp dport 3478 accept and an IONOS upstream firewall exception. Authentik on LXC 124 now provides OIDC for the netbird dashboard. Full context in mesh.md changelog.
2026-04-28 — wiki entry created
Initial documentation.
2026-04-23 — hardened
nftables firewall, mesh-only SSH, fail2ban traefik jail, Plesk disabled, auto-reboot 04:00 UTC, wireguard/fail2ban invariant established.