ITGOIT - Wiki

Netbird

If you use Cloudflare:
Make sure to enable: gRPC Allow gRPC connections to your origin server. (In the network category)

cd /home/myusername/docker

The following command will create a folder called netbird then it will cd into netbird/infrastructure_files

REPO="https://github.com/netbirdio/netbird/"; LATEST_TAG=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest)); echo $LATEST_TAG; git clone --depth 1 --branch $LATEST_TAG $REPO && cd netbird/infrastructure_files

Configure netbird

Configure netbird with Authentik

  1. Go to: Admin interface > Applications > Providers
  2. Click on: Create
  3. Select: OAuth2/OpenID Provider then click Next
  4. Insert the following settings:
  1. Go to: Admin interface > Applications > Applications
  2. Click on: Create
  3. Insert the following settings:

Name: Netbird
Slug: netbird
Provider: Netbird

  1. Go to: Admin interface > Directory > Users
  2. Click on: Create Service account
  3. Insert the following settings then click on Create:

Username: netbird
Create group: disable
Expiring: disable

Make sure to save the username and password because you need to add it later in setup.env

  1. Go to: Admin interface > Directory > Groups
  2. Click on: authentik Admins
  3. Click on: Users
  4. Click on: Add existing user
  5. Click on: + (Plus icon)
  6. Select Netbird and click Add

Verify if the endpoint returns a JSON response by calling it from your browser.
https://YOUR_AUTHENTIK_HOST_AND_PORT/application/o/netbird/.well-known/openid-configuration

nano setup.env

Add the following config to setup.env

# Dashboard domain.
NETBIRD_DOMAIN="netbird.DOMAIN.COM"

# TURN server domain. e.g. turn.mydomain.com
# if not specified it will assume NETBIRD_DOMAIN
NETBIRD_TURN_DOMAIN=""

# TURN server public IP address
# required for a connection involving peers in
# the same network as the server and external peers
# If you are confused just use the main server IP where netbird is hosted on
NETBIRD_TURN_EXTERNAL_IP="192.168.1.x"

NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="https://authentik.DOMAIN.COM/application/o/netbird/.well-known/openid-configuration"

# Replace IMPORT_CLIENT_ID_HERE with the authentik client ID of netbird
NETBIRD_AUTH_AUDIENCE="IMPORT_CLIENT_ID_HERE"
NETBIRD_AUTH_CLIENT_ID="IMPORT_CLIENT_ID_HERE"
NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID="IMPORT_CLIENT_ID_HERE"
NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid"
NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=false
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS="53000"

# Image tags
# You can force specific tags for each component; will be set to latest if empty
NETBIRD_DASHBOARD_TAG=""
NETBIRD_SIGNAL_TAG=""
NETBIRD_MANAGEMENT_TAG=""
COTURN_TAG=""

NETBIRD_AUTH_SUPPORTED_SCOPES="openid profile email offline_access api"
NETBIRD_USE_AUTH0="false"
NETBIRD_MGMT_IDP="authentik"

NETBIRD_IDP_MGMT_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
NETBIRD_IDP_MGMT_CLIENT_SECRET=""
NETBIRD_IDP_MGMT_EXTRA_USERNAME="netbird"
NETBIRD_IDP_MGMT_EXTRA_PASSWORD="IMPORT_SERVICE_ACCOUNT_PASSWORD_HERE"

NETBIRD_DISABLE_LETSENCRYPT=true
NETBIRD_LETSENCRYPT_EMAIL=""
NETBIRD_DISABLE_ANONYMOUS_METRICS=false
NETBIRD_MGMT_DNS_DOMAIN=netbird.selfhosted
NETBIRD_MGMT_API_PORT=443
NETBIRD_SIGNAL_PORT=443

Configure for traefik

cd /home/myusername/docker/traefik-crowdsec/traefik-data
nano fileConfig.yml
http:
  routers:

##########################################################
###======================ROUTERS======================###
### NetBird - router ###
    netbird:
      entryPoints:
        - https
        - http
      rule: "Host(`netbird.DOMAIN.COM`)"
      service: netbird


  # NetBird API - router
    netbird-api:
      rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/api`)"
      service: netbird-api


  # --- WebSocket Management Router ---
    netbird-ws-mgmt:
      rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/ws-proxy/management`)"
      service: netbird-api


  # --- WebSocket Signal Router ---
    netbird-ws-signal:
      rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/ws-proxy/signal`)"
      service: netbird-signal-http


  # NetBird Management - router (gRPC)
    netbird-management:
      rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/management.ManagementService/`)"
      service: netbird-management


  # NetBird Signal - Router (gRPC)
    netbird-signal:
      rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/signalexchange.SignalExchange/`)"
      service: netbird-signal


  # NetBird Relay - Router
    netbird-relay:
      rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/relay`)"
      service: netbird-relay


