Skip to content

Latest commit

 

History

History
347 lines (266 loc) · 37.6 KB

File metadata and controls

347 lines (266 loc) · 37.6 KB

netbird-reverse-proxy

A Helm chart for deploying the NetBird Reverse Proxy, which provides HTTP and HTTPS reverse proxy functionality for NetBird-managed services.

TL;DR;

helm repo add christianhuth https://charts.christianhuth.de
helm repo update
helm install my-release christianhuth/netbird-reverse-proxy

Introduction

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

Prerequisites

  • 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 via netbird-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.

Installing the Chart

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

Uninstalling the Chart

To uninstall the my-release deployment:

helm uninstall my-release

The command removes all the Kubernetes components associated with the chart and deletes the release.

Usage Scenarios

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.

Architecture

Why this proxy can't sit behind a normal Ingress

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.

Connecting to a self-hosted management server: proxy.managementServer.address

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.SignalExchange
  • management.ManagementService
  • management.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: Exact

Why proxy.address defaults to :8443, not :443

The 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: NetBird-Only services

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.

Certificate source: proxy.tls.source

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

secret - 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: ClusterIssuer
proxy:
  tls:
    source: secret
    existingSecret: my-proxy-tls

acme (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=true so certificates survive pod restarts (otherwise every restart re-requests them and can hit Let's Encrypt rate limits)
  • keep replicaCount: 1 and leave autoscaling.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-lease when running inside Kubernetes (the default, "auto", resolves to it) and uses a coordination.k8s.io Lease to coordinate certificate issuance - even with a single replica. The chart creates the Role/RoleBinding this needs automatically (see rbac.create); without it, the proxy can never acquire the Lease and certificate issuance never even starts, with no obvious error in the logs beyond repeated unknown domain TLS 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.

Exposure: service.type=LoadBalancer

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

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

Health checks

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.

Geolocation database persistence

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.

CrowdSec integration

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

Using an Existing Secret for the Proxy Token

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

Then reference it:

helm install my-release christianhuth/netbird-reverse-proxy \
  --set proxy.managementServer.auth.existingSecret=my-netbird-proxy-secret

Values

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