Reverse Proxy Troubleshooting

Updated

This guide helps you diagnose and resolve common reverse proxy issues in NetBird. Follow the structured approach below to identify and fix problems quickly.

If a service behind a routing peer is unreachable in general (not specific to the reverse proxy), see Troubleshooting resource connectivity to find which layer is dropping the traffic.

If a NetBird-Only service returns 403 Forbidden, especially when curl works but a browser fails, start with 403 Forbidden with NetBird-Only Access.

Quick Diagnostics Checklist

Before diving deep, run through this quick checklist:

# 1. Is NetBird connected on the routing peer?
netbird status -d

# 2. Is the reverse proxy service showing in the dashboard?
#    Check Dashboard -> Reverse Proxy -> Services

# 3. Can the routing peer reach the target service locally?
# From the routing peer (or inside its container):
curl -sS -o /dev/null -w "%{http_code}" http://<target-ip>:<port> --max-time 5
# Docker:
docker exec <routing-peer-container> wget -qO- http://<target-ip>:<port> --timeout=5

# 4. Check proxy logs for error details
# Look for 502 status codes and connection errors:
# "Peer Not Connected" -> peer is offline or unreachable
# "Connection Error: operation timed out" -> routing/ACL issue
# "no route to host" -> invalid target IP (e.g., network address .0)

# 5. Is the target IP the same as the routing peer's IP?
# If yes, this is a known issue. Switch the target type to Peer.
# See Issue 1 below.

# 6. Is the service listening on the right interface?
# It must bind to 0.0.0.0 (or :: for IPv6), the NetBird IP,
# or the destination IP, NOT 127.0.0.1/localhost.
ss -tlnp | grep <port>
# or on macOS:
lsof -iTCP:<port> -sTCP:LISTEN

# 7. Verify the target type matches your setup
# Peer -> service runs on a machine with NetBird installed
# Host/Subnet -> service runs on a device without NetBird, reached via routing peer

If any of these fail, continue to the relevant section below.

Self-Hosted Debugging with the Proxy Debug Endpoint

Self-hosted deployments can enable a built-in debug endpoint on the proxy for deeper diagnostics. This is disabled by default.

Enabling the debug endpoint

Enable the debug endpoint by setting the environment variable or passing the flag when starting the proxy:

# Environment variable (e.g., in Docker Compose)
NB_PROXY_DEBUG_ENDPOINT=true

# Or as a CLI flag
netbird-proxy --debug-endpoint

The endpoint listens on localhost:8444 by default. To change the address:

NB_PROXY_DEBUG_ENDPOINT_ADDRESS=localhost:9090
# Or
netbird-proxy --debug-endpoint --debug-endpoint-addr localhost:9090

You can also access the debug endpoint directly in a browser at http://localhost:8444/debug for an overview of server uptime, connected clients, and service status.

Debug CLI commands

Once the debug endpoint is enabled, use the netbird-proxy debug CLI commands to inspect proxy state. All commands support a --json flag for machine-readable output and --addr to specify the debug endpoint address.

Check proxy health

netbird-proxy debug health

Returns management connection state and overall client health.

List connected clients

netbird-proxy debug clients

Shows all connected clients with their service counts and status.

Inspect a specific client

netbird-proxy debug status <account-id>

Shows detailed status for a client. You can filter results:

# Filter by peer IP
netbird-proxy debug status <account-id> --filter-by-ips 10.0.0.10

# Filter by connection status
netbird-proxy debug status <account-id> --filter-by-status connected

# Filter by connection type
netbird-proxy debug status <account-id> --filter-by-connection-type P2P

TCP ping through a client

netbird-proxy debug ping <account-id> <host> [port]

Tests TCP connectivity from the proxy through a client's network to a target host. Port defaults to 80. This is useful for confirming whether the proxy can reach a target service through a specific routing peer:

# Test if the proxy can reach a service at 10.0.0.10:32400 through the client
netbird-proxy debug ping <account-id> 10.0.0.10 32400

Set client log level

netbird-proxy debug log level <account-id> <level>