##########################################################
###======================SERVICES======================###
  services:

  ### NetBird - service ###
    netbird:
      loadBalancer:
        servers:
          - url: http://192.168.1.x:9180

  # NetBird API - service
    netbird-api:
      loadBalancer:
        servers:
          - url: http://192.168.1.x:9184

  # NetBird Management - service
    netbird-management:
      loadBalancer:
        servers:
          - url: h2c://192.168.1.x:9184

  # NetBird Signal - service
    netbird-signal:
      loadBalancer:
        servers:
          - url: h2c://192.168.1.x:9182

  # NetBird Signal - service (For WebSockets)
    netbird-signal-http:
      loadBalancer:
        servers:
          - url: http://192.168.1.x:9182

  # NetBird Relay - service
    netbird-relay:
      loadBalancer:
        servers:
          - url: http://192.168.1.x:33080
./configure.sh
cd artifacts && docker compose up -d

The following docker-compose.yml file is just a example for using it with a existing reverse proxy you could replace it existing docker-compose.yml with this one in the artifacts folder but make sure you change the DOMAIN. COM to your domain and the PROVIDER_CLIENT_ID_HERE

services:
  dashboard:
    image: netbirdio/dashboard:latest
    container_name: netbird-dashboard
    restart: unless-stopped
    ports:
      - 9180:80
#      - 443:443
    environment:
      # Endpoints
      NETBIRD_MGMT_API_ENDPOINT: https://netbird.DOMAIN.COM:443
      NETBIRD_MGMT_GRPC_API_ENDPOINT: https://netbird.DOMAIN.COM:443
      # OIDC
      AUTH_AUDIENCE: PROVIDER_CLIENT_ID_HERE
      AUTH_CLIENT_ID: PROVIDER_CLIENT_ID_HERE
      AUTH_CLIENT_SECRET: 
      AUTH_AUTHORITY: https://authentik.DOMAIN.COM/application/o/netbird/
      USE_AUTH0: false
      AUTH_SUPPORTED_SCOPES: openid profile email offline_access api
      AUTH_REDIRECT_URI: 
      AUTH_SILENT_REDIRECT_URI: 
      NETBIRD_TOKEN_SOURCE: accessToken
      # SSL
      NGINX_SSL_PORT: 443
      # Letsencrypt
#      - LETSENCRYPT_DOMAIN=
#      - LETSENCRYPT_EMAIL=
#    volumes:
#      - ./netbird_container/netbird-letsencrypt:/etc/letsencrypt/
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"
    networks:
      - proxy


  signal:
    image: netbirdio/signal:latest
    container_name: netbird-signal
    restart: unless-stopped
    volumes:
      - ./netbird_container/netbird-signal:/var/lib/netbird
    ports:
      - 9182:80
  #      # port and command for Let's Encrypt validation
  #      - 443:443
  #    command: ["--letsencrypt-domain", "", "--log-file", "console"]
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"
    networks:
      - proxy


  management:
    image: netbirdio/management:latest
    container_name: netbird-management
    restart: unless-stopped
    depends_on:
      - dashboard
    volumes:
      - ./netbird_container/netbird-mgmt:/var/lib/netbird
#      - ./netbird_container/netbird-letsencrypt:/etc/letsencrypt:ro
      - ./management.json:/etc/netbird/management.json
    ports:
      - 9184:443 #API port
  #    # command for Let's Encrypt validation without dashboard container
  #    command: ["--letsencrypt-domain", "", "--log-file", "console"]
    command: [
      "--port", "443",
      "--log-file", "console",
      "--log-level", "info",
      "--disable-anonymous-metrics=false",
      "--single-account-mode-domain=netbird.DOMAIN.COM",
      "--dns-domain=netbird.selfhosted"
      ]
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"
    networks:
      - proxy


  coturn:
    image: coturn/coturn:latest
    container_name: netbird-coturn
    restart: unless-stopped
    #domainname: netbird.DOMAIN.COM # only needed when TLS is enabled
    volumes:
      - ./turnserver.conf:/etc/turnserver.conf:ro
    #      - ./privkey.pem:/etc/coturn/private/privkey.pem:ro
    #      - ./cert.pem:/etc/coturn/certs/cert.pem:ro
    network_mode: host
    command:
      - -c /etc/turnserver.conf
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"
        
  relay:
    image: netbirdio/relay:latest
    container_name: netbird-relay
    restart: unless-stopped
    environment:
      NB_LOG_LEVEL: info
      NB_LISTEN_ADDRESS: :33080
      NB_EXPOSED_ADDRESS: rels://netbird.DOMAIN.COM:443/relay
      NB_AUTH_SECRET: XtsT4YH4g99nehxK39X8zcjlay2e1MMPWm6GKxguXJs # generate a new key with: openssl rand -base64 32 | sed 's/=//g'
    ports:
      - 33080:33080
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"
    networks:
      - proxy
      
