Skip to content

NetBird

The NetBird server on Bifrost coordinates a WireGuard mesh; an in-cluster routing peer advertises 192.168.1.0/24, so Bifrost’s Traefik and any enrolled laptop or phone reach LAN services. NetBird docs.

Servernetbird.madhan.app on Bifrost
Peersbifrost-agent (Docker on Bifrost), k8s-routing-peer (StatefulSet, namespace netbird)
Sourcenetbird/config.yaml, bootstrap.sh, netbird_peer.go
AuthEmbedded Dex, Authentik as upstream connector
flowchart LR
  L[Laptop / phone] -->|WireGuard| P
  T[Traefik] --> S[netbird-server<br/>management, signal,<br/>relay, Dex]
  B[bifrost-agent] <-->|WireGuard| P[k8s-routing-peer<br/>hostNetwork]
  S -. coordinates .- B & P
  P -->|IP forward<br/>+ MASQUERADE| G[Cilium Gateway<br/>192.168.1.220]
  S -->|Dex connector| A[Authentik]

The routing peer floats to any worker. Forwarded packets leave its node’s LAN NIC and are handled by Cilium on another node; the hop-by-hop path is in Traffic. Peer details live on NetBird peer.

The combined server always runs embedded Dex, so every JWT NetBird validates is signed by Dex. auth.issuer must stay https://netbird.madhan.app/oauth2; pointing it at Authentik makes Dex claim Authentik’s identity while signing with its own keys, and logins fail with unable to find appropriate key. Authentik is a connector inside Dex, added in the UI.

SettingValueWhy
auth.issuerhttps://netbird.madhan.app/oauth2Embedded Dex; see above
Dashboard clientnetbird-dashboard, public, no secretHard-coded static client in Dex
reverseProxy.trustedHTTPProxies172.30.0.10/32Traefik’s fixed IP in bifrost_net
store.enginesqlite, encrypted with NB_DATA_STORE_KEY
dataDir/var/lib/netbirdPeer and server state; see the caution below
Traefik routersgRPC (/signalexchange…, /management…) as h2c; /relay, /ws-proxy/, /api, /oauth2 as HTTP; everything else to the dashboardDefined in services.yml
STUNUDP 3478, published by netbird-serverBypasses Traefik
KeyStorePurposeRotate
NB_DATA_STORE_KEYSOPSSQLite encryption keyNever: the database is encrypted with it
NB_RELAY_SECRETSOPSRelay authYes; all peers reconnect
NB_OWNER_PASSWORDSOPSLocal admin admin@madhan.app in DexAfter the Authentik connector works
NETBIRD_CLIENT_SECRETSOPSDex → Authentik connectorYes
NB_PROXY_TOKENSOPSnetbird-proxy access tokenYes
NB_BIFROST_SETUP_KEYSOPSEnrols bifrost-agentYes
NETBIRD_SETUP_KEYOpenBao secret/netbirdEnrols k8s-routing-peer via the CSI driverYes

Setup keys are used only on first registration; after that each peer reuses its persisted key. Both peers may share one reusable key value.

Run after the first just core hetzner up has all containers healthy.

  1. Open https://netbird.madhan.app and sign in as admin@madhan.app with NB_OWNER_PASSWORD.

  2. Settings → Identity Providers → Add → Authentik:

    FieldValue
    Client IDaumenijDycfG1cQURqH9BNJpV3KVUCoMHGPUVUlT
    Client secretNETBIRD_CLIENT_SECRET from SOPS
    Issuerhttps://auth.madhan.app/application/o/netbird/

    The redirect URI shown must be https://netbird.madhan.app/oauth2/callback; just core authentik up already registered it. Test a GitHub login.

  3. Create credentials: Settings → Access Tokens (for NB_PROXY_TOKEN), and two reusable Setup Keys, bifrost-agent and k8s-routing-peer.

  4. Store the Bifrost values and redeploy, which starts netbird-agent and netbird-proxy:

    Terminal window
    sops edit secrets/bootstrap.sops.yaml # add NB_PROXY_TOKEN, NB_BIFROST_SETUP_KEY
    just core hetzner up
  5. Store the routing peer’s key in OpenBao:

    Terminal window
    export ROOT_TOKEN=$(just openbao-token)
    printf '%s\n%s' "$ROOT_TOKEN" '<k8s-routing-peer key>' | kubectl exec -i -n openbao openbao-0 -c openbao -- \
    sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao kv patch -mount=secret netbird NETBIRD_SETUP_KEY=-'
    just openbao-revoke
  6. Wait for the peer to connect: kubectl exec -n netbird netbird-peer-0 -- netbird status shows Management: Connected.

  7. Under Peers, delete any stale k8s-routing-peer duplicates so exactly one remains.

  8. Network Routes → Add Route: network 192.168.1.0/24, routing peer k8s-routing-peer, distribution group All. bifrost-agent must be in All to receive the route.

  9. Verify from Bifrost:

    Terminal window
    ssh root@178.156.199.250 'docker exec netbird-agent netbird routes list' # 192.168.1.0/24 Selected
    ssh root@178.156.199.250 'curl -s -o /dev/null -w "%{http_code}\n" -H "Host: grafana.madhan.app" http://192.168.1.220/'
Terminal window
netbird up --management-url https://netbird.madhan.app # enrol a laptop
netbird expose 8080 # temporary <name>.proxy.madhan.app
kubectl exec -n netbird netbird-peer-0 -- netbird status

netbird expose goes through Traefik TCP passthrough to netbird-proxy, which gets its own ACME certificate; it bypasses Authentik.

Public routes return 504 after the routing peer restarted

Section titled “Public routes return 504 after the routing peer restarted”

The Network Route points at a stale peer entry. Delete disconnected k8s-routing-peer duplicates under Peers and reassign the route.

Login fails with unable to find appropriate key

Section titled “Login fails with unable to find appropriate key”

auth.issuer points somewhere other than embedded Dex. Restore https://netbird.madhan.app/oauth2.

NB_SKIP_SOCKET_MARK was set on the routing peer. Remove it. For other tunnel problems see NetBird peer.