Compare commits

...

1 Commits

Author SHA1 Message Date
jnfrati
8e05834384 [infra] Unify self-hosted deployment paths in getting-started.sh
Make getting-started.sh the single entry point for all self-hosted
deployments. The wizard now asks two independent questions - identity
provider (built-in vs standalone OIDC) and architecture (combined
netbird-server vs separate management/signal/relay containers) - and
renders the matching Docker Compose deployment with full reverse-proxy
parity (built-in Traefik, external Traefik, Nginx, NPM, Caddy, manual).

Highlights:
- setup.env contract: every wizard answer is persisted; --non-interactive
  re-renders idempotently from the file (IaC), --render-only generates
  without starting services. Secrets are generated once and appended so
  re-renders never rotate them.
- Split architecture renders a modern management.json (embedded Dex or
  external OIDC via PKCE), drops coturn entirely (the relay container
  serves STUN via NB_ENABLE_STUN), and optionally adds a PostgreSQL
  container when the postgres engine is selected without a DSN.
- The standalone-IdP path is framed around its real differentiator:
  multi-account support. The built-in IdP supports external SSO
  connectors but enforces single account mode.
- configure.sh, getting-started-with-dex.sh and
  getting-started-with-zitadel.sh print deprecation banners; their
  templates are frozen pending removal.
- New tests/test-render.sh validates all 8 combos (JSON validity,
  compose config, idempotent re-render, combined+external rejection)
  and runs in CI as the test-render-matrix job.
2026-06-11 13:34:30 +02:00
7 changed files with 2045 additions and 212 deletions

View File

@@ -16,6 +16,20 @@ concurrency:
cancel-in-progress: true
jobs:
test-render-matrix:
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get install -y jq
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run getting-started.sh render tests
run: bash infrastructure_files/tests/test-render.sh
test-docker-compose:
runs-on: ubuntu-latest
strategy:

View File

@@ -1,6 +1,20 @@
#!/bin/bash
set -e
echo "**********************************************************************************"
echo "* DEPRECATION NOTICE *"
echo "* *"
echo "* configure.sh and its templates are deprecated and will be removed in a future *"
echo "* release. Use getting-started.sh instead, which now covers multi-container *"
echo "* deployments with your own OIDC provider: *"
echo "* *"
echo "* ./getting-started.sh # interactive wizard *"
echo "* ./getting-started.sh --non-interactive # render from setup.env (IaC) *"
echo "* *"
echo "* Migration guide: https://docs.netbird.io/selfhosted/selfhosted-guide *"
echo "**********************************************************************************"
echo ""
if ! which curl >/dev/null 2>&1; then
echo "This script uses curl fetch OpenID configuration from IDP."
echo "Please install curl and re-run the script https://curl.se/"

View File

@@ -5,6 +5,15 @@ set -e
# NetBird Getting Started with Dex IDP
# This script sets up NetBird with Dex as the identity provider
echo "**********************************************************************************"
echo "* DEPRECATION NOTICE *"
echo "* *"
echo "* getting-started-with-dex.sh is deprecated and will be removed in a future *"
echo "* release. The built-in identity provider IS Dex, embedded in the NetBird *"
echo "* server - no separate container needed. Use getting-started.sh instead. *"
echo "**********************************************************************************"
echo ""
# Sed pattern to strip base64 padding characters
SED_STRIP_PADDING='s/=//g'

View File

@@ -2,6 +2,17 @@
set -e
echo "**********************************************************************************"
echo "* DEPRECATION NOTICE *"
echo "* *"
echo "* getting-started-with-zitadel.sh is deprecated and will be removed in a future *"
echo "* release. Use getting-started.sh instead: pick the built-in IdP for the *"
echo "* simplest setup, or choose 'your own OIDC provider' to connect a Zitadel *"
echo "* instance you manage (see *"
echo "* https://docs.netbird.io/selfhosted/identity-providers/zitadel). *"
echo "**********************************************************************************"
echo ""
handle_request_command_status() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2

File diff suppressed because it is too large Load Diff

View File