networks:
  proxy:
    external: true

management.json

    "Relay": {
        "Addresses": ["rels://netbird.DOMAIN.COM:443/relay"],
        "CredentialsTTL": "24h",
        "Secret": "XtsT4YH4g99nehxK39X8zcjlay2e1MMPWm6GKxguXJs"
    },

Netbird client (Docker)

cd /home/myusername/docker
mkdir netbird_client && cd "$_"

When netbird-client network is set to MACVLAN (LAN) the server host ip will not be accessable
When netbird-client network is set to default docker network it will have full access

nano docker-compose.yml

services:
  netbird-client:
      image: netbirdio/netbird:latest
      container_name: netbird-client
      hostname: gateway-prod-01
      restart: unless-stopped
      network_mode: host # Use host networking for Routing Peers for best performance
      privileged: true
      cap_add:
        - NET_ADMIN
        - SYS_ADMIN # for better BPF/Kernel support
        - SYS_RESOURCE
      environment:
        NB_SETUP_KEY: IMPORT_SETUP_KEY_HERE
        NB_MANAGEMENT_URL: https://netbird.DOMAIN.COM:443
#        NB_ADMIN_URL: https://netbird.DOMAIN.COM:443
        NB_ALLOW_SERVER_SSH: true # for emergency access to the gateway
      volumes:
        - ./data:/etc/netbird

Netbird client (Windows)

Download the client here: https://pkgs.netbird.io/windows/x64

Update Netbird

docker compose pull
docker compose up -d --force-recreate

verify netbird version

docker inspect IMAGE_ID | grep version

Ultimate Enterprise Zero-Trust VPN Setup

1. Architecture Overview

Requirements
- Traefik
- Technitium
- Netbird client
- Netbird

This infrastructure usecase utilizes NetBird operating on a Zero-Trust model.
Unlike traditional VPNs that grant access to the entire network upon connection, this architecture uses NetBird's "Networks" and "Access Policies" to grant least-privilege access.

Traffic routing is handled dynamically via Domain Resources (*.local.domain.com). This means the VPN intercepts traffic based on the URL the user requests, securely queries the internal DNS (Technitium), and tunnels the traffic to the Traefik reverse proxy.

192.168.1.100 = Main linux server with Docker, Technitium, traefik, netbird etc...
192.168.1.123 = Technitium DNS hosted from 192.168.1.100 with a macvlan

2. Crucial Networking Rules

The "Overlapping Subnet" Rule

For VPN routing to work flawlessly, the Client's physical network (e.g., a hotel Wi-Fi) must not use the same IP subnet as the Office/Server network.

If the Server is on 192.168.1.0/24 and a remote developer connects from a home network that is ALSO 192.168.1.0/24, the operating system will drop the VPN packets because physical Ethernet connections always take priority over VPN tunnels.

✅ The Enterprise Solution (Recommended):
The Server/Office network should be migrated to an obscure subnet (e.g., 10.42.0.0/24 or 192.168.99.0/24). This guarantees that remote users will never experience IP collisions when traveling.

⚠️ The Temporary Workaround (Not Scalable!):
If a user is actively experiencing an overlapping subnet block and the server subnet cannot be changed, you can force their OS to obey the VPN by manually injecting a /32 route on their client machine:

The Macvlan Host-to-Container Rule

The gateway-prod-01 NetBird client runs in Docker network_mode: host. The Technitium DNS runs in a macvlan network on the same host. Linux kernel security blocks a host from directly pinging its own Macvlan IPs.
Use a SystemD "Macvlan Shim" service on the Linux host to bypass this restriction so the VPN gateway can reach the DNS server.

