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.

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. Service subdomains will be created under this domain (e.g., myapp.proxy.example.com)
  • Wildcard DNS capability - ability to create a *.proxy.example.com DNS record pointing to your server
  • Port 443 accessible - the proxy needs this for ACME TLS-ALPN-01 challenges (certificate provisioning)

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 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

Step 4: Set up DNS records

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

TypeNameContent
CNAMEproxy.example.comnetbird.example.com
CNAME*.proxy.example.comnetbird.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.proxy.example.com, dashboard.proxy.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:///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:

ProviderWhere to add the redirect URI
Auth0Application > Settings > Allowed Callback URLs
OktaApplication > General > Login redirect URIs
KeycloakClient > Settings > Valid redirect URIs
ZitadelApplication > Redirect Settings > Redirect URIs
Generic OIDCRefer to your provider's documentation

Step 3: Restart management server

Restart the management service to pick up the configuration change:

docker compose restart management

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.

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 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

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

VariableRequiredDescriptionDefault
NB_PROXY_TOKENYesAccess token generated via netbird-server token create (combined) or netbird-mgmt token create (multi-container). The proxy refuses to start without it.-
NB_PROXY_DOMAINYesBase domain for this proxy instance (e.g., proxy.example.com). Determines the domain available for services.-
NB_PROXY_MANAGEMENT_ADDRESSNoURL of your NetBird management server. The proxy connects via gRPC to register itself.https://api.netbird.io:443
NB_PROXY_ADDRESSNoAddress the proxy listens on.:8443 (Docker), :443 (binary)
NB_PROXY_ACME_CERTIFICATESNoSet to true to enable automatic TLS certificate provisioning via Let's Encrypt.false
NB_PROXY_ACME_CHALLENGE_TYPENoACME challenge type: tls-alpn-01 (port 443) or http-01 (port 80).tls-alpn-01
NB_PROXY_CERTIFICATE_FILENoTLS certificate filename within the certificate directory (for static certificate mode).tls.crt
NB_PROXY_CERTIFICATE_KEY_FILENoTLS private key filename within the certificate directory (for static certificate mode).tls.key
NB_PROXY_CERTIFICATE_DIRECTORYNoDirectory where static certificate files are stored../certs
NB_PROXY_ALLOW_INSECURENoAllow insecure (non-TLS) gRPC connection to the management server. Set to true when connecting over an internal Docker network.false
NB_PROXY_FORWARDED_PROTONoProtocol to report in the X-Forwarded-Proto header. Set to https when TLS is terminated at the proxy.-
NB_PROXY_DEBUG_LOGSNoEnable 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