@@ -1,117 +1,111 @@
## example file, you can copy this file to setup.env and update its values
##
# NetBird deployment configuration - example file
#
# Copy to setup.env, fill in the values, and render the deployment with:
# ./getting-started.sh --non-interactive
#
# Running ./getting-started.sh without flags starts an interactive wizard that
# writes this file for you. Re-rendering from the same file is idempotent, so
# it is safe to keep setup.env under configuration management (IaC).
#
# NOTE: the legacy configure.sh variable set (NETBIRD_TURN_DOMAIN, coturn,
# per-service ports, NETBIRD_DISABLE_LETSENCRYPT, ...) is deprecated together
# with configure.sh. See https://docs.netbird.io/selfhosted/selfhosted-guide
# Image tags
# you can force specific tags for each component; will be set to latest if empty
NETBIRD_DASHBOARD_TAG=""
NETBIRD_SIGNAL_TAG=""
NETBIRD_MANAGEMENT_TAG=""
COTURN_TAG=""
NETBIRD_RELAY_TAG=""
# Dashboard domain. e.g. app.mydomain.com
# Your NetBird domain, e.g. netbird.mydomain.com (required)
NETBIRD_DOMAIN=""
# TURN server domain. e.g. turn.mydomain.com
# if not specified it will assume NETBIRD_DOMAIN
NETBIRD_TURN_DOMAIN=""
# Deployment architecture:
# combined - single netbird-server container (management + signal + relay + STUN)
# split - separate management, signal and relay containers (for scale-out,
# required when using your own OIDC provider)
NETBIRD_ARCHITECTURE="combined"
# TURN server public IP address
# required for a connection involving peers in
# the same network as the server and external peers
# usually matches the IP for the domain set in NETBIRD_TURN_DOMAIN
NETBIRD_TURN_EXTERNAL_IP=""
# Identity provider:
# embedded - built-in IdP with local users (simplest)
# external - your own OIDC provider (requires NETBIRD_ARCHITECTURE=split)
NETBIRD_IDP_MODE="embedded"
# -------------------------------------------
# OIDC
# e.g., https://example.eu.auth0.com/.well-known/openid-configuration
# External OIDC provider (NETBIRD_IDP_MODE=external)
# See https://docs.netbird.io/selfhosted/identity-providers
# -------------------------------------------
# e.g. https://keycloak.example.com/realms/netbird/.well-known/openid-configuration
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=""
# The default setting is to transmit the audience to the IDP during authorization. However,
# if your IDP does not have this capability, you can turn this off by setting it to false.
#NETBIRD_DASH_AUTH_USE_AUDIENCE=false
NETBIRD_AUTH_AUDIENCE=""
# e.g. netbird-client
# OAuth client ID registered for NetBird, e.g. netbird
NETBIRD_AUTH_CLIENT_ID=""
# indicates the scopes that will be requested to the IDP
NETBIRD_AUTH_SUPPORTED_SCOPES=""
# NETBIRD_AUTH_CLIENT_SECRET is required only by Google workspace.
# NETBIRD_AUTH_CLIENT_SECRET=""
# if you want to use a custom claim for the user ID instead of 'sub', set it here
# NETBIRD_AUTH_USER_ID_CLAIM=""
# indicates whether to use Auth0 or not: true or false
# Client secret, only if your IdP requires one (e.g. Google Workspace)
NETBIRD_AUTH_CLIENT_SECRET=""
# JWT audience; defaults to the client ID when empty
NETBIRD_AUTH_AUDIENCE=""
# Scopes requested from the IdP
NETBIRD_AUTH_SUPPORTED_SCOPES="openid profile email"
# Set to true only for Auth0
NETBIRD_USE_AUTH0="false"
# if your IDP provider doesn't support fragmented URIs, configure custom
# redirect and silent redirect URIs, these will be concatenated into your NETBIRD_DOMAIN domain.
# NETBIRD_AUTH_REDIRECT_URI="/peers"
# NETBIRD_AUTH_SILENT_REDIRECT_URI="/add-peers"
# Updates the preference to use id tokens instead of access token on dashboard
# Okta and Gitlab IDPs can benefit from this
# NETBIRD_TOKEN_SOURCE="idToken"
# -------------------------------------------
# OIDC Device Authorization Flow
# -------------------------------------------
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID=""
# Some IDPs requires different audience, scopes and to use id token for device authorization flow
# you can customize here:
NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid"
NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=false
# -------------------------------------------
# OIDC PKCE Authorization Flow
# -------------------------------------------
# Comma separated port numbers. if already in use, PKCE flow will choose an available port from the list as an alternative
# eg. 53000,54000
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS="53000"
# -------------------------------------------
# IDP Management
# -------------------------------------------
# Token the dashboard sends to the management API: accessToken or idToken
# (Okta and GitLab benefit from idToken)
NETBIRD_TOKEN_SOURCE="accessToken"
# Device authorization flow for headless clients (optional)
#NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID=""
# IdP management API for user/group sync (optional)
# eg. zitadel, auth0, azure, keycloak
NETBIRD_MGMT_IDP="none"
# Some IDPs requires different client id and client secret for management api
NETBIRD_IDP_MGMT_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
NETBIRD_IDP_MGMT_CLIENT_SECRET=""
# Required when setting up with Keycloak "https://<YOUR_KEYCLOAK_HOST_AND_PORT>/admin/realms/netbird"
# NETBIRD_IDP_MGMT_EXTRA_ADMIN_ENDPOINT=
# With some IDPs may be needed enabling automatic refresh of signing keys on expire
# NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=false
# NETBIRD_IDP_MGMT_EXTRA_ variables. See https://docs.netbird.io/selfhosted/identity-providers for more information about your IDP of choice.
# -------------------------------------------
# Letsencrypt
# -------------------------------------------
# Disable letsencrypt
# if disabled, cannot use HTTPS anymore and requires setting up a reverse-proxy to do it instead
NETBIRD_DISABLE_LETSENCRYPT=false
# e.g. hello@mydomain.com
NETBIRD_LETSENCRYPT_EMAIL=""
# -------------------------------------------
# Extra settings
# -------------------------------------------
# Disable anonymous metrics collection, see more information at https://netbird.io/docs/FAQ/metrics-collection
NETBIRD_DISABLE_ANONYMOUS_METRICS=false
# DNS DOMAIN configures the domain name used for peer resolution. By default it is netbird.selfhosted
NETBIRD_MGMT_DNS_DOMAIN=netbird.selfhosted
# Disable default all-to-all policy for new accounts
NETBIRD_MGMT_DISABLE_DEFAULT_POLICY=false
# -------------------------------------------
# Relay settings
# -------------------------------------------
# Relay server domain. e.g. relay.mydomain.com
# if not specified it will assume NETBIRD_DOMAIN
NETBIRD_RELAY_DOMAIN=""
#NETBIRD_MGMT_IDP=""
#NETBIRD_IDP_MGMT_CLIENT_ID=""
#NETBIRD_IDP_MGMT_CLIENT_SECRET=""
# Relay server connection port. If none is supplied
# it will default to 33080
# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy
NETBIRD_RELAY_PORT=""
# -------------------------------------------
# Reverse proxy
# -------------------------------------------
# 0 = built-in Traefik (automatic TLS via Let's Encrypt)
# 1 = existing Traefik instance (labels)
# 2 = Nginx (config template is generated)
# 3 = Nginx Proxy Manager (advanced config is generated)
# 4 = external Caddy (Caddyfile snippet is generated)
# 5 = other/manual
NETBIRD_REVERSE_PROXY_TYPE="0"
# Let's Encrypt notification email (required for type 0)
NETBIRD_TRAEFIK_ACME_EMAIL=""
# Settings for an existing Traefik instance (type 1)
NETBIRD_TRAEFIK_EXTERNAL_NETWORK=""
NETBIRD_TRAEFIK_ENTRYPOINT="websecure"
NETBIRD_TRAEFIK_CERTRESOLVER=""
# Bind container ports to 127.0.0.1 only (types 2-5)
NETBIRD_BIND_LOCALHOST_ONLY="true"
# Docker network of your reverse proxy, if it runs in Docker (types 2-4)
NETBIRD_EXTERNAL_PROXY_NETWORK=""
# Management API connecting port. If none is supplied
# it will default to 33073
# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy
NETBIRD_MGMT_API_PORT=""
# -------------------------------------------
# NetBird Proxy and CrowdSec
# (combined architecture with built-in Traefik only)
# -------------------------------------------
NETBIRD_ENABLE_PROXY="false"
NETBIRD_ENABLE_CROWDSEC="false"
# Signal service connecting port. If none is supplied
# it will default to 10000
# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy
NETBIRD_SIGNAL_PORT=""
# -------------------------------------------
# Datastore
# -------------------------------------------
# sqlite or postgres. With postgres and an empty DSN a PostgreSQL container is
# added to the deployment (split architecture only).
NETBIRD_STORE_CONFIG_ENGINE="sqlite"
NETBIRD_STORE_ENGINE_POSTGRES_DSN=""
NETBIRD_POSTGRES_PASSWORD=""
# -------------------------------------------
# Secrets
# Generated automatically on first run and appended to setup.env.
# Keep them stable across re-renders or peers will lose connectivity.
# -------------------------------------------
NETBIRD_RELAY_AUTH_SECRET=""
NETBIRD_DATASTORE_ENC_KEY=""
# -------------------------------------------
# Docker image overrides (optional)
# -------------------------------------------
#DASHBOARD_IMAGE="netbirdio/dashboard:latest"
#NETBIRD_SERVER_IMAGE="netbirdio/netbird-server:latest"
#MANAGEMENT_IMAGE="netbirdio/management:latest"
#SIGNAL_IMAGE="netbirdio/signal:latest"
#RELAY_IMAGE="netbirdio/relay:latest"
#POSTGRES_IMAGE="postgres:16-alpine"
#TRAEFIK_IMAGE="traefik:v3.6"