3. Step-by-Step Configuration Guide

Phase 1: Group Architecture

Maintain a strict separation between User Groups and Resource Groups.

Phase 2: Internal DNS Integration (Technitium)

NetBird needs to know how to resolve the internal .local domains without hijacking the public Cloudflare domains.

  1. Navigate to DNS -> Nameservers -> Add Nameserver.
  2. Type: Custom DNS
  3. Name: Technitium Internal
  4. Nameserver IP: 192.168.1.123 (Port 53)
  5. Match Domains: local.domain.com (Do NOT add domain.com here, or public sites will break).
  6. Toggle: Enable "Mark match domains as search domains".
  7. Distribution Groups: people-sysadmins, people-developers, people-users, AND routing-peers-prod.

Phase 3: Defining the Networks

We use the modern "Networks" tab (not the legacy "Network Routes").

Network 1: Internal Web Apps (Domain Routing)

Network 2: Physical Infrastructure

Network 3: Docker Backend (Sysadmin only)

Phase 4: Access Control Policies

These policies map the Users to the Resources to establish Zero-Trust.

  1. DNS Access Policy
    • Source: people-users, people-developers, people-sysadmins
    • Destination: res-dns
    • Protocol: UDP/TCP Port 53
  2. General Web Access
    • Source: people-users, people-developers, people-sysadmins
    • Destination: res-web-internal
    • Protocol: TCP Ports 80, 443
  3. Sysadmin / DevOps Access
    • Source: people-sysadmins
    • Destination: res-docker-mgmt
    • Protocol: ALL

4. Infrastructure Recovery Scripts

The Macvlan Shim Service

If the Linux Host is ever rebuilt, the NetBird Gateway will lose the ability to query Technitium DNS unless this shim is recreated.

sudo nano /etc/systemd/system/macvlan-shim.service

Insert the following contents in macvlan-shim.service

[Unit]
Description=Macvlan Shim for Technitium DNS (NetBird to Docker Macvlan)
After=network.target network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes

ExecStartPre=-/sbin/ip link del macvlan-shim
ExecStart=/sbin/ip link add macvlan-shim link REPLACE_ME_WITH_NETWORK_CARD_NAME type macvlan mode bridge
ExecStart=/sbin/ip addr add 192.168.1.250/32 dev macvlan-shim
ExecStart=/sbin/ip link set macvlan-shim up
ExecStart=/sbin/ip route add 192.168.1.123/32 dev macvlan-shim
ExecStop=-/sbin/ip link del macvlan-shim

[Install]
WantedBy=multi-user.target

Activation:

sudo systemctl daemon-reload
sudo systemctl enable --now macvlan-shim.service

Troubleshooting

System Crash / Too many open files (Coturn Socket Exhaustion)

Symptom:
The entire Linux host crashes or loses network connectivity. Checking the logs (journalctl or dmesg) reveals critical errors like systemd-networkd-wait-online: Could not create manager: Too many open files.

Root Cause:
Because the NetBird Coturn container runs in network_mode: host, if a listening IP is not explicitly defined, it defaults to listening on 0.0.0.0 (all network interfaces). On a host running many Docker containers, this causes Coturn to open TCP and UDP sockets for every single Docker bridge network (e.g., 172.x.x.x), rapidly exhausting the Linux kernel's file descriptor limit and crashing system services.

The Fix:
Force Coturn to only listen on the host machine's primary local IP rather than 0.0.0.0.

  1. Open the Coturn configuration file:
