diff --git a/Caddyfile b/Caddyfile index be97de4..d2cf872 100644 --- a/Caddyfile +++ b/Caddyfile @@ -21,11 +21,19 @@ status.{$DOMAIN} { reverse_proxy uptime-kuma:3001 } -n8n.{$DOMAIN} { - reverse_proxy n8n:5678 -} +# n8n has no public route any more - it is reachable over the mesh only (episode 5) +# NetBird control plane (episode 5) - dashboard, API, gRPC, relay, built-in IdP vpn.{$DOMAIN} { - reverse_proxy wg-easy:51821 + # Native gRPC (needs HTTP/2 cleartext to backend) + @grpc header Content-Type application/grpc* + reverse_proxy @grpc h2c://netbird-server:80 + + # Combined server paths (relay, signal, management, OAuth2) + @backend path /relay* /ws-proxy/* /api/* /oauth2/* + reverse_proxy @backend netbird-server:80 + + # Dashboard (everything else) + reverse_proxy /* netbird-dashboard:80 } diff --git a/compose.yml b/compose.yml index 4306c7c..da57762 100644 --- a/compose.yml +++ b/compose.yml @@ -9,7 +9,7 @@ # uptime-kuma monitoring + alerts # n8n automation hub # wg-easy WireGuard VPN (optional — start with: --profile vpn) -# (full Netbird mesh gets its own episode) +# (the full NetBird mesh is in netbird/ — episode 5) # # Quickstart: # 1. cp .env.example .env && edit it (domain, email, secrets) @@ -22,6 +22,11 @@ name: selfhostedops-stack networks: proxy: + driver_opts: + # Docker 28+ firewalls direct access to container IPs from other + # interfaces (raw PREROUTING drop per container). This lets the NetBird + # mesh interface reach the stack containers - see netbird/ (episode 5). + com.docker.network.bridge.trusted_host_interfaces: "wt0" ci: internal: diff --git a/netbird/README.md b/netbird/README.md new file mode 100644 index 0000000..466ede9 --- /dev/null +++ b/netbird/README.md @@ -0,0 +1,70 @@ +# NetBird - the mesh around the stack (episode 5) + +Two containers: the dashboard and a combined server that carries the +management API, the signal service, a relay, STUN and a built-in identity +provider. Everything except one UDP port rides through the same Caddy as the +rest of the stack. + +## Install (on the box, next to the stack) + +```bash +mkdir -p /opt/netbird && cd /opt/netbird +curl -fsSL https://raw.githubusercontent.com/netbirdio/netbird/main/infrastructure_files/getting-started.sh -o getting-started.sh + +NETBIRD_DOMAIN=vpn.example.com \ +NETBIRD_REVERSE_PROXY_TYPE=4 \ +NETBIRD_EXTERNAL_PROXY_NETWORK=selfhostedops-stack_proxy \ +NETBIRD_BIND_LOCALHOST_ONLY=true \ +bash getting-started.sh +``` + +The script renders `docker-compose.yml`, `config.yaml`, `dashboard.env` and +the exact Caddy block to paste (`caddyfile-netbird.txt` - the same block is +already in this repo's `Caddyfile`). Reload Caddy, then open +`https://vpn.example.com`: a one-time setup page asks for the first admin +account. That account lives in the built-in provider and is your break-glass +login - store the password in the vault from episode 4. + +## Forgejo as the network's identity provider + +1. Forgejo -> Site administration -> Applications -> new OAuth2 application. + Redirect URI: `https://vpn.example.com/oauth2/callback` (confidential client). +2. NetBird dashboard -> Settings -> Identity Providers -> Add Identity Provider. + Type: OIDC (Generic). Issuer: your Forgejo base URL. Paste client ID + secret. +3. The login page now shows "Continue with Forgejo". First-time users land in + *pending approval* until an admin approves them in the Team list. + +## Machines join + +Servers use setup keys (Settings -> Setup Keys -> Create Key): + +```bash +curl -fsSL https://pkgs.netbird.io/install.sh | sh +netbird up --setup-key --management-url https://vpn.example.com:443 +``` + +People just run `netbird up --management-url https://vpn.example.com:443`; +the CLI prints a device-code URL, and the browser side is the same +"Continue with Forgejo" button. + +## Reaching the containers (and the Docker 28+ trap) + +Dashboard -> Networks: add a network, add the compose subnet as a resource +(e.g. `172.18.0.0/16`), add the box itself as routing peer, add one access +policy. Then note the one line already present on the `proxy` network in +`compose.yml`: + +```yaml +networks: + proxy: + driver_opts: + com.docker.network.bridge.trusted_host_interfaces: "wt0" +``` + +Docker 28+ drops any packet to a container IP that does not arrive on +Docker's own bridge (a raw-table rule, so DOCKER-USER does not help). The +option above makes Docker write the accept rules itself. Changing it needs a +network recreate (`docker compose down && docker compose up -d`). + +After that, delete a service's public block from the `Caddyfile`, reload +Caddy, and the service is reachable over the mesh only.