A Helm chart for deploying the NetBird Reverse Proxy, which provides HTTP and HTTPS reverse proxy functionality for NetBird-managed services.
helm repo add christianhuth https://charts.christianhuth.de
helm repo update
helm install my-release christianhuth/netbird-reverse-proxyThis chart bootstraps the NetBird Reverse Proxy on a Kubernetes cluster using the Helm package manager.
The NetBird Reverse Proxy provides reverse proxy functionality for NetBird-managed services. It always terminates TLS itself - it cannot run behind a load balancer or Gateway that terminates TLS and forwards plain HTTP to it. See the Architecture section before choosing how to expose it and where its certificate comes from.
- Kubernetes 1.19+
- A NetBird management server - either NetBird Cloud, or your own self-hosted instance (which needs a couple of extra considerations, see Connecting to a self-hosted management server)
- A proxy token (
proxy.managementServer.auth.token), generated vianetbird-server token create(or your management server's equivalent) - A domain name pointed at wherever this chart's Service ends up (
proxy.domain)
The default certificate source (proxy.tls.source=selfSigned) has no further prerequisites - it's meant for getting started quickly, not for production with real external clients. For production, you'll also need either cert-manager (proxy.tls.source=secret) or just a public DNS record and an internet-reachable proxy (proxy.tls.source=acme, no cert-manager needed) - see Usage Scenarios and Certificate source.
To install the chart with the release name my-release:
helm repo add christianhuth https://charts.christianhuth.de
helm repo update
helm install my-release christianhuth/netbird-reverse-proxy \
--set proxy.managementServer.address="https://my-netbird-management.example.com" \
--set proxy.domain="my-proxy.example.com" \
--set proxy.managementServer.auth.token="my-proxy-token"This installs the chart in its default configuration: a LoadBalancer Service and a self-signed TLS certificate generated by the chart itself, requiring no other prerequisites. This default is fine for testing, but not for a production setup with real external clients - see Architecture for switching to a cert-manager-issued or ACME certificate. The Values section lists the values that can be configured during installation.
Tip: List all releases using
helm list
To uninstall the my-release deployment:
helm uninstall my-releaseThe command removes all the Kubernetes components associated with the chart and deletes the release.
Quick recipes for common setups. Each builds on the previous one - see Architecture for the reasoning behind each value if you want it.
Just evaluating the chart - see Installing the Chart above. The defaults (proxy.tls.source=selfSigned, service.type=LoadBalancer) need nothing beyond a token and a domain.
Production, using NetBird Cloud - leave proxy.managementServer.address unset (it defaults to NetBird Cloud), and switch to a real certificate source. acme is the simpler option since it needs no cert-manager dependency:
proxy:
domain: "my-proxy.example.com"
managementServer:
auth:
token: "my-proxy-token" # or auth.existingSecret, see below
tls:
source: acme
persistence:
enabled: true
storageClassName: "my-storage-class"Prefer cert-manager instead? Set proxy.tls.source: secret with a wildcard Certificate (*.my-proxy.example.com) instead of the tls/persistence block above - see Certificate source for why it needs to be a wildcard.
Production, using a self-hosted management server - same as above, plus point proxy.managementServer.address at it:
proxy:
domain: "my-proxy.example.com"
managementServer:
address: "https://netbird-grpc.example.com"
auth:
token: "my-proxy-token"
tls:
source: acme
persistence:
enabled: true
storageClassName: "my-storage-class"If that management server sits behind a Gateway API GRPCRoute (or Traefik, or any other gRPC-aware router), double check it routes management.ProxyService in addition to the services a "normal" NetBird deployment needs - this is the single most common thing that breaks here, and the proxy's pod will simply stay at 0/1 Ready with no obvious error if it's missing. See Connecting to a self-hosted management server for the exact symptom and fix.
The NetBird Reverse Proxy always terminates TLS itself for client-facing traffic - there is no plain-HTTP listener it can fall back to. This rules out a standard Kubernetes Ingress or a Gateway API HTTPRoute attached to an HTTPS listener, because both terminate TLS before forwarding the request, and would hand the proxy decrypted HTTP it cannot process. NetBird's own self-hosted docs call this out explicitly: they require Traefik configured for TLS passthrough in front of the proxy, specifically because passthrough forwards the raw, still-encrypted TLS bytes based on the SNI hostname alone, leaving the proxy to do the actual termination.
A plain Kubernetes Service (ClusterIP, NodePort, or LoadBalancer) never terminates TLS - it only ever forwards TCP - so it is passthrough by definition and needs no special configuration. This is why this chart's default exposure is a LoadBalancer Service rather than an Ingress.
There are therefore two independent choices to make: where the certificate comes from, and how the proxy is reached from outside the cluster.
If your NetBird management server is self-hosted and sits behind a gRPC-aware router or gateway (Traefik, a Gateway API GRPCRoute, etc.), that router needs to route three gRPC services to the management backend, not just the two a "normal" NetBird deployment needs:
signalexchange.SignalExchangemanagement.ManagementServicemanagement.ProxyService- used specifically for the proxy's connection to the management server, and easy to miss because regular NetBird clients (desktop/mobile apps, agents) never call it - only this reverse proxy does.
If management.ProxyService isn't routed, the proxy's pod will stay stuck at 0/1 Ready and its logs will repeat:
management connection failed, retrying in ...: mapping stream: receive msg: rpc error: code = Unimplemented desc =
That Unimplemented with an empty description is the router (Envoy, Traefik, etc.) rejecting a gRPC method it has no route for - the management server itself does implement management.ProxyService, it's just not reachable. For a Gateway API GRPCRoute, add a third rule alongside the existing two, pointing at the same backend:
rules:
- backendRefs:
- name: netbird-server-grpc # whatever your management gRPC Service is named
port: 80
matches:
- method:
service: management.ProxyService
type: ExactThe proxy's container image runs as a non-root user, and binding a port below 1024 (like 443) as non-root requires the NET_BIND_SERVICE capability - which may not be grantable on every cluster (some admission policies or sandboxed runtimes block it even when correctly requested). To avoid that dependency entirely, proxy.address defaults to the binary's own non-privileged default, :8443. This only changes where the proxy listens inside the container - service.https.port still defaults to the conventional 443 externally: the Service's https port has a named targetPort that always points at whatever port proxy.address resolves to, so clients still connect to https://your-domain with no port suffix needed.
This also holds for ACME's tls-alpn-01 challenge (proxy.tls.source=acme, see below): the CA only needs port 443 to be reachable on your public IP, and the Service already forwards external 443 to proxy.address internally - so the non-privileged default works there too, with no need to bind a privileged port directly. If you do need the container to bind a privileged port itself for some other reason, set proxy.address to a privileged port and add the NET_BIND_SERVICE capability under securityContext.capabilities.add.
proxy.private (NB_PROXY_PRIVATE) advertises this proxy cluster's support for "NetBird-Only" private services - an opt-in, per-service access-control feature you enable per-service in the NetBird UI/API, not a network-exposure setting. Most deployments don't need it, so it defaults to false. Enabling it makes the proxy additionally set up a hardcoded :80/:443 listener pair for such services, regardless of what proxy.address is set to - so if you enable it, both of those ports need to be bindable (see the NET_BIND_SERVICE note above). The chart automatically adds the http (80) Service and container port whenever proxy.private=true or ACME http-01 (below) is in use - in every other configuration nothing listens on port 80, so the chart omits it entirely.
Before choosing a source, it matters what you're actually securing: services you expose through the NetBird UI/API each get their own auto-generated, UUID-prefixed subdomain under proxy.domain (e.g. <uuid>.my-proxy.example.com) - clients reach that hostname, not proxy.domain itself. proxy.domain only needs its own valid certificate if you expose a service at the bare domain too.
selfSigned (default) - the chart generates a self-signed certificate for proxy.domain and stores it in a Secret it manages itself, with no external dependency at all (no cert-manager, no internet access for ACME) - so helm install works out of the box. The generated certificate is re-used across upgrades (the chart looks up the existing Secret before generating a new one), but is never automatically rotated - delete the generated Secret yourself to force regeneration. It only covers proxy.domain itself, not the per-service UUID subdomains - fine for CI/testing pipelines or fully private/internal deployments where clients don't need a publicly-trusted certificate. Switch to secret or acme below for a production setup with real external clients.
proxy:
domain: "my-proxy.example.com"
tls:
source: selfSignedsecret - mount an existing Kubernetes Secret of type kubernetes.io/tls into the certificate directory. This is the natural fit if you already run cert-manager: a cert-manager Certificate issues into a Secret with keys tls.crt / tls.key, which are exactly the filenames the proxy expects by default. The proxy watches the certificate files and hot-reloads them, so cert-manager's automatic renewals are picked up without restarting the pod - no extra glue required. Since this is a single static certificate, if you're exposing services on their auto-generated UUID subdomains (the common case), it needs to be a wildcard certificate (*.my-proxy.example.com) to cover them - a certificate for just the bare domain will leave those services with a certificate mismatch.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-proxy-tls
spec:
secretName: my-proxy-tls
dnsNames:
- "*.my-proxy.example.com"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuerproxy:
tls:
source: secret
existingSecret: my-proxy-tlsacme (recommended for production) - the proxy requests and renews its own certificates directly from Let's Encrypt (NB_PROXY_ACME_CERTIFICATES=true). Unlike the other two sources, this isn't a single static certificate: the proxy obtains one on demand, per actual hostname, the first time something connects for a domain the management server has told it about (via the mapping sync) - including each service's auto-generated UUID subdomain, automatically, with no wildcard needed. proxy.domain itself only gets a certificate this way if it's also an exposed service; connections to the bare domain otherwise correctly fail with unknown domain rather than serving a certificate for a domain nothing has authorized. No cert-manager dependency, but proxy.domain must be set, and the proxy must be reachable from the internet for the challenge. The default challenge type, tls-alpn-01, validates on whatever port is externally reachable as 443 - this chart's Service already forwards external 443 to proxy.address internally (see above), so the chart's non-privileged :8443 default works fine; no need to change proxy.address. Since certificate state lives on local disk inside the pod:
- enable
persistence.enabled=trueso certificates survive pod restarts (otherwise every restart re-requests them and can hit Let's Encrypt rate limits) - keep
replicaCount: 1and leaveautoscaling.enabled=false- certificate state is not shared between pods, so each replica would request its own certificates independently - the proxy auto-detects
proxy.tls.certLockMethod=k8s-leasewhen running inside Kubernetes (the default, "auto", resolves to it) and uses acoordination.k8s.ioLease to coordinate certificate issuance - even with a single replica. The chart creates the Role/RoleBinding this needs automatically (seerbac.create); without it, the proxy can never acquire the Lease and certificate issuance never even starts, with no obvious error in the logs beyond repeatedunknown domainTLS handshake warnings.
proxy:
domain: "my-proxy.example.com"
tls:
source: acme
persistence:
enabled: true
storageClassName: "my-storage-class"proxy.tls.acme.challengeType=http-01 is also available as an alternative to the default tls-alpn-01 - it validates on proxy.tls.acme.address (port 80 by default) instead, needing service.http.port (80) reachable from the internet instead of 443. Some ACME providers (e.g. ZeroSSL) also require External Account Binding - set proxy.tls.acme.eab.kid and proxy.tls.acme.eab.existingSecret (a Secret with a hmacKey key) for those.
This is the normal way to run this proxy: the Service gets its own external IP directly from your cloud provider or MetalLB, and since a Service is always L4, passthrough is automatic - no special configuration needed. Pair it with external-dns to publish a DNS record for the assigned IP automatically, matching the hostname configured in proxy.domain / your certificate:
service:
type: LoadBalancer
annotations:
external-dns.alpha.kubernetes.io/hostname: my-proxy.example.comThis chart intentionally does not support Ingress or Gateway API routes: both terminate TLS by design, which - as explained above - this proxy cannot run behind. If you need to multiplex several TLS-terminating backends behind a single shared IP via SNI, that is a deliberate, more advanced setup (Gateway API TLSRoute with a Passthrough listener) that is outside the scope of this chart - the dedicated LoadBalancer above is the supported path.
The proxy exposes a combined health endpoint at proxy.health.address (:8080 by default - the chart deliberately binds this to all interfaces, unlike the binary's own localhost:8080 default, so that standard Kubernetes httpGet probes can reach it). This chart wires up livenessProbe/readinessProbe/startupProbe against it automatically:
/healthz/live(liveness) - a pure process check, independent of the management server connection, so a transient management-server outage won't cause Kubernetes to restart the pod./healthz/ready(readiness) - gated on the management server connection; the pod is taken out of the Service's endpoints while disconnected./healthz/startup(startup) - the full check set, including initial sync completion.
The same port also serves Prometheus metrics at /metrics.
A separate, more sensitive debug endpoint (connected clients, runtime stats, packet capture) can be enabled via proxy.debugEndpoint.enabled - it stays bound to localhost and is not exposed via the Service even when enabled; reach it with kubectl exec/port-forward.
The proxy downloads a GeoLite2 city database to proxy.geoDataDir from pkgs.netbird.io at startup if it's missing. By default this isn't backed by a volume, so it's re-downloaded on every pod restart. If you want to avoid that, or are running without egress to pkgs.netbird.io, mount a volume at that path yourself via extraVolumes/extraVolumeMounts.
Set proxy.crowdsec.apiUrl to enable IP reputation checks against a CrowdSec LAPI instance. Provide the bouncer API key via proxy.crowdsec.auth.apiKey or, preferably, proxy.crowdsec.auth.existingSecret (a Secret with an apiKey key).
Instead of providing the token in plain text via proxy.managementServer.auth.token, create a Kubernetes Secret first:
kubectl create secret generic my-netbird-proxy-secret --from-literal=token=my-proxy-tokenThen reference it:
helm install my-release christianhuth/netbird-reverse-proxy \
--set proxy.managementServer.auth.existingSecret=my-netbird-proxy-secret| Key | Type | Default | Description |
|---|---|---|---|
| affinity | object | {} |
Affinity settings for pod assignment |
| autoscaling.enabled | bool | false |
Enable Horizontal POD autoscaling. Note: Not recommended when using built-in ACME certificates (proxy.tls.source=acme) as certificate state is not shared between pods. |
| autoscaling.maxReplicas | int | 100 |
Maximum number of replicas |
| autoscaling.minReplicas | int | 1 |
Minimum number of replicas |
| autoscaling.targetCPUUtilizationPercentage | int | 80 |
Target CPU utilization percentage |
| extraEnv | list | [] |
additional environment variables to be added to the pods |
| extraEnvFrom | list | [] |
additional environment variables from ConfigMaps or Secrets |
| extraVolumeMounts | list | [] |
additional volume mounts to add to the container, paired with extraVolumes |
| extraVolumes | list | [] |
additional volumes to add to the pod. Useful for example to mount a pre-populated GeoLite2 database (proxy.geoDataDir) or a wildcard certificate directory, neither of which this chart provisions directly. |
| fullnameOverride | string | "" |
String to fully override "netbird-reverse-proxy.fullname" |
| image.pullPolicy | string | "Always" |
image pull policy |
| image.registry | string | "docker.io" |
image registry |
| image.repository | string | "netbirdio/reverse-proxy" |
image repository |
| image.tag | string | "" |
Overrides the image tag whose default is the chart appVersion. |
| imagePullSecrets | list | [] |
If defined, uses a Secret to pull an image from a private Docker registry or repository. |
| livenessProbe.failureThreshold | int | 3 |
Failure threshold for livenessProbe |
| livenessProbe.initialDelaySeconds | int | 0 |
Initial delay seconds for livenessProbe |
| livenessProbe.path | string | "/healthz/live" |
HTTP path for livenessProbe, queried against proxy.health.address. Defaults to a pure process check, independent of the management server connection - see proxy.health in README.md for the other paths the proxy exposes (e.g. /healthz/ready, /healthz/startup). |
| livenessProbe.periodSeconds | int | 10 |
Period seconds for livenessProbe |
| livenessProbe.successThreshold | int | 1 |
Success threshold for livenessProbe |
| livenessProbe.timeoutSeconds | int | 1 |
Timeout seconds for livenessProbe |
| nameOverride | string | "" |
Provide a name in place of netbird-reverse-proxy |
| nodeSelector | object | {} |
Node labels for pod assignment |
| persistence.accessModes | list | ["ReadWriteOnce"] |
The desired access modes the volume should have |
| persistence.annotations | object | {} |
Annotations to be added to the PersistentVolumeClaim |
| persistence.enabled | bool | false |
Enable persistent storage for the certs directory. Only relevant when proxy.tls.source=acme; recommended in that case to avoid re-requesting certificates on every pod restart. Has no effect for proxy.tls.source=secret or selfSigned, since both mount the certificate directly from a Secret rather than writing to the certs directory. |
| persistence.existingClaim | string | "" |
Provide an existing PersistentVolumeClaim. If set, no new PVC will be created. |
| persistence.resources | object | {"requests":{"storage":"1Gi"}} |
Represents the minimum and maximum resources the volume should have |
| persistence.storageClassName | string | "" |
Name of the StorageClass required by the claim |
| podAnnotations | object | {} |
Annotations to be added to the pods |
| podSecurityContext | object | see values.yaml | pod-level security context |
| proxy.address | string | ":8443" |
Address the proxy's main TLS listener binds to (NB_PROXY_ADDRESS). The proxy always terminates TLS itself for client-facing traffic - there is no plain-HTTP mode for this listener. Defaults to the binary's own non-privileged default port, so the container never needs to bind a privileged port (<1024) itself; service.https.port still exposes the conventional 443 externally - the Service's named targetPort maps it to whatever numeric port is set here. Must be in ":PORT" form (no host part) - the chart derives the container's port from it. |
| proxy.crowdsec.apiUrl | string | "" |
CrowdSec LAPI URL for IP reputation checks (NB_PROXY_CROWDSEC_API_URL). Leave empty to disable the CrowdSec integration entirely. |
| proxy.crowdsec.auth.apiKey | string | "" |
CrowdSec bouncer API key (NB_PROXY_CROWDSEC_API_KEY). It is strongly recommended to use proxy.crowdsec.auth.existingSecret instead of providing the key here. |
| proxy.crowdsec.auth.existingSecret | string | "" |
Name of an existing secret containing the CrowdSec bouncer API key. If set, proxy.crowdsec.auth.apiKey will be ignored. The secret must contain a key named apiKey. |
| proxy.debugEndpoint.address | string | "localhost:8444" |
Address for the debug HTTP endpoint (NB_PROXY_DEBUG_ENDPOINT_ADDRESS). Only relevant when proxy.debugEndpoint.enabled=true. |
| proxy.debugEndpoint.enabled | bool | false |
Enable the debug HTTP endpoint (NB_PROXY_DEBUG_ENDPOINT), which exposes richer introspection (connected clients, runtime stats, packet capture) than the health endpoint. Disabled by default since it's a more sensitive surface; left bound to localhost (not exposed via the Service) even when enabled - reach it via kubectl exec/port-forward. |
| proxy.domain | string | "" |
Base domain the proxy serves (NB_PROXY_DOMAIN). Services exposed via the NetBird UI/API typically get their own auto-generated, UUID-prefixed subdomain under this base domain (e.g. ".<proxy.domain>") rather than using proxy.domain itself - see proxy.tls.source for how that affects which domain(s) actually need a valid certificate. |
| proxy.forwardedProto | string | "auto" |
Value to set for the X-Forwarded-Proto header sent to backends (NB_PROXY_FORWARDED_PROTO). One of "auto", "http", or "https". |
| proxy.geoDataDir | string | "/var/lib/netbird/geolocation" |
Directory for the GeoLite2 city database (NB_PROXY_GEO_DATA_DIR), auto-downloaded from pkgs.netbird.io at startup if missing. Not backed by a volume by default, so it is re-downloaded on every pod restart; mount a volume here via extraVolumes/extraVolumeMounts (e.g. a small PVC) if you want to avoid that, or are running in an environment without egress to pkgs.netbird.io. |
| proxy.health.address | string | ":8080" |
Address for the health probe endpoint (NB_PROXY_HEALTH_ADDRESS), used for this chart's liveness/readiness/startup probes. Deliberately overrides the binary's own "localhost:8080" default to bind all interfaces within the pod's network namespace, since the kubelet's httpGet probes connect to the Pod IP and cannot reach a port bound only to 127.0.0.1 inside the container. This is not exposed via the Service, so it stays unreachable from outside the pod's network namespace regardless. |
| proxy.logLevel | string | "info" |
Log level for the proxy (NB_PROXY_LOG_LEVEL) |
| proxy.managementServer.address | string | "" |
Address of the NetBird management server (NB_PROXY_MANAGEMENT_ADDRESS). Leave empty to use the binary's own default, NetBird Cloud (https://api.netbird.io:443) - only set this if you run your own self-hosted management server. |
| proxy.managementServer.allowInsecure | bool | true |
Allow an insecure (non-TLS) gRPC connection to the management server (NB_PROXY_ALLOW_INSECURE). |
| proxy.managementServer.auth.existingSecret | string | "" |
Name of an existing secret containing the proxy's management server token. If set, proxy.managementServer.auth.token will be ignored. The secret must contain a key named token. |
| proxy.managementServer.auth.token | string | "" |
Token used to authenticate this proxy against the management server (NB_PROXY_TOKEN), generated via netbird-server token create (or your management server's equivalent). It is strongly recommended to use proxy.managementServer.auth.existingSecret instead of providing the token here. |
| proxy.maxDialTimeout | string | "" |
Cap the per-service backend dial timeout (NB_PROXY_MAX_DIAL_TIMEOUT), e.g. "5s". Empty means no cap (binary default). |
| proxy.maxSessionIdleTimeout | string | "" |
Cap the per-service session idle timeout (NB_PROXY_MAX_SESSION_IDLE_TIMEOUT), e.g. "30m". Empty means no cap (binary default). |
| proxy.private | bool | false |
Advertises this proxy cluster's support for "NetBird-Only" private services (NB_PROXY_PRIVATE) - see https://netbird.io/knowledge-hub/netbird-only-private-services. This is an opt-in, per-service access-control feature configured in the NetBird UI/API, not a network-exposure setting - most deployments don't need it. Enabling it makes the proxy set up an additional, hardcoded :80/:443 listener pair for such services regardless of proxy.address, which requires NET_BIND_SERVICE for both ports rather than just the one proxy.address points at. |
| proxy.proxyProtocol | bool | false |
Enable PROXY protocol on the proxy's TCP listeners to preserve real client IPs behind an L4 load balancer that supports it, e.g. an AWS NLB with proxy protocol enabled (NB_PROXY_PROXY_PROTOCOL). |
| proxy.requireSubdomain | bool | false |
Require a subdomain label in front of proxy.domain for routed services (NB_PROXY_REQUIRE_SUBDOMAIN). |
| proxy.tls.acme.address | string | ":80" |
HTTP address for ACME http-01 challenges (NB_PROXY_ACME_ADDRESS). Only used when proxy.tls.acme.challengeType=http-01. |
| proxy.tls.acme.challengeType | string | "tls-alpn-01" |
ACME challenge type when proxy.tls.source=acme (NB_PROXY_ACME_CHALLENGE_TYPE). "tls-alpn-01" (default) validates on whatever port is externally reachable as 443 - this chart's Service already forwards that to proxy.address internally, so it works fine at proxy.address's non-privileged default. "http-01" instead validates on proxy.tls.acme.address (default :80), requiring service.http.port (80) to actually reach the pod from the internet. |
| proxy.tls.acme.directory | string | "" |
Override the ACME directory URL (NB_PROXY_ACME_DIRECTORY), e.g. to use Let's Encrypt's staging environment while testing (to avoid production rate limits) or a different ACME CA. Defaults to the Let's Encrypt production directory when empty. |
| proxy.tls.acme.eab.existingSecret | string | "" |
Name of an existing Secret containing the ACME EAB HMAC key (NB_PROXY_ACME_EAB_HMAC_KEY). The secret must contain a key named hmacKey. Required together with proxy.tls.acme.eab.kid if your ACME CA requires EAB. |
| proxy.tls.acme.eab.kid | string | "" |
ACME External Account Binding key ID (NB_PROXY_ACME_EAB_KID), required by some ACME CAs (e.g. ZeroSSL, some enterprise CAs). Leave empty if your CA doesn't require EAB. |
| proxy.tls.certLockMethod | string | "auto" |
Certificate coordination method for multi-replica deployments (NB_PROXY_CERT_LOCK_METHOD). One of "auto", "flock", or "k8s-lease". "flock" only works with a single replica (it coordinates via a local file lock). "k8s-lease" coordinates across replicas using a Kubernetes Lease object - the chart auto-detects and uses this inside Kubernetes (the default, "auto", resolves to it), and creates the RBAC it needs automatically (see rbac.create). Note that the Lease only coordinates issuance timing (so replicas don't race to request the same certificate); it does not share already-issued certificate state between replicas - see the multi-replica note under proxy.tls.source=acme above. |
| proxy.tls.certificateDirectory | string | "/certs" |
Directory where TLS certificate files are read from / stored (NB_PROXY_CERTIFICATE_DIRECTORY). The proxy expects tls.crt and tls.key inside this directory (NB_PROXY_CERTIFICATE_FILE / NB_PROXY_CERTIFICATE_KEY_FILE default to those names) - which is also the default key layout of a Kubernetes Secret of type kubernetes.io/tls, so cert-manager output can be mounted as-is. |
| proxy.tls.certificateFile | string | "tls.crt" |
TLS certificate filename within proxy.tls.certificateDirectory (NB_PROXY_CERTIFICATE_FILE). Only needs changing if you mount a Secret/volume that uses different filenames. |
| proxy.tls.certificateKeyFile | string | "tls.key" |
TLS private key filename within proxy.tls.certificateDirectory (NB_PROXY_CERTIFICATE_KEY_FILE). Only needs changing if you mount a Secret/volume that uses different filenames. |
| proxy.tls.existingSecret | string | "" |
Name of an existing Secret of type kubernetes.io/tls containing tls.crt and tls.key. Required when proxy.tls.source=secret. |
| proxy.tls.source | string | "selfSigned" |
Source of the TLS certificate served to clients. One of: - "selfSigned" (default): the chart generates a self-signed certificate for proxy.domain (required) and stores it in a Secret it manages itself - no external dependency at all, so helm install works out of the box. The generated certificate is stable across upgrades (re-used if the Secret already exists) but is not renewed automatically; delete the generated Secret to force regeneration. Fine for CI/testing or fully private/internal deployments where clients don't need a publicly-trusted certificate - switch to "secret" for a production setup with real external clients. - "secret": mount an existing Kubernetes Secret of type kubernetes.io/tls (e.g. issued by a cert-manager Certificate resource) into proxy.tls.certificateDirectory. The proxy hot-reloads the certificate when the file changes on disk, so cert-manager renewals (which update the Secret, which kubelet syncs to the mounted volume) are picked up without restarting the pod. Recommended for production with real, publicly-trusted certificates. - "acme": the proxy requests and renews its own certificates via ACME (Let's Encrypt) - sets NB_PROXY_ACME_CERTIFICATES=true. Requires the proxy to be reachable from the internet and proxy.domain to be set. Unlike "secret"/"selfSigned", this isn't a single static certificate - the proxy obtains one on demand per actual hostname the management server reports (including each exposed service's auto-generated subdomain), the first time a client connects for it. The default ACME challenge type (tls-alpn-01) validates on whatever port is externally reachable as 443 - this chart's Service already forwards external 443 to proxy.address internally, so the non-privileged ":8443" default works fine; no need to change proxy.address. Enable persistence.enabled to avoid re-requesting certificates (and risking Let's Encrypt rate limits) on every pod restart. |
| proxy.trustedProxies | string | "" |
Comma-separated list of trusted upstream proxy CIDR ranges (NB_PROXY_TRUSTED_PROXIES), e.g. "10.0.0.0/8,192.168.1.1". Only set this if there is an L4 load balancer or proxy in front of the Service that the proxy should trust for client-IP forwarding headers. Empty by default. |
| rbac.create | bool | true |
Specifies whether a Role/RoleBinding should be created for proxy.tls.certLockMethod=k8s-lease coordination (only rendered when proxy.tls.source=acme and proxy.tls.certLockMethod is "auto" or "k8s-lease" - "auto" resolves to "k8s-lease" automatically inside Kubernetes, so this applies to the default too). Without it, the proxy can't create/update the Lease it uses to coordinate certificate issuance, and ACME certificate acquisition will silently never complete. Disable this if you manage this RBAC yourself. |
| readinessProbe.failureThreshold | int | 3 |
Failure threshold for readinessProbe |
| readinessProbe.initialDelaySeconds | int | 0 |
Initial delay seconds for readinessProbe |
| readinessProbe.path | string | "/healthz/ready" |
HTTP path for readinessProbe, queried against proxy.health.address. Defaults to /healthz/ready, which is gated on the management server connection - the pod is taken out of the Service's endpoints while disconnected. Set this to /healthz/live instead (e.g. in a CI pipeline with no real management server reachable) if you want readiness to ignore management connectivity entirely. |
| readinessProbe.periodSeconds | int | 10 |
Period seconds for readinessProbe |
| readinessProbe.successThreshold | int | 1 |
Success threshold for readinessProbe |
| readinessProbe.timeoutSeconds | int | 1 |
Timeout seconds for readinessProbe |
| replicaCount | int | 1 |
Number of replicas. Note: When using built-in ACME certificates (proxy.tls.source=acme), running more than one replica is not recommended as certificate state is not shared between pods. |
| resources | object | {} |
Resource limits and requests for the controller pods. |
| revisionHistoryLimit | int | 10 |
The number of old ReplicaSets to retain |
| securityContext | object | see values.yaml | container-level security context. The image's default user is the non-numeric "netbird" user (uid 1000, gid 1000) - runAsUser/ runAsGroup must be set explicitly to numeric values, otherwise the kubelet cannot verify runAsNonRoot against a non-numeric image user and refuses to start the container. No capabilities need to be added by default: proxy.address defaults to the non-privileged ":8443", so the container never binds a port below 1024 itself. If you set proxy.address to a privileged port, enable proxy.private, or otherwise need port 80/443 bound directly inside the container, add the NET_BIND_SERVICE capability here yourself. |
| service.annotations | object | {} |
Additional annotations for the Service resource. For example, to let external-dns manage a DNS record pointing at this Service's LoadBalancer IP: external-dns.alpha.kubernetes.io/hostname: my-proxy.example.com |
| service.http.port | int | 80 |
Kubernetes service port for HTTP traffic. The chart only creates this Service port (and the matching container port) when it's actually needed: proxy.private=true (per-account :80/:443 listeners) or proxy.tls.source=acme with proxy.tls.acme.challengeType=http-01. In every other configuration, nothing listens on port 80 at all, so it's omitted entirely. |
| service.https.port | int | 443 |
Kubernetes service port for HTTPS traffic. Always forwards to the container port derived from proxy.address (see there), regardless of this external port number. |
| service.type | string | "LoadBalancer" |
Kubernetes service type. LoadBalancer exposes the proxy directly with its own external IP; since a Service always operates at L4, TLS is never terminated by the Service and passes through to the proxy untouched. Combine with external-dns (see service.annotations) to automatically publish a DNS record for the assigned IP. |
| serviceAccount.annotations | object | {} |
Annotations to add to the service account |
| serviceAccount.create | bool | true |
Specifies whether a service account should be created |
| serviceAccount.name | string | "" |
The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
| startupProbe.failureThreshold | int | 60 |
Failure threshold for startupProbe. Defaults high (60 x periodSeconds=5s = 5 minutes) to comfortably cover the GeoLite2 database download the proxy performs at startup before it's ready to serve. |
| startupProbe.initialDelaySeconds | int | 0 |
Initial delay seconds for startupProbe |
| startupProbe.path | string | "/healthz/startup" |
HTTP path for startupProbe, queried against proxy.health.address. Defaults to /healthz/startup, the full check set including initial sync completion. |
| startupProbe.periodSeconds | int | 5 |
Period seconds for startupProbe |
| startupProbe.successThreshold | int | 1 |
Success threshold for startupProbe |
| startupProbe.timeoutSeconds | int | 1 |
Timeout seconds for startupProbe |
| tolerations | list | [] |
Toleration labels for pod assignment |
Specify each parameter using the --set key=value[,key=value] argument to helm install.
Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example,
helm install my-release -f values.yaml christianhuth/netbird-reverse-proxy