nano /home/myusername/docker/netbird/infrastructure_files/artifacts/turnserver.conf
  1. Find the listening-ip and relay-ip settings, uncomment them (remove the #), and set them to the server's primary LAN IP (e.g., 192.168.1.100 or 192.168.1.x):
listening-ip=192.168.1.x
relay-ip=192.168.1.x

(Ensure there are no active lines saying 0.0.0.0)

  1. Restart the Coturn container:
docker restart netbird-coturn
  1. Verify the fix. It should now only see the specific IP listening, not a massive list of internal Docker network addresses:
sudo ss -tuln | grep 3478

Here is the Wiki-style article adapted for the New Combined Container Architecture (v0.65.0+).

Even though NetBird merged everything into two containers (dashboard and netbird-server), the underlying protocol for the Signal and Management streams is still gRPC. Therefore, the exact same rules regarding WAF buffering, Gzip, and Docker timeouts apply!

Netbird Desktop client hangs on Connecting

The Root Cause: gRPC vs. Web Application Firewalls (WAF)

The new netbird-server container combines HTTP API, WebSockets, and gRPC onto a single port (usually 80 inside the container). Traefik uses a multiplexer to split this traffic.

The WAF Buffering Problem:
When you enable CrowdSec AppSec (Deep Packet Inspection) or Gzip globally in Traefik, they attempt to "buffer" the request body to scan it for malicious payloads or compress it.
Because gRPC (used by NetBird's Signal and Management services) is a continuous, never-ending HTTP/2 stream, the WAF waits forever for the stream to "finish." This traps the connection, causing the client to hang on "Connecting...".

Step 1: Remove Global WAF and Gzip Middlewares

You cannot apply crowdsec-bouncer or gzip globally to your HTTPS entrypoint. If you do, it will intercept and break the gRPC streams.

Incorrect (traefik.yml):

  https:
    address: :443
    http:
      middlewares:
        - crowdsec-bouncer@file  # ❌ REMOVE THIS
        - gzip@file              # ❌ REMOVE THIS

(Fix: Remove them from traefik.yml and instead apply them directly to standard websites in your fileConfig.yml, completely omitting them from the NetBird gRPC routers).

Step 2: Use Internal Docker Routing (Bypass 60s Timeout)

Just like the legacy setup, if you route Traefik traffic to your Host IP (e.g., 192.168.1.95), Docker's internal firewall will aggressively kill idle gRPC streams every 60 seconds, causing constant reconnects.

Ensure Traefik and NetBird share a Docker network (e.g., dmz) and use the internal container names.

Correct docker-compose.yml for NetBird:

services:
  dashboard:
    image: netbirdio/dashboard:latest
    container_name: netbird-dashboard
    restart: unless-stopped
    networks:
      - dmz
      
  netbird-server:
    image: netbirdio/netbird-server:latest
    container_name: netbird-server
    restart: unless-stopped
    ports:
      # We only need to expose STUN to the outside world! 
      # Traefik will talk to the HTTP/gRPC port entirely internally.
      - "3478:3478/udp" 
    volumes:
      - ./data:/var/lib/netbird
      - ./config.yaml:/etc/netbird/config.yaml
    command: ["--config", "/etc/netbird/config.yaml"]
    networks:
      - dmz

networks:
  dmz:
    external: true

Step 3: Correct Traefik Dynamic Configuration

Notice how we split the standard HTTP/WebSocket routes from the native gRPC routes. The gRPC router must point to a service using the h2c:// scheme, and neither router should use the CrowdSec middleware.

Correct fileConfig.yml:

http:
  routers:
    # 1. Dashboard Router
    netbird-dashboard:
      entryPoints:
        - https
      rule: "Host(`netbird.example.com`)"
      service: netbird-dashboard-svc
      middlewares:
        - "securityHeadersPrivate"

    # 2. HTTP & WebSocket Backend Router (API, Embedded IdP, Relay, WS)
    netbird-backend:
      entryPoints:
        - https
      rule: "Host(`netbird.example.com`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
      service: netbird-server-svc
      middlewares:
        - "securityHeadersPrivate"

    # 3. Native gRPC Router (Signal, Management, & ProxyService)
    # 🚨 DO NOT ADD CROWDSEC OR GZIP MIDDLEWARES HERE 🚨
    netbird-grpc:
      entryPoints:
        - https
      rule: "Host(`netbird.example.com`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
      service: netbird-server-grpc-svc

  services:
    # Dashboard uses internal Docker port 80
    netbird-dashboard-svc:
      loadBalancer:
        servers:
          - url: "http://netbird-dashboard:80"

    # Standard HTTP uses internal Docker port 80
    netbird-server-svc:
      loadBalancer:
        servers:
          - url: "http://netbird-server:80"

    # gRPC uses internal Docker port 80, but REQUIRES the h2c:// scheme!
    netbird-server-grpc-svc:
      loadBalancer:
        servers:
          - url: "h2c://netbird-server:80"

Step 4: Ensure Cloudflare is "DNS Only" (Grey Cloud)

If you use Cloudflare for your DNS, the record pointing to NetBird (e.g., netbird.example.com) must be set to "Grey Cloud" (DNS Only). Cloudflare's proxy buffers gRPC streams identically to CrowdSec, and will break the connection.

By bypassing the WAF for gRPC streams and using internal Docker networking to avoid NAT connection pruning, your new unified NetBird server will connect instantly and remain highly stable.