View File

@@ -0,0 +1,196 @@
#!/bin/bash
# Render-validation tests for getting-started.sh.
# Runs the script in --non-interactive --render-only mode for every supported
# architecture / IdP / reverse-proxy combination and validates the generated
# files. Requires jq; uses `docker compose config` when docker is available.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GETTING_STARTED="$SCRIPT_DIR/../getting-started.sh"
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
FAILURES=0
compose_validate() {
if docker compose version &>/dev/null; then
docker compose -f "$1" config -q
return $?
fi
if command -v docker-compose &>/dev/null; then
docker-compose -f "$1" config -q
return $?
fi
# Without docker fall back to a structural sanity check
grep -q "^services:" "$1"
return $?
}
write_oidc_fixture() {
# A file:// OpenID discovery document stands in for a real IdP
cat > "$1" <<EOF
{
"issuer": "https://idp.example.org/realms/netbird",
"authorization_endpoint": "https://idp.example.org/realms/netbird/protocol/openid-connect/auth",
"token_endpoint": "https://idp.example.org/realms/netbird/protocol/openid-connect/token",
"jwks_uri": "https://idp.example.org/realms/netbird/protocol/openid-connect/certs",
"device_authorization_endpoint": "https://idp.example.org/realms/netbird/protocol/openid-connect/auth/device"
}
EOF
}
run_case() {
local name="$1"
shift
local expected_files=("$@")
local case_dir="$WORK_DIR/$name"
mkdir -p "$case_dir"
# setup.env content is provided on stdin
cat > "$case_dir/setup.env"
echo "--- case: $name"
if ! (cd "$case_dir" && bash "$GETTING_STARTED" --non-interactive --render-only > render.log 2>&1); then
echo "FAIL($name): render exited non-zero"
tail -5 "$case_dir/render.log" | sed 's/^/ /'
FAILURES=$((FAILURES + 1))
return 0
fi
for f in "${expected_files[@]}"; do
if [[ ! -f "$case_dir/$f" ]]; then
echo "FAIL($name): expected file $f was not generated"
FAILURES=$((FAILURES + 1))
fi
done
if [[ -f "$case_dir/management.json" ]] && ! jq . "$case_dir/management.json" > /dev/null; then
echo "FAIL($name): management.json is not valid JSON"
FAILURES=$((FAILURES + 1))
fi
if [[ -f "$case_dir/docker-compose.yml" ]] && ! compose_validate "$case_dir/docker-compose.yml"; then
echo "FAIL($name): docker-compose.yml failed validation"
FAILURES=$((FAILURES + 1))
fi
# Re-rendering from the persisted setup.env must be byte-identical (idempotent)
local checksums_before checksums_after
checksums_before=$(cd "$case_dir" && sha256sum docker-compose.yml dashboard.env config.yaml management.json 2>/dev/null || true)
if ! (cd "$case_dir" && bash "$GETTING_STARTED" --non-interactive --render-only > render2.log 2>&1); then
echo "FAIL($name): re-render exited non-zero"
FAILURES=$((FAILURES + 1))
return 0
fi
checksums_after=$(cd "$case_dir" && sha256sum docker-compose.yml dashboard.env config.yaml management.json 2>/dev/null || true)
if [[ "$checksums_before" != "$checksums_after" ]]; then
echo "FAIL($name): re-render from setup.env is not idempotent"
FAILURES=$((FAILURES + 1))
fi
return 0
}
OIDC_FIXTURE="$WORK_DIR/openid-configuration-fixture.json"
write_oidc_fixture "$OIDC_FIXTURE"
run_case combined-embedded-traefik docker-compose.yml config.yaml dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="combined"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="0"
NETBIRD_TRAEFIK_ACME_EMAIL="admin@example.org"
EOF
run_case combined-embedded-nginx docker-compose.yml config.yaml dashboard.env nginx-netbird.conf <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="combined"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="2"
EOF
run_case split-embedded-traefik docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="0"
NETBIRD_TRAEFIK_ACME_EMAIL="admin@example.org"
EOF
run_case split-embedded-external-traefik docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="1"
NETBIRD_TRAEFIK_ENTRYPOINT="websecure"
NETBIRD_TRAEFIK_CERTRESOLVER="letsencrypt"
EOF
run_case split-external-nginx-postgres docker-compose.yml management.json dashboard.env nginx-netbird.conf <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_AUTH_CLIENT_SECRET="some-secret"
NETBIRD_REVERSE_PROXY_TYPE="2"
NETBIRD_STORE_CONFIG_ENGINE="postgres"
EOF
run_case split-external-manual docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_REVERSE_PROXY_TYPE="5"
NETBIRD_BIND_LOCALHOST_ONLY="false"
EOF
# Auto-forced split must be accepted when no architecture is given with external IdP
run_case split-external-defaulted docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_REVERSE_PROXY_TYPE="5"
EOF
# Invalid combination must fail: combined + external IdP
echo "--- case: combined-external-rejected"
REJECT_DIR="$WORK_DIR/combined-external-rejected"
mkdir -p "$REJECT_DIR"
cat > "$REJECT_DIR/setup.env" <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="combined"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_REVERSE_PROXY_TYPE="5"
EOF
if (cd "$REJECT_DIR" && bash "$GETTING_STARTED" --non-interactive --render-only > render.log 2>&1); then
echo "FAIL(combined-external-rejected): combined + external IdP was not rejected"
FAILURES=$((FAILURES + 1))
fi
# Spot-check rendered content
SPLIT_EXT="$WORK_DIR/split-external-nginx-postgres"
if [[ -f "$SPLIT_EXT/management.json" ]]; then
[[ $(jq -r '.HttpConfig.AuthIssuer' "$SPLIT_EXT/management.json") == "https://idp.example.org/realms/netbird" ]] || { echo "FAIL: external AuthIssuer mismatch"; FAILURES=$((FAILURES + 1)); }
[[ $(jq -r '.EmbeddedIdP' "$SPLIT_EXT/management.json") == "null" ]] || { echo "FAIL: external mode must not configure EmbeddedIdP"; FAILURES=$((FAILURES + 1)); }
[[ $(jq -r '.StoreConfig.Engine' "$SPLIT_EXT/management.json") == "postgres" ]] || { echo "FAIL: postgres engine not set"; FAILURES=$((FAILURES + 1)); }
grep -q "postgres:" "$SPLIT_EXT/docker-compose.yml" || { echo "FAIL: postgres container missing"; FAILURES=$((FAILURES + 1)); }
fi
SPLIT_EMB="$WORK_DIR/split-embedded-traefik"
if [[ -f "$SPLIT_EMB/management.json" ]]; then
[[ $(jq -r '.EmbeddedIdP.Enabled' "$SPLIT_EMB/management.json") == "true" ]] || { echo "FAIL: embedded IdP not enabled in split mode"; FAILURES=$((FAILURES + 1)); }
[[ $(jq -r '.Relay.Addresses[0]' "$SPLIT_EMB/management.json") == "rels://netbird.example.org:443" ]] || { echo "FAIL: relay address mismatch"; FAILURES=$((FAILURES + 1)); }
fi
echo ""
if [[ $FAILURES -gt 0 ]]; then
echo "$FAILURES test(s) failed"
exit 1
fi
echo "All render tests passed"