Files
oikos/knowledge/wiki/infrastructure/vps-hardening.md
dtoro 8a6422bd7d docs: move narrative wiki under knowledge/wiki/ (phase 3)
Problem: node and cross-cutting narratives lived at the repo root
(containers/, vms/, infrastructure/, host .md files), interleaved with the
machine-readable substrate.

Change:
- Move containers/ -> knowledge/wiki/containers/, vms/ -> knowledge/wiki/vms/,
  infrastructure/ -> knowledge/wiki/infrastructure/, hosts/{hubris,strong}.md ->
  knowledge/wiki/hosts/, infrastructure/references/ -> knowledge/sources/references/,
  GLOSSARY.md -> knowledge/GLOSSARY.md.
- Add knowledge/{index.md,log.md,sources/index.md} scaffolding.
- Rewrite all relative links repo-wide via a path-resolving mapper (inbound +
  outbound + between-moved-files), including .hermes/, runbooks, operations,
  investigations, plans, README, AGENTS.
- Repoint inventory.yaml doc_page fields and regenerate hosts/*.yaml (which
  embed doc_page); update oikos/gen-topology.py output path, candidate doc
  paths, and footer links; update code-comment doc paths.

Substrate untouched in place: inventory.yaml, hosts/*.yaml (regenerated,
idempotent), oikos/ code, mcp/, secrets/, bin/.

Verification:
- Logical broken-link set identical to pre-move baseline (net 128 -> 127; the
  topology regen fixed one, introduced none). Remaining are pre-existing refs
  to destroyed/archived nodes, out of scope for this move.
- gen-topology.py --check exit 0 (in sync); cards carry knowledge/wiki/ doc paths.
- build_host_files.py idempotent; all inventory doc_page targets resolve.
- MCP contract verified: get_page/search_docs/get_changelog resolve moved pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:35:23 +02:00

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; routes 192.168.8.0/24 via 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.conf by homelab render-vps-configs from sops-encrypted secrets/turn-shared-secret.yaml).
  • Config rendering: /etc/turnserver.conf + /opt/management.json are generated from templates in vps/*.tmpl on this repo by homelab 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 :22 is dropped by the nftables firewall. SSH reaches the VPS only over wt0. ListenAddress itself is still 0.0.0.0 — gating is firewall-layer.
  • Authorized root keys: PVE (root@hubris), Mac Mini (d.toro.v@pm.me). Add a new device with ssh-copy-id root@100.122.165.149 from a mesh peer before disabling its access paths.

Firewall — nftables (inet hubris-fw)

Config at /etc/nftables.conf, service enabled.

  • Public iface ens6. Wireguard iface wt0.
  • INPUT on ens6 allow-list: DHCP (67→68), rate-limited ICMP/ICMPv6, TCP 3478 (coturn TURN-TCP, added 2026-05-21). Everything else drops.
  • wt0 fully accepted in INPUT. lo accepted.
  • 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 (typed ipv4_addr, flag timeout) holds fail2ban's drops.
  • Coexists with Docker's ip nat / ip filter tables (iptables-nft compat). Do NOT flush ruleset in this config — it'll wipe Docker's state too.

fail2ban

  • Jail traefik-4xx tails /var/log/traefik/access.log (bind-mounted from container). Filter at /etc/fail2ban/filter.d/traefik-4xx.conf matches 401/403/404/429 from blog-public@file or artifacto-public@file routers only — netbird-grpc traffic isn't considered.
  • Tunables: findtime=600, maxretry=30, bantime=3600.
  • Action at /etc/fail2ban/action.d/nft-hubris.conf adds/removes elements from inet hubris-fw banned4 with 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.log on 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-upgrades enabled (stock).
  • Drop-in at /etc/apt/apt.conf.d/52hubris-reboot.conf sets auto-reboot at 04:00 UTC when /var/run/reboot-required is set.
  • Runs inside the stock apt-daily-upgrade.timer.

Recovery paths

Ordered by preference:

  1. SSH via mesh — primary. Any mesh peer with an authorized key.
  2. IONOS web console (my.ionos.com → VPS → Console) — uses the system password, not SSH keys. Bypasses any firewall misconfig.
  3. IONOS rescue mode — boot rescue, mount rootfs, edit /etc/nftables.conf or /etc/ssh/sshd_config.d/10-hubris-hardening.conf to a known-good state, reboot.

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.