Temporarily change a client's log level for debugging. Valid levels: trace, debug, info, warn, error.

View sync response

netbird-proxy debug sync-response <account-id>

Shows the latest sync response for a client, which includes the service and peer configuration the proxy has received from the management server.

Common Issues and Solutions

Issue 1: 502 errors when routing peer forwards to its own IP

Symptoms:

  • Reverse proxy services return 502 "Peer Not Connected" or 502 "Connection Error: operation timed out"
  • The target destination IP belongs to the same machine running the routing peer
  • Other services routed through the same routing peer to different IPs on the network work fine
  • Proxy logs show timeout errors like:
proxy error: host=10.0.0.10:32400 status=502 title="Connection Error" err=connect tcp 10.0.0.10:32400: operation timed out

For example, if a routing peer runs on a machine with IP 10.0.0.10 and the reverse proxy target is http://10.0.0.10:32400, the connection will time out. However, a target pointing at http://10.0.0.50:8096 (a different machine on the same subnet) works without issue through the same routing peer.

This happens because when a reverse proxy service uses a network resource (subnet) target, the routing peer is expected to forward traffic to other hosts on the subnet, not deliver tunneled traffic back to itself. The management server does not generate the necessary ACL rules for self-targeted traffic, so the connection times out. This is expected behavior.

You can confirm this by verifying the service is reachable locally on the routing peer (outside of the tunnel):

docker exec <routing-peer-container> wget -qO- http://10.0.0.10:32400 --timeout=5

If this returns a response (even a 401 Unauthorized), the service is reachable locally but not through the tunnel, confirming the issue.

Solutions:

Change the target type from Subnet (network resource) to Peer for any service running on the same machine as the routing peer.

  1. Open the reverse proxy service in the NetBird dashboard.
  2. Edit the target.
  3. Change the target type from Host or Subnet to Peer.
  4. Select the peer that corresponds to the machine running the service (this is the same machine as your routing peer).
  5. Set the protocol and port as before (e.g., HTTP, port 32400).
  6. Save the service.

When the target type is Peer, the proxy sends traffic directly to that peer through the WireGuard tunnel. The traffic arrives on the peer's local network stack and is delivered to the listening service without any forwarding hop. This bypasses the subnet routing path entirely, so the missing ACL does not apply.

Issue 2: Service bound to localhost is unreachable

