diff --git a/ansible/roles/netbird-proxy/defaults/main.yml b/ansible/roles/netbird-proxy/defaults/main.yml index 6a431bfd..5b38c96e 100644 --- a/ansible/roles/netbird-proxy/defaults/main.yml +++ b/ansible/roles/netbird-proxy/defaults/main.yml @@ -17,3 +17,8 @@ netbird_proxy_domain: "proxy.renner.uno" # Reuse VPS-1's proxy token — a shared NB_PROXY_TOKEN across members with the # same NB_PROXY_DOMAIN is a valid HA cluster, so no separate token is needed. netbird_proxy_token: "{{ vault_netbird_proxy_token }}" + +# Relay auth secret — MUST be identical across every relay instance and the +# management server's Relay.Secret (same vault var as the netbird-server +# role); credentials minted by management only validate against it. +netbird_relay_secret: "{{ vault_netbird_relay_secret }}" diff --git a/ansible/roles/netbird-proxy/handlers/main.yml b/ansible/roles/netbird-proxy/handlers/main.yml index 536b21db..cd5546b0 100644 --- a/ansible/roles/netbird-proxy/handlers/main.yml +++ b/ansible/roles/netbird-proxy/handlers/main.yml @@ -10,3 +10,8 @@ ansible.builtin.command: cmd: docker compose up -d --force-recreate proxy chdir: /opt/netbird-proxy + +- name: Restart netbird relay + ansible.builtin.command: + cmd: docker compose up -d --force-recreate relay + chdir: /opt/netbird-proxy diff --git a/ansible/roles/netbird-proxy/tasks/main.yml b/ansible/roles/netbird-proxy/tasks/main.yml index e4bf59bc..fcc00d1f 100644 --- a/ansible/roles/netbird-proxy/tasks/main.yml +++ b/ansible/roles/netbird-proxy/tasks/main.yml @@ -63,6 +63,13 @@ mode: "0600" notify: Restart netbird proxy +- name: Deploy relay.env (contains the shared relay auth secret) + ansible.builtin.template: + src: relay.env.j2 + dest: /opt/netbird-proxy/relay.env + mode: "0600" + notify: Restart netbird relay + - name: Pull reverse-proxy image ansible.builtin.command: cmd: docker compose pull diff --git a/ansible/roles/netbird-proxy/templates/docker-compose.yml.j2 b/ansible/roles/netbird-proxy/templates/docker-compose.yml.j2 index 53adc6f0..c3a95083 100644 --- a/ansible/roles/netbird-proxy/templates/docker-compose.yml.j2 +++ b/ansible/roles/netbird-proxy/templates/docker-compose.yml.j2 @@ -38,6 +38,34 @@ services: max-size: "500m" max-file: "2" + # Relay cluster member — additional NetBird relay (+ embedded STUN) so + # relayed connections and new-P2P candidate discovery survive a + # controller-VPS (Hetzner) outage. Registered in the management server's + # Relay.Addresses/Stuns (netbird-server role, management.json). Host + # network like the proxy: the ws listener (:33080) and STUN (:3478/udp) + # must sit on the host's un-NATed public addresses — STUN behind NAT + # reports wrong reflexive candidates. Both ports are >1024, so the + # ip_unprivileged_port_start sysctl is not involved. + # + # Deliberately NOT covered by the netbird-proxy-watchdog (which probes the + # proxy's public URL): this relay is a fallback with client-side failover + # to the other relays, so a wedged instance degrades redundancy rather + # than availability; restart: unless-stopped catches crashes. If active + # relay monitoring is wanted later, the binary serves + # http://localhost:9000/health (verified on 0.73.2). + relay: + image: netbirdio/relay:{{ netbird_version }} + container_name: netbird-relay + restart: unless-stopped + network_mode: host + env_file: + - ./relay.env + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "2" + # netbird-proxy listens on :443 only (NB_PROXY_ADDRESS=:443) and has no HTTP # listener, so proxy-only hosts leave port 80 unanswered. This did not show # while proxy.renner.uno still resolved to the controller host, because the diff --git a/ansible/roles/netbird-proxy/templates/relay.env.j2 b/ansible/roles/netbird-proxy/templates/relay.env.j2 new file mode 100644 index 00000000..21e61a2a --- /dev/null +++ b/ansible/roles/netbird-proxy/templates/relay.env.j2 @@ -0,0 +1,21 @@ +# NetBird relay cluster member (per docs.netbird.io "Set Up External Relay +# Servers"). Registered in the management server's Relay.Addresses so the +# mesh keeps a working relay when the controller VPS (Hetzner) is down. +NB_LOG_LEVEL=info +NB_LISTEN_ADDRESS=:33080 +# Plaintext rel:// (not rels://): Let's Encrypt is impossible on this host — +# :443 belongs to the reverse-proxy (tls-alpn-01) and :80 to the redirect, so +# neither HTTP-01 nor a second ALPN listener can run, and shipping a +# zone-wide DNS-01 token to an internet-facing host is a worse trade than +# plaintext transport. Peer payload stays WireGuard-encrypted end to end; +# the exposure is relay framing metadata and credential sniff-and-reuse +# (bounded by the 24h credential TTL) to an on-path attacker. +NB_EXPOSED_ADDRESS=rel://{{ inventory_hostname }}:33080 +# Must equal the management server's Relay.Secret (vault, shared with the +# VPS relay) — mismatches fail silently per upstream docs. +NB_AUTH_SECRET={{ netbird_relay_secret }} +# Embedded STUN: reflexive-candidate discovery must survive a controller +# outage, or no NEW P2P connection can form while Hetzner is down and the +# relay diversity here would be moot for NATed peers. +NB_ENABLE_STUN=true +NB_STUN_PORTS=3478 diff --git a/ansible/roles/netbird-server/templates/management.json.j2 b/ansible/roles/netbird-server/templates/management.json.j2 index 67d13f37..6c20433d 100644 --- a/ansible/roles/netbird-server/templates/management.json.j2 +++ b/ansible/roles/netbird-server/templates/management.json.j2 @@ -3,6 +3,14 @@ { "Proto": "udp", "URI": "stun:{{ netbird_server_host }}:3478" + }, + { + "Proto": "udp", + "URI": "stun:proxy-1.renner.uno:3478" + }, + { + "Proto": "udp", + "URI": "stun:proxy-2.renner.uno:3478" } ], "TURNConfig": { @@ -23,7 +31,11 @@ "URI": "{{ netbird_server_host }}:443" }, "Relay": { - "Addresses": ["rels://{{ netbird_server_host }}:443"], + "Addresses": [ + "rels://{{ netbird_server_host }}:443", + "rel://proxy-2.renner.uno:33080", + "rel://proxy-1.renner.uno:33080" + ], "CredentialsTTL": "24h", "Secret": "{{ netbird_relay_secret }}" },