Skip to content

Latest commit

 

History

History
480 lines (344 loc) · 22.1 KB

File metadata and controls

480 lines (344 loc) · 22.1 KB

Migration Guide: Enable Reverse Proxy Feature

This guide walks you through adding the NetBird Reverse Proxy to an existing self-hosted deployment. By the end, you'll have a netbird-proxy container running alongside your existing services, ready to expose internal applications to the public internet.

**Who is this guide for?** This migration guide is for existing self-hosted users who: - Already have a working NetBird deployment (management server, dashboard, signal, relay) - Want to enable the [Reverse Proxy](/manage/reverse-proxy) feature to expose internal services publicly - Are running their services via Docker Compose

Why Traefik is required

The NetBird proxy container manages its own TLS certificates (via Let's Encrypt or static files). This means the reverse proxy sitting in front of it must not terminate TLS - it needs to pass raw TLS connections through to the proxy container untouched.

This capability is called TLS passthrough, and among common reverse proxies, only Traefik supports it via its TCP routers. Other reverse proxies (Nginx, Caddy, Nginx Proxy Manager) terminate TLS themselves and cannot forward the raw encrypted connection, which breaks the proxy's certificate management.

If your current deployment uses a reverse proxy other than Traefik, you'll need to switch before enabling this feature. See Switching to Traefik for instructions.

Overview of changes

What you're adding

  • proxy service (container: netbird-proxy) - a new service in your Docker Compose stack that handles TLS termination, certificate provisioning, and traffic forwarding for reverse proxy services
  • proxy.env file - environment variables for the proxy container, including domain, token, and ACME configuration
  • Traefik TCP labels - routing rules that tell Traefik to pass TLS connections through to the proxy container
  • Wildcard DNS record - so that all service subdomains (e.g., myapp.proxy.example.com) resolve to your server
  • Proxy access token - generated via the management CLI, used by the proxy to authenticate with the management server

What stays the same

  • Your existing NetBird services are unchanged
  • Your configuration files (config.yaml for combined setup, management.json for multi-container setup) require no modifications - unless you use an external identity provider (not the embedded IdP). See Configure SSO for external identity providers below.
  • Existing peers, networks, and access policies are unaffected

Prerequisites

Before starting, ensure you have:

  • Traefik as your reverse proxy (see Why Traefik is required above)
  • Latest NetBird images - pull the latest version to ensure the management server and CLI support the reverse proxy feature and token creation
  • A domain for the proxy - e.g., proxy.example.com or netbird.example.com (it can be the same domain as the NetBird server itself). Service subdomains will be created under this domain (e.g., myapp.netbird.example.com)
  • Wildcard DNS capability - ability to create a *.netbird.example.com DNS record pointing to your server
  • Port 443 accessible - the proxy needs this for ACME TLS-ALPN-01 challenges (certificate provisioning)
This guide covers both the **combined container** setup (`netbirdio/netbird-server`, the default for new deployments) and the **multi-container** setup (separate `management`, `signal`, and `relay` images). Where commands or configuration differ between the two setups, both variants are shown.

Migration steps

Step 1: Backup current configuration

# Create a backup directory
mkdir -p netbird-backup-$(date +%Y%m%d)
cd netbird-backup-$(date +%Y%m%d)

# Backup configuration files
cp ../docker-compose.yml .
cp ../config.yaml . 2>/dev/null       # Combined container setup
cp ../management.json . 2>/dev/null   # Multi-container setup
cp ../*.env . 2>/dev/null || echo "No .env files found"

Step 2: Generate a proxy access token

The proxy authenticates with the management server using an access token. Generate one using the server CLI.

Combined container (netbirdio/netbird-server):

docker exec -it netbird-server /go/bin/netbird-server token create \
  --name "my-proxy" --config <netbird-data-dir>/config.yaml

Multi-container (separate netbirdio/management image):

docker exec -it netbird-management /go/bin/netbird-mgmt token create --name "my-proxy"

This outputs a token in the format nbx_... (40 characters). Save the token immediately - it is only displayed once. The management server stores only a SHA-256 hash.

You can manage tokens later with:

# List all tokens (combined container)
docker exec -it netbird-server /go/bin/netbird-server token list \
  --config <netbird-data-dir>/config.yaml

# List all tokens (multi-container)
docker exec -it netbird-management /go/bin/netbird-mgmt token list

# Revoke a token by ID (combined container)
docker exec -it netbird-server /go/bin/netbird-server token revoke <token-id> \
  --config <netbird-data-dir>/config.yaml

# Revoke a token by ID (multi-container)
docker exec -it netbird-management /go/bin/netbird-mgmt token revoke <token-id>

Step 3: Add the proxy service to docker-compose.yml

Add the following service to your docker-compose.yml. Adjust the depends_on value to match your management service name:

proxy:
  image: netbirdio/reverse-proxy:latest
  container_name: netbird-proxy
  restart: unless-stopped
  networks: [netbird]
  depends_on:
    - netbird-server  # Use "management" for multi-container setup
  env_file:
    - ./proxy.env
  volumes:
    - netbird_proxy_certs:/certs
  labels:
    - traefik.enable=true
    - traefik.tcp.routers.proxy-passthrough.entrypoints=websecure
    - traefik.tcp.routers.proxy-passthrough.rule=HostSNI(`*`)
    - traefik.tcp.routers.proxy-passthrough.tls.passthrough=true
    - traefik.tcp.routers.proxy-passthrough.service=proxy-tls
    - traefik.tcp.routers.proxy-passthrough.priority=1
    - traefik.tcp.services.proxy-tls.loadbalancer.server.port=8443
  logging:
    driver: "json-file"
    options:
      max-size: "500m"
      max-file: "2"

Also add the netbird_proxy_certs volume to your volumes: section:

volumes:
  # ...existing volumes...
  netbird_proxy_certs:

Then create a proxy.env file with the proxy configuration.

Combined container (netbirdio/netbird-server):

NB_PROXY_DOMAIN=proxy.example.com
NB_PROXY_TOKEN=nbx_your_token_here
NB_PROXY_MANAGEMENT_ADDRESS=http://netbird-server:80
NB_PROXY_ALLOW_INSECURE=true
NB_PROXY_ADDRESS=:8443
NB_PROXY_ACME_CERTIFICATES=true
NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01
NB_PROXY_CERTIFICATE_DIRECTORY=/certs

Multi-container (separate netbirdio/management image):

NB_PROXY_DOMAIN=proxy.example.com
NB_PROXY_TOKEN=nbx_your_token_here
NB_PROXY_MANAGEMENT_ADDRESS=http://management:33073
NB_PROXY_ALLOW_INSECURE=true
NB_PROXY_ADDRESS=:8443
NB_PROXY_ACME_CERTIFICATES=true
NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01
NB_PROXY_CERTIFICATE_DIRECTORY=/certs
The proxy connects to the management server directly over the Docker network rather than through Traefik; this avoids the need to route the `/management.ProxyService/` gRPC service through your reverse proxy. The `NB_PROXY_ALLOW_INSECURE=true` setting is safe here because the traffic never leaves the Docker network.

If your proxy and management server run on separate hosts and cannot communicate over a shared Docker network, see Connecting through Traefik below.

The Traefik labels configure a TCP router that:

  • Catches any request not matched by higher-priority HTTP routers via HostSNI(*) (wildcard)
  • Uses the websecure entrypoint (port 443)
  • Passes the TLS connection through without termination (tls.passthrough=true)
  • Uses priority=1 to avoid intercepting traffic meant for the main NetBird HTTP routers on the same entrypoint
  • Forwards traffic to the proxy container on port 8443
The `HostSNI(*)` rule acts as a catch-all for any domain not matched by the existing NetBird HTTP routers. The `priority=1` ensures this TCP router only handles traffic that no other router claims. Any domain pointing to your server that isn't `netbird.example.com` will be forwarded to the proxy.

Step 4: Set up DNS records

Create one DNS record pointing to the server running your NetBird stack - one for the base proxy domain and one wildcard for service subdomains:

Type Name Content
CNAME *.netbird.example.com netbird.example.com

The base domain record is required because a wildcard DNS record does not cover the bare domain itself. The wildcard record ensures that all service subdomains (e.g., myapp.netbird.example.com, dashboard.netbird.example.com) resolve to your server where Traefik forwards them to the proxy container.

Step 5: Apply changes

# Pull the new image
docker compose pull proxy

# Start the proxy alongside existing services
docker compose up -d

# Verify all services are running
docker compose ps

# Check proxy logs
docker compose logs -f proxy

You should see log messages indicating the proxy has connected to the management server and is ready to serve traffic.

Step 6: Verify in the dashboard

Once the proxy connects to the management server:

  1. Open your NetBird dashboard
  2. Navigate to Reverse Proxy > Services
  3. Click Add Service
  4. In the domain selector, you should see your proxy domain (e.g., proxy.example.com) with a Cluster badge

If the domain appears, the proxy is connected and ready. You can now create your first service.

Configure SSO for external identity providers

Who this applies to

This section applies to multi-container deployments using a standalone external identity provider (Auth0, Okta, Keycloak, Zitadel, etc.) instead of the built-in embedded IdP (Dex). If you are running the combined container or deployed using the quickstart script with default settings, you are using the embedded IdP and can skip this section - the callback is registered automatically.

Why this is needed

The reverse proxy SSO feature authenticates users through an OAuth2/OIDC flow that redirects through a callback endpoint on the management server (/api/reverse-proxy/callback). The embedded IdP registers this callback automatically, but external IdPs need it configured manually. Without this configuration, SSO authentication on reverse proxy services will silently fail.

Option A: Quick fix (keep your external IdP)

If you want to keep using your current external identity provider, follow these three steps:

Step 1: Add callback URL to management.json (multi-container only)

Add the AuthCallbackURL and AuthClientID fields to the HttpConfig section of your management.json:

"HttpConfig": {
  ...existing fields...,
  "AuthClientID": "<your-auth-client-id>",
  "AuthCallbackURL": "https://<your-management-domain>/api/reverse-proxy/callback"
}

Replace <your-management-domain> with your NetBird management server domain (the same domain used for the dashboard). Replace <your-auth-client-id> with the OAuth2 client ID from your identity provider (the same client ID used for the dashboard application).

Step 2: Register callback in your IdP

In your identity provider's application settings, add the following URL as an allowed redirect URI / callback URL:

https://<your-management-domain>/api/reverse-proxy/callback

This is in addition to any existing redirect URIs (like /auth or /silent-auth).

Where to find this setting in common providers:

Provider Where to add the redirect URI
Auth0 Application > Settings > Allowed Callback URLs
Okta Application > General > Login redirect URIs
Keycloak Client > Settings > Valid redirect URIs
Zitadel Application > Redirect Settings > Redirect URIs
Generic OIDC Refer to your provider's documentation

Step 3: Restart management server

Restart the management service to pick up the configuration change:

docker compose restart management

Option B: Migrate to the embedded IdP (recommended)

The embedded IdP (Dex) handles the reverse proxy callback registration automatically - no manual configuration needed. If you want a simpler setup, consider migrating to the embedded IdP.

With the embedded IdP, external identity providers can still be used as connectors alongside local authentication. This means your users can continue to sign in with their existing accounts (Google, Okta, Keycloak, etc.) while the embedded IdP manages the OIDC layer.

See the Identity Providers page for instructions on adding external IdPs as connectors.

Migrating from a standalone external IdP to the embedded IdP with your IdP as a connector requires user ID migration. See the [Migration Guide](/selfhosted/identity-providers#migration-guide-and-backwards-compatibility) or contact [support@netbird.io](mailto:support@netbird.io) for assistance.

Verification

After configuring SSO for your external identity provider, verify it works:

  1. Create a reverse proxy service with SSO authentication enabled
  2. Open the service URL in an incognito/private browser window
  3. Confirm you are redirected to your IdP login page
  4. After authenticating, confirm you are redirected back to the service and can access it

If the redirect fails or you see an error from your IdP, double-check that the callback URL is correctly registered in both your configuration (management.json for multi-container setups) and your identity provider's settings.

Connecting through Traefik instead of Docker network

If your proxy container cannot reach the management container directly - for example, if they run on separate hosts - you can route the proxy's management connection through Traefik instead. This requires three additional configuration steps.

1. Add the ProxyService gRPC route

The proxy communicates with the management server over two gRPC services: ManagementService and ProxyService. Both paths must be routed through Traefik. Find the existing gRPC router label in your docker-compose.yml - in a standard deployment this is traefik.http.routers.netbird-grpc - and add the ProxyService path prefix:

traefik.http.routers.netbird-grpc.rule=Host(`netbird.example.com`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))

Without the /management.ProxyService/ route, the proxy will fail to register with the management server.

2. Fix DNS resolution (same-host only)

If your proxy and management server run on **separate hosts** - which is the typical reason for routing through Traefik - you can skip this step. The proxy will resolve your management domain to the remote host's public IP via normal DNS.

If your proxy and Traefik run on the same host but you still need to route through Traefik (rather than using the direct Docker network connection above), the proxy must resolve the management domain to Traefik's container IP. Without this, the domain resolves to the host's public IP and the connection loops back through the external interface - a hairpin NAT problem.

To fix DNS resolution on the same host, assign a static IP to the Traefik container and add an extra_hosts entry to the proxy service:

# In your docker-compose.yml

networks:
  netbird:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/24
          gateway: 172.30.0.1

services:
  traefik:
    # ...existing traefik config...
    networks:
      netbird:
        ipv4_address: 172.30.0.10

  proxy:
    # ...existing proxy config...
    extra_hosts:
      - "netbird.example.com:172.30.0.10"

Replace netbird.example.com with your actual management domain.

3. Increase Traefik's idle timeout for gRPC

Traefik's default idle timeout (180 seconds) is too short for the long-lived gRPC streams used between the proxy and management server. Without increasing it, the proxy will report connection timeout errors and the dashboard may show the proxy agent as offline.

Add the following to your Traefik static configuration:

# In traefik.yml
entryPoints:
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        idleTimeout: "0"

Or as a command-line argument:

# In docker-compose.yml
services:
  traefik:
    command:
      # ...existing args...
      - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"

Finally, update proxy.env to connect through Traefik and remove NB_PROXY_ALLOW_INSECURE:

NB_PROXY_MANAGEMENT_ADDRESS=https://netbird.example.com:443
# Do NOT set NB_PROXY_ALLOW_INSECURE when connecting over TLS through Traefik
If you use the `extra_hosts` approach above, you **must** assign a static IP to Traefik. Without it, Docker may assign a different IP on container restart and the `extra_hosts` entry will silently point to the wrong address.

For users not on Traefik

If your self-hosted deployment currently uses Nginx, Caddy, or another reverse proxy, you'll need to switch to Traefik before enabling the Reverse Proxy feature. See the Traefik setup instructions for a step-by-step guide on configuring Traefik for your NetBird deployment.

Environment variable reference

Variable Required Description Default
NB_PROXY_TOKEN Yes Access token generated via netbird-server token create (combined) or netbird-mgmt token create (multi-container). The proxy refuses to start without it. -
NB_PROXY_DOMAIN Yes Base domain for this proxy instance (e.g., proxy.example.comor netbird.example.com). Determines the domain available for services. -
NB_PROXY_MANAGEMENT_ADDRESS No URL of your NetBird management server. The proxy connects via gRPC to register itself. https://api.netbird.io:443
NB_PROXY_ADDRESS No Address the proxy listens on. :8443 (Docker), :443 (binary)
NB_PROXY_ACME_CERTIFICATES No Set to true to enable automatic TLS certificate provisioning via Let's Encrypt. false
NB_PROXY_ACME_CHALLENGE_TYPE No ACME challenge type: tls-alpn-01 (port 443) or http-01 (port 80). tls-alpn-01
NB_PROXY_CERTIFICATE_FILE No TLS certificate filename within the certificate directory (for static certificate mode). tls.crt
NB_PROXY_CERTIFICATE_KEY_FILE No TLS private key filename within the certificate directory (for static certificate mode). tls.key
NB_PROXY_CERTIFICATE_DIRECTORY No Directory where static certificate files are stored. ./certs
NB_PROXY_ALLOW_INSECURE No Allow insecure (non-TLS) gRPC connection to the management server. Set to true when connecting over an internal Docker network. false
NB_PROXY_FORWARDED_PROTO No Protocol to report in the X-Forwarded-Proto header. Set to https when TLS is terminated at the proxy. -
NB_PROXY_DEBUG_LOGS No Enable debug-level logging. false

Troubleshooting

Certificate provisioning failures

Symptom: Services stay in certificate_pending or move to certificate_failed status.

Checklist:

  1. Verify port 443 is accessible from the internet (required for tls-alpn-01 challenge)
  2. Ensure the wildcard DNS record resolves correctly: dig myapp.proxy.example.com
  3. Check proxy logs for ACME errors: docker compose logs proxy | grep -i acme
  4. If using http-01 challenge type, ensure port 80 is also accessible

TLS passthrough not working

Symptom: The proxy starts but services return TLS errors or Traefik's default certificate.

Checklist:

  1. Verify Traefik labels include tls.passthrough=true
  2. Confirm the router is configured as a TCP router (not HTTP) - labels should use traefik.tcp.routers, not traefik.http.routers
  3. Check that the HostSNI rule is set to HostSNI(*) (wildcard catch-all)
  4. Verify the TCP router has priority=1 to prevent it from intercepting traffic meant for the main NetBird HTTP routers
  5. Ensure the websecure entrypoint is configured in your Traefik configuration
  6. Restart Traefik after adding the proxy container: docker compose restart traefik

Port conflicts

Symptom: The proxy container fails to start with an address-in-use error.

Solution: The proxy listens on port 8443 inside the container. If another service uses port 8443 on the same Docker network, change NB_PROXY_ADDRESS to a different port and update the Traefik label loadbalancer.server.port to match.

Rollback procedure

If you need to remove the proxy and revert to your previous configuration:

# Stop all services
docker compose down

# Restore your backup
cd netbird-backup-YYYYMMDD
cp docker-compose.yml ../

# Restart without the proxy
cd ..
docker compose up -d

You can also revoke the proxy token to prevent the proxy from reconnecting:

# Combined container
docker exec -it netbird-server /go/bin/netbird-server token list \
  --config <netbird-data-dir>/config.yaml
docker exec -it netbird-server /go/bin/netbird-server token revoke <token-id> \
  --config <netbird-data-dir>/config.yaml

# Multi-container
docker exec -it netbird-management /go/bin/netbird-mgmt token list
docker exec -it netbird-management /go/bin/netbird-mgmt token revoke <token-id>

Additional resources