Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

### Added

- **netbird**: Document STUN networking setup in README — explains why STUN
needs a separate service (UDP), and covers options for LoadBalancer,
shared static IP, and NodePort configurations (#67).

## [0.4.0] — 2026-04-09

### Changed
Expand Down
75 changes: 74 additions & 1 deletion charts/netbird/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,78 @@ dashboard:
- netbird.example.com
```

## STUN Networking

NetBird's embedded STUN server uses **UDP port 3478**, which standard HTTP
ingress controllers cannot proxy. The chart therefore creates a dedicated
Kubernetes Service (`server.stunService`) for STUN traffic, separate from
the HTTP ingress.

NetBird clients derive the STUN URI from `server.config.exposedAddress` —
the hostname in `exposedAddress` is combined with port 3478 to form
`stun:<hostname>:3478`. This means the STUN hostname **must resolve to an
IP that reaches the STUN service**.

### Option 1: Separate LoadBalancer (default)

The chart defaults to `server.stunService.type: LoadBalancer`, which
provisions a dedicated external IP for UDP traffic. Because this IP
differs from the ingress controller IP, you need a DNS record that points
to the STUN LoadBalancer:

```
netbird.example.com → Ingress IP (HTTP / gRPC / Relay)
stun.netbird.example.com → STUN LB IP (UDP 3478)
```

Retrieve the STUN external IP after deployment:

```bash
kubectl get svc <release>-server-stun -n <namespace> \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```

If you use a separate hostname for STUN you will also need to configure a
custom STUN URI in the NetBird server config so that clients connect to the
correct address.

### Option 2: Shared static IP (single DNS entry)

On cloud providers that support static IP assignment you can give the
**same IP** to both the ingress controller and the STUN LoadBalancer. A
single DNS record then serves both HTTP and UDP traffic:

```yaml
server:
stunService:
type: LoadBalancer
port: 3478
annotations:
# GKE example:
networking.gke.io/load-balancer-ip-refs: "my-static-ip"
# AWS NLB with Elastic IP:
service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-xxx"
```

Ensure your ingress controller's external Service also uses the same
static IP so that `exposedAddress` resolves to one address for all
protocols.

### Option 3: NodePort

Expose STUN on a fixed port across all cluster nodes. Useful when a cloud
LoadBalancer is not available or when nodes already have public IPs:

```yaml
server:
stunService:
type: NodePort
port: 3478
```

Point DNS at one or more node IPs. Clients will connect on the allocated
NodePort (check `kubectl get svc` for the assigned port).

## Personal Access Token (PAT) Seeding

The chart can optionally seed the database with a Personal Access Token
Expand Down Expand Up @@ -685,7 +757,8 @@ ADFS) can be tested manually:
└──────────────────────────────────────────────────────────┘
STUN Service :3478/UDP
(LoadBalancer)
(LoadBalancer — separate IP,
cannot use HTTP Ingress)
```

## Upstream Source
Expand Down
6 changes: 6 additions & 0 deletions charts/netbird/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Components:
Server (Management + Signal + Relay + STUN):
Service: {{ include "netbird.server.fullname" . }}:{{ .Values.server.service.port }}
STUN Service: {{ include "netbird.server.fullname" . }}-stun:{{ .Values.server.stunService.port }} ({{ .Values.server.stunService.type }})
NOTE: STUN uses UDP and cannot go through HTTP ingress. Ensure DNS for
your exposedAddress resolves to an IP that reaches this service.
{{- if eq .Values.server.stunService.type "LoadBalancer" }}
Retrieve the STUN external IP:
kubectl get svc {{ include "netbird.server.fullname" . }}-stun -n {{ .Release.Namespace }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
{{- end }}

{{- if .Values.server.ingress.enabled }}
Server HTTP Ingress (API + OAuth2):
Expand Down
8 changes: 7 additions & 1 deletion charts/netbird/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ server:
exposedAddress: ""

# -- List of UDP ports for the embedded STUN server.
# These ports must also be exposed via the stunService.
# These ports must also be exposed via the stunService. STUN uses UDP
# which cannot be proxied by standard HTTP ingress controllers — see
# the "STUN Networking" section in README.md for setup options.
stunPorts:
- 3478

Expand Down Expand Up @@ -407,6 +409,10 @@ server:
port: 80

# -- Dedicated service for the STUN/TURN UDP port.
# STUN uses UDP which cannot go through a standard HTTP ingress, so this
# service gets its own external IP by default (LoadBalancer). To use a
# single DNS entry, assign a shared static IP via annotations. See the
# "STUN Networking" section in README.md for detailed options.
stunService:
type: LoadBalancer
port: 3478
Expand Down
Loading