Symptoms:

  • Reverse proxy returns 502 errors even though the service is running
  • The service works when accessed locally on the machine (e.g., curl http://localhost:8080) but fails through the reverse proxy
  • Proxy logs show connection refused or timeout errors

This happens when the target service is configured to listen only on 127.0.0.1 (localhost) or ::1. Traffic arriving through NetBird comes from the WireGuard tunnel interface, not the loopback interface, so a service bound to localhost will refuse the connection.

How to check:

On the target machine, verify what address the service is listening on:

# Linux
ss -tlnp | grep <port>

# macOS
lsof -iTCP:<port> -sTCP:LISTEN

# Windows (PowerShell)
Get-NetTCPConnection -LocalPort <port> -State Listen | Select-Object LocalAddress,LocalPort

If the Local Address column shows 127.0.0.1 or ::1, the service is only reachable from the machine itself.

Solution:

Reconfigure the service to listen on one of the following:

  • 0.0.0.0 (all IPv4 interfaces) or :: (all IPv6 interfaces), the simplest option
  • The machine's NetBird IP (e.g., 100.x.y.z) if you want to restrict access to NetBird traffic only
  • The specific LAN IP used as the reverse proxy destination

Where to change this depends on the service. Look for a bind, listen, host, or address setting in the service's configuration file. For example:

# Common examples across different services
bind-address = 0.0.0.0
host: 0.0.0.0
listen_addresses = '*'
server.host: "0.0.0.0"

After changing the bind address, restart the service and verify with ss or lsof that it now listens on the correct interface.

Issue 3: Geo-restrictions block internal traffic from the management server

Symptoms:

  • Adding an external identity provider (e.g., PocketID) exposed through the reverse proxy fails with a 403 Forbidden error
  • The management server logs show repeated connector initialization failures:
ERRO [err: failed to open connector: failed to create connector : failed to get provider: 403 Forbidden: Forbidden
] idp/dex/logrus_handler.go:83: Failed to get connector
  • The IdP is accessible from a browser and from the host machine, but not from inside the management server container
  • The instance setup status remains false in the dashboard

This applies to self-hosted setups where an external identity provider (such as Authentik or PocketID) is exposed through the NetBird reverse proxy. For example, the architecture described in Self-host NetBird with Authentik. The management server (Dex) needs to reach the IdP's OIDC discovery endpoint through the reverse proxy to initialize the connector. However, traffic originating from inside the Docker network does not carry a valid geo-IP. The proxy sees an internal Docker IP instead of a public IP, which doesn't match any country, so the restriction rejects the request with a 403.

How to confirm:

Test connectivity to the IdP from inside the management server's network. Use a lightweight container attached to the same network namespace:

docker run --rm --network container:netbird-server curlimages/curl -s https:///.well-known/openid-configuration

If this returns Forbidden while the same URL works from the host:

curl -s https:///.well-known/openid-configuration

Then geo-restrictions on the reverse proxy service are blocking internal traffic.

Solution:

Remove or adjust the geo-restriction on the reverse proxy service that exposes your IdP. You can do this from the NetBird dashboard under Reverse Proxy > Services, then edit the service and remove the country restriction.

If you are locked out of the dashboard because the IdP connector cannot initialize (e.g., you already removed local users), you can modify the restriction directly in the management database:

  1. Stop all NetBird containers:
cd ~/netbird
docker compose stop
  1. Identify the service ID for your IdP service:
docker run --rm --user root -v :/data --entrypoint sqlite3 keinos/sqlite3 /data/store.db \
  "SELECT id, domain, restrictions FROM services;"
  1. Clear the geo-restriction on the IdP service:
docker run --rm --user root -v :/data --entrypoint sqlite3 keinos/sqlite3 /data/store.db \
  "UPDATE services SET restrictions='{}' WHERE id='';"
  1. Start the containers:
docker compose start
  1. Verify the connector initializes by checking the management server logs:
docker logs netbird-server --tail 20

Once you regain access to the dashboard, you can re-enable geo-restrictions on the service if needed. To prevent this issue in the future, consider one of the following:

  • Leave geo-restrictions disabled on services that the management server needs to reach internally (such as your IdP).
  • Use a separate network path for the IdP that doesn't pass through the reverse proxy's geo-restriction layer.

Issue 4: TCP service is active but connections get no response

Symptoms:

  • A TCP service shows as active and its listen port accepts connections
  • Clients connect but receive no response

Cause:

A service's status reflects the proxy's listener, not the backend: a TCP service stays active and keeps accepting connections even when nothing listens on its target port.

Solution:

Check the backend, not the service status: on the target host, confirm that the application is listening on the target port and binds to an interface the proxy can reach (step 6 of the Quick Diagnostics Checklist, and Issue 2).

Issue 5: 403 Forbidden with NetBird-Only Access

Symptoms:

  • A service with NetBird-Only Access returns 403 Forbidden even though the client is connected and belongs to an allowed peer group.
  • The same URL may work with curl or another browser, while the affected browser still fails in a private window.

NetBird supplies authorized peers with private DNS records pointing the service hostname to the proxy's NetBird address. The request must reach the proxy over the NetBird tunnel so it can identify the calling peer. Browser DNS-over-HTTPS (DoH) can bypass the system's NetBird DNS configuration. If the browser receives the public address instead, it reaches the public endpoint and fails the tunnel check. A private browsing window does not necessarily change the browser's DNS settings.

Check the connection from the affected device:

netbird status -d
curl -v --noproxy '*' https://service.example.com/

Replace service.example.com with the service hostname. The --noproxy option makes this a direct test, ignoring any HTTP proxy configured for curl. Inspect the Trying and Connected to lines: the destination should be one of the proxy cluster's NetBird addresses, rather than the public address of your proxy host or wildcard DNS record.

The browser may still use an HTTP proxy configured in its own or the system's proxy settings, even when direct curl succeeds. That proxy may reach the service through its public endpoint, causing a 403 because the request lacks the calling peer's tunnel identity. Check those proxy settings and, if your organization's policy permits, bypass the proxy for the service hostname. Changing browser DNS settings alone may not fix a request routed through an HTTP proxy.

If curl succeeds over the NetBird address but one browser fails, check that browser's DNS settings. Curl's result does not establish which resolver or destination the browser uses. If curl also resolves to the public address or cannot resolve the hostname, check that the client uses NetBird-managed DNS.

Use system DNS for the service hostname:

Firefox: Open Settings > Privacy & Security > DNS over HTTPS, then Manage Exceptions (under Advanced settings in some versions). Add the service hostname so Firefox uses system DNS for it. See Mozilla's DNS-over-HTTPS settings and exceptions.

To confirm the cause in Firefox, you can temporarily set DNS over HTTPS to Off and retry. If that fixes access, add the hostname exception and restore your preferred protection level for other sites.

Chrome (desktop): Open Settings > Privacy and security > Security. Under Advanced, turn Use secure DNS off to test with the system resolver. A custom public DNS provider does not receive the private records NetBird supplies to the client. If this restores access, keep Chrome using system DNS while accessing these private services. See Google's Secure DNS settings.

After changing either browser's settings, clear its DNS cache or fully restart it before retrying while connected to NetBird.

If any client still receives 403:

  • Confirm that the calling peer, not just its user, belongs to a service access group. See NetBird-Only access groups.
  • Check the service's CIDR allowlists and blocklists against the caller's NetBird address. These restrictions still apply to tunnel traffic.
  • If you use a custom account IP range, check the deployed proxy version's address requirements. The current tunnel authentication check accepts RFC1918 private IPv4, IPv6 ULA, and the default CGNAT range (100.64.0.0/10); addresses outside those ranges fail that check.
  • Use the access logs and backend logs to identify which component returned the 403. A backend can return 403 after proxy authentication succeeds; the status alone does not identify the cause.

Advanced Debugging with Packet Capture

If the checks above don't reveal the issue, you can use packet capture tools to verify whether traffic is actually arriving on the routing peer or target machine. This is especially useful for diagnosing routing, firewall, or NAT issues.

Linux (tcpdump)

On the routing peer or target machine, capture traffic on the relevant interface:

# Capture traffic on the WireGuard interface (wt0 is the default NetBird interface)
sudo tcpdump -i wt0 -n port <target-port>

# Capture on all interfaces to see where traffic arrives
sudo tcpdump -i any -n port <target-port>

# Save to a file for later analysis
sudo tcpdump -i wt0 -n port <target-port> -w /tmp/capture.pcap

If you see packets arriving on wt0 but no response, the issue is on the target machine (firewall, service not listening, wrong bind address). If no packets arrive at all, the issue is upstream (routing, ACLs, or peer connectivity).

macOS (tcpdump)

# NetBird uses utun interfaces on macOS. Find yours with ifconfig
sudo tcpdump -i utun4 -n port <target-port>

Windows (pktmon)

# List network interfaces to find the WireGuard/NetBird adapter
pktmon list

# Start a capture filtered by port
pktmon start --capture --pkt-size 0 -c <component-id> -t <target-port>

# Stop and convert to pcapng for Wireshark
pktmon stop
pktmon etl2pcap pktmon.etl -o capture.pcapng

Wireshark

You can also open any .pcap or .pcapng file in Wireshark for visual inspection. Use display filters to narrow down the traffic:

tcp.port == 
ip.addr == 

When to use each target type

Target typeUse case
PeerThe service runs directly on a machine that has the NetBird client installed.
Host / SubnetThe service runs on a device that does not have a NetBird client and relies on a routing peer to forward traffic to it on the local network.

Common mistakes

Pointing a subnet target at the network address instead of a host address. For example, using 10.0.0.0:8989 instead of 10.0.0.10:8989. The network address (.0) is not a valid host and will return "no route to host." Always use the specific IP of the machine running the service.