diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index 9ad1f2f67..b9c551c8a 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -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: diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index 92252d0b3..6560f8d31 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -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/" diff --git a/infrastructure_files/getting-started-with-dex.sh b/infrastructure_files/getting-started-with-dex.sh index 5e605f19c..a96dfaddf 100755 --- a/infrastructure_files/getting-started-with-dex.sh +++ b/infrastructure_files/getting-started-with-dex.sh @@ -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' diff --git a/infrastructure_files/getting-started-with-zitadel.sh b/infrastructure_files/getting-started-with-zitadel.sh index f503cbeac..e8297e9a7 100644 --- a/infrastructure_files/getting-started-with-zitadel.sh +++ b/infrastructure_files/getting-started-with-zitadel.sh @@ -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 diff --git a/infrastructure_files/getting-started.sh b/infrastructure_files/getting-started.sh index 770cecc44..136bccc6b 100755 --- a/infrastructure_files/getting-started.sh +++ b/infrastructure_files/getting-started.sh @@ -2,9 +2,17 @@ set -e -# NetBird Getting Started with Embedded IdP (Dex) -# This script sets up NetBird with the embedded Dex identity provider -# No separate Dex container or reverse proxy needed - IdP is built into management server +# NetBird Getting Started +# Sets up a self-hosted NetBird deployment. Two architectures are supported: +# - combined: a single netbird-server container (management + signal + relay + STUN) +# using the built-in identity provider +# - split: separate management, signal and relay containers, using either the +# built-in identity provider or your own OIDC provider +# +# All wizard answers are persisted to setup.env. Re-running with --non-interactive +# renders the same deployment from that file (suitable for IaC/automation). +# +# Usage: getting-started.sh [--non-interactive] [--render-only] [--setup-env=FILE] # Sed pattern to strip base64 padding characters SED_STRIP_PADDING='s/=//g' @@ -122,6 +130,151 @@ read_nb_domain() { return 0 } +check_curl() { + if ! command -v curl &> /dev/null + then + echo "curl is not installed or not in PATH, please install with your package manager. e.g. sudo apt install curl" > /dev/stderr + exit 1 + fi + return 0 +} + +require_interactive() { + # Guards prompts so --non-interactive runs fail loudly instead of hanging on /dev/tty + local var_hint="$1" + if [[ "$NON_INTERACTIVE" == "true" ]]; then + echo "Missing or invalid value for $var_hint in non-interactive mode. Set it in $SETUP_ENV_FILE or as an environment variable." > /dev/stderr + exit 1 + fi + return 0 +} + +read_idp_mode() { + echo "" > /dev/stderr + echo "Which identity provider (IdP) do you want to use?" > /dev/stderr + echo " [0] Built-in IdP (recommended - local users, external providers can be added for SSO later)" > /dev/stderr + echo " [1] Your own OIDC provider as the sole authentication path (Keycloak, Zitadel, Okta, ...)" > /dev/stderr + echo "" > /dev/stderr + echo "Note: the built-in IdP supports connecting external identity providers for SSO" > /dev/stderr + echo "from the dashboard, but runs in single account mode. Choose [1] only if you" > /dev/stderr + echo "need multiple accounts or want no local user management at all." > /dev/stderr + echo "" > /dev/stderr + echo -n "Enter choice [0-1] (default: 0): " > /dev/stderr + read -r CHOICE < /dev/tty + + case "$CHOICE" in + ""|0) echo "embedded" ;; + 1) echo "external" ;; + *) + echo "Invalid choice. Please enter 0 or 1." > /dev/stderr + read_idp_mode + ;; + esac + return 0 +} + +read_architecture() { + echo "" > /dev/stderr + echo "How do you want to run the NetBird server components?" > /dev/stderr + echo " [0] Single combined container (recommended - simplest to operate)" > /dev/stderr + echo " [1] Separate containers per service (management, signal, relay - for scale-out)" > /dev/stderr + echo "" > /dev/stderr + echo -n "Enter choice [0-1] (default: 0): " > /dev/stderr + read -r CHOICE < /dev/tty + + case "$CHOICE" in + ""|0) echo "combined" ;; + 1) echo "split" ;; + *) + echo "Invalid choice. Please enter 0 or 1." > /dev/stderr + read_architecture + ;; + esac + return 0 +} + +read_oidc_endpoint() { + echo "" > /dev/stderr + echo "Enter your IdP's OpenID configuration endpoint." > /dev/stderr + echo "e.g. https://keycloak.example.com/realms/netbird/.well-known/openid-configuration" > /dev/stderr + echo "See https://docs.netbird.io/selfhosted/identity-providers for per-provider instructions." > /dev/stderr + echo -n "OIDC configuration endpoint: " > /dev/stderr + read -r ENDPOINT < /dev/tty + if [[ -z "$ENDPOINT" ]]; then + echo "The OIDC configuration endpoint is required." > /dev/stderr + read_oidc_endpoint + return + fi + echo "$ENDPOINT" + return 0 +} + +read_oidc_client_id() { + echo "" > /dev/stderr + echo "Enter the OAuth client ID you registered for NetBird in your IdP." > /dev/stderr + echo -n "Client ID (e.g. netbird): " > /dev/stderr + read -r CLIENT_ID < /dev/tty + if [[ -z "$CLIENT_ID" ]]; then + echo "The client ID is required." > /dev/stderr + read_oidc_client_id + return + fi + echo "$CLIENT_ID" + return 0 +} + +read_oidc_audience() { + local default_audience="$1" + echo "" > /dev/stderr + echo "Enter the JWT audience your IdP issues tokens with." > /dev/stderr + echo -n "Audience (default: ${default_audience}): " > /dev/stderr + read -r AUDIENCE < /dev/tty + if [[ -z "$AUDIENCE" ]]; then + AUDIENCE="$default_audience" + fi + echo "$AUDIENCE" + return 0 +} + +read_oidc_client_secret() { + echo "" > /dev/stderr + echo "Enter the OAuth client secret, if your IdP requires one." > /dev/stderr + echo -n "Client secret (leave empty for public clients/PKCE): " > /dev/stderr + read -r SECRET < /dev/tty + echo "$SECRET" + return 0 +} + +read_store_engine() { + echo "" > /dev/stderr + echo "Which database engine should the management service use?" > /dev/stderr + echo " [0] SQLite (default - file-based, no extra container)" > /dev/stderr + echo " [1] PostgreSQL (recommended for larger deployments)" > /dev/stderr + echo "" > /dev/stderr + echo -n "Enter choice [0-1] (default: 0): " > /dev/stderr + read -r CHOICE < /dev/tty + + case "$CHOICE" in + ""|0) echo "sqlite" ;; + 1) echo "postgres" ;; + *) + echo "Invalid choice. Please enter 0 or 1." > /dev/stderr + read_store_engine + ;; + esac + return 0 +} + +read_postgres_dsn() { + echo "" > /dev/stderr + echo "Enter the DSN of an existing PostgreSQL server, or leave empty to add" > /dev/stderr + echo "a PostgreSQL container to this deployment." > /dev/stderr + echo -n "DSN (e.g. host=db.example.com user=netbird password=... dbname=netbird port=5432): " > /dev/stderr + read -r DSN < /dev/tty + echo "$DSN" + return 0 +} + read_reverse_proxy_type() { echo "" > /dev/stderr echo "Which reverse proxy will you use?" > /dev/stderr @@ -269,6 +422,22 @@ get_upstream_host() { return 0 } +management_ready() { + # With the built-in IdP we can poll its discovery document for a strict 200. + # With an external IdP the management API is the only public endpoint; any + # HTTP response that is not a gateway error means the backend is up. + local base_url="$1" + if [[ "$IDP_MODE" == "embedded" ]]; then + curl -sk -f -o /dev/null "${base_url}/oauth2/.well-known/openid-configuration" 2>/dev/null + return $? + fi + + local code + code=$(curl -sk -o /dev/null -w '%{http_code}' "${base_url}/api/accounts" 2>/dev/null) + [[ "$code" != "000" && "$code" != "502" && "$code" != "503" && "$code" != "504" ]] + return $? +} + wait_management_proxy() { local proxy_container="${1:-traefik}" local use_docker_logs=false @@ -288,8 +457,7 @@ wait_management_proxy() { echo -n "Waiting for NetBird server to become ready" counter=1 while true; do - # Check the embedded IdP endpoint through the reverse proxy - if curl -sk -f -o /dev/null "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth2/.well-known/openid-configuration" 2>/dev/null; then + if management_ready "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"; then break fi if [[ $counter -eq 60 ]]; then @@ -302,7 +470,7 @@ wait_management_proxy() { $DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container" fi fi - $DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server + $DOCKER_COMPOSE_COMMAND logs --tail=20 "$SERVER_SERVICE" fi echo -n " ." sleep 2 @@ -319,14 +487,13 @@ wait_management_direct() { echo -n "Waiting for NetBird server to become ready" counter=1 while true; do - # Check the embedded IdP endpoint directly (no reverse proxy) - if curl -sk -f -o /dev/null "http://${upstream_host}:${MANAGEMENT_HOST_PORT}/oauth2/.well-known/openid-configuration" 2>/dev/null; then + if management_ready "http://${upstream_host}:${MANAGEMENT_HOST_PORT}"; then break fi if [[ $counter -eq 60 ]]; then echo "" echo "Taking too long. Checking logs..." - $DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server + $DOCKER_COMPOSE_COMMAND logs --tail=20 "$SERVER_SERVICE" fi echo -n " ." sleep 2 @@ -345,15 +512,38 @@ initialize_default_values() { NETBIRD_PORT=80 NETBIRD_HTTP_PROTOCOL="http" NETBIRD_RELAY_PROTO="rel" - NETBIRD_RELAY_AUTH_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING") - # Note: DataStoreEncryptionKey must keep base64 padding (=) for Go's base64.StdEncoding - DATASTORE_ENCRYPTION_KEY=$(openssl rand -base64 32) NETBIRD_STUN_PORT=3478 + # Deployment shape + IDP_MODE="embedded" # embedded | external + ARCHITECTURE="combined" # combined | split + + # External OIDC provider (IDP_MODE=external) + AUTH_OIDC_ENDPOINT="" + AUTH_CLIENT_ID="" + AUTH_CLIENT_SECRET="" + AUTH_AUDIENCE="" + AUTH_SUPPORTED_SCOPES="openid profile email" + USE_AUTH0="false" + TOKEN_SOURCE="accessToken" + AUTH_REDIRECT_URI="/nb-auth" + AUTH_SILENT_REDIRECT_URI="/nb-silent-auth" + + # Datastore (split architecture prompts for this; combined defaults to sqlite) + STORE_ENGINE="sqlite" # sqlite | postgres + POSTGRES_DSN="" + POSTGRES_PASSWORD="" + DEPLOY_POSTGRES="false" + # Docker images DASHBOARD_IMAGE=${DASHBOARD_IMAGE:-"netbirdio/dashboard:latest"} # Combined server replaces separate signal, relay, and management containers NETBIRD_SERVER_IMAGE=${NETBIRD_SERVER_IMAGE:-"netbirdio/netbird-server:latest"} + # Split architecture images + MANAGEMENT_IMAGE=${MANAGEMENT_IMAGE:-"netbirdio/management:latest"} + SIGNAL_IMAGE=${SIGNAL_IMAGE:-"netbirdio/signal:latest"} + RELAY_IMAGE=${RELAY_IMAGE:-"netbirdio/relay:latest"} + POSTGRES_IMAGE=${POSTGRES_IMAGE:-"postgres:16-alpine"} NETBIRD_PROXY_IMAGE=${NETBIRD_PROXY_IMAGE:-"netbirdio/reverse-proxy:latest"} TRAEFIK_IMAGE=${TRAEFIK_IMAGE:-"traefik:v3.6"} CROWDSEC_IMAGE=${CROWDSEC_IMAGE:-"crowdsecurity/crowdsec:v1.7.7"} @@ -364,10 +554,15 @@ initialize_default_values() { TRAEFIK_CERTRESOLVER="" TRAEFIK_ACME_EMAIL="" DASHBOARD_HOST_PORT="8080" - MANAGEMENT_HOST_PORT="8081" # Combined server port (management + signal + relay) + MANAGEMENT_HOST_PORT="8081" # Combined server / split management host port + SIGNAL_HOST_PORT="10000" # Split architecture only + RELAY_HOST_PORT="33080" # Split architecture only BIND_LOCALHOST_ONLY="true" EXTERNAL_PROXY_NETWORK="" + # Service name used in compose logs hints (combined: netbird-server, split: management) + SERVER_SERVICE="netbird-server" + # Traefik static IP within the internal bridge network TRAEFIK_IP="172.30.0.10" @@ -381,8 +576,181 @@ initialize_default_values() { return 0 } +ensure_secrets() { + # Secrets are generated only when not provided (setup.env or environment), + # so re-rendering an existing deployment keeps peers and data intact + GENERATED_SECRETS="" + if [[ -z "$NETBIRD_RELAY_AUTH_SECRET" ]]; then + NETBIRD_RELAY_AUTH_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING") + GENERATED_SECRETS="$GENERATED_SECRETS NETBIRD_RELAY_AUTH_SECRET" + fi + # Note: DataStoreEncryptionKey must keep base64 padding (=) for Go's base64.StdEncoding + if [[ -z "$DATASTORE_ENCRYPTION_KEY" ]]; then + DATASTORE_ENCRYPTION_KEY=$(openssl rand -base64 32) + GENERATED_SECRETS="$GENERATED_SECRETS NETBIRD_DATASTORE_ENC_KEY" + fi + if [[ "$DEPLOY_POSTGRES" == "true" && -z "$POSTGRES_PASSWORD" ]]; then + POSTGRES_PASSWORD=$(openssl rand -base64 24 | sed "$SED_STRIP_PADDING") + GENERATED_SECRETS="$GENERATED_SECRETS NETBIRD_POSTGRES_PASSWORD" + fi + return 0 +} + +load_setup_env() { + if [[ -f "$SETUP_ENV_FILE" ]]; then + set -a + # shellcheck disable=SC1090 + source "$SETUP_ENV_FILE" + set +a + elif [[ -z "$NETBIRD_DOMAIN" ]]; then + echo "Non-interactive mode requires $SETUP_ENV_FILE or configuration via environment variables (at least NETBIRD_DOMAIN)." > /dev/stderr + exit 1 + fi + + IDP_MODE=${NETBIRD_IDP_MODE:-$IDP_MODE} + ARCHITECTURE=${NETBIRD_ARCHITECTURE:-$ARCHITECTURE} + AUTH_OIDC_ENDPOINT=${NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT:-$AUTH_OIDC_ENDPOINT} + AUTH_CLIENT_ID=${NETBIRD_AUTH_CLIENT_ID:-$AUTH_CLIENT_ID} + AUTH_CLIENT_SECRET=${NETBIRD_AUTH_CLIENT_SECRET:-$AUTH_CLIENT_SECRET} + AUTH_AUDIENCE=${NETBIRD_AUTH_AUDIENCE:-$AUTH_AUDIENCE} + AUTH_SUPPORTED_SCOPES=${NETBIRD_AUTH_SUPPORTED_SCOPES:-$AUTH_SUPPORTED_SCOPES} + USE_AUTH0=${NETBIRD_USE_AUTH0:-$USE_AUTH0} + TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-$TOKEN_SOURCE} + AUTH_REDIRECT_URI=${NETBIRD_AUTH_REDIRECT_URI:-$AUTH_REDIRECT_URI} + AUTH_SILENT_REDIRECT_URI=${NETBIRD_AUTH_SILENT_REDIRECT_URI:-$AUTH_SILENT_REDIRECT_URI} + STORE_ENGINE=${NETBIRD_STORE_CONFIG_ENGINE:-$STORE_ENGINE} + POSTGRES_DSN=${NETBIRD_STORE_ENGINE_POSTGRES_DSN:-$POSTGRES_DSN} + POSTGRES_PASSWORD=${NETBIRD_POSTGRES_PASSWORD:-$POSTGRES_PASSWORD} + REVERSE_PROXY_TYPE=${NETBIRD_REVERSE_PROXY_TYPE:-$REVERSE_PROXY_TYPE} + TRAEFIK_ACME_EMAIL=${NETBIRD_TRAEFIK_ACME_EMAIL:-$TRAEFIK_ACME_EMAIL} + TRAEFIK_EXTERNAL_NETWORK=${NETBIRD_TRAEFIK_EXTERNAL_NETWORK:-$TRAEFIK_EXTERNAL_NETWORK} + TRAEFIK_ENTRYPOINT=${NETBIRD_TRAEFIK_ENTRYPOINT:-$TRAEFIK_ENTRYPOINT} + TRAEFIK_CERTRESOLVER=${NETBIRD_TRAEFIK_CERTRESOLVER:-$TRAEFIK_CERTRESOLVER} + BIND_LOCALHOST_ONLY=${NETBIRD_BIND_LOCALHOST_ONLY:-$BIND_LOCALHOST_ONLY} + EXTERNAL_PROXY_NETWORK=${NETBIRD_EXTERNAL_PROXY_NETWORK:-$EXTERNAL_PROXY_NETWORK} + ENABLE_PROXY=${NETBIRD_ENABLE_PROXY:-$ENABLE_PROXY} + ENABLE_CROWDSEC=${NETBIRD_ENABLE_CROWDSEC:-$ENABLE_CROWDSEC} + DATASTORE_ENCRYPTION_KEY=${NETBIRD_DATASTORE_ENC_KEY:-$DATASTORE_ENCRYPTION_KEY} + # NETBIRD_RELAY_AUTH_SECRET is used directly + return 0 +} + +write_setup_env() { + if [[ "$NON_INTERACTIVE" == "true" ]]; then + persist_generated_secrets + return 0 + fi + + cat > "$SETUP_ENV_FILE" <> "$SETUP_ENV_FILE" + echo "# Secrets generated by getting-started.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$SETUP_ENV_FILE" + for secret in $GENERATED_SECRETS; do + case "$secret" in + NETBIRD_RELAY_AUTH_SECRET) echo "NETBIRD_RELAY_AUTH_SECRET=\"$NETBIRD_RELAY_AUTH_SECRET\"" >> "$SETUP_ENV_FILE" ;; + NETBIRD_DATASTORE_ENC_KEY) echo "NETBIRD_DATASTORE_ENC_KEY=\"$DATASTORE_ENCRYPTION_KEY\"" >> "$SETUP_ENV_FILE" ;; + NETBIRD_POSTGRES_PASSWORD) echo "NETBIRD_POSTGRES_PASSWORD=\"$POSTGRES_PASSWORD\"" >> "$SETUP_ENV_FILE" ;; + esac + echo "Generated $secret and appended it to $SETUP_ENV_FILE" + done + return 0 +} + +fetch_oidc_configuration() { + check_curl + echo "Loading OpenID configuration from $AUTH_OIDC_ENDPOINT" + if ! curl -fsSL "$AUTH_OIDC_ENDPOINT" -o openid-configuration.json; then + echo "Failed to fetch the OpenID configuration from $AUTH_OIDC_ENDPOINT" > /dev/stderr + echo "Check that the URL is reachable and points to a .well-known/openid-configuration document." > /dev/stderr + exit 1 + fi + + NETBIRD_AUTH_AUTHORITY=$(jq -r '.issuer // empty' openid-configuration.json) + NETBIRD_AUTH_JWT_CERTS=$(jq -r '.jwks_uri // empty' openid-configuration.json) + NETBIRD_AUTH_TOKEN_ENDPOINT=$(jq -r '.token_endpoint // empty' openid-configuration.json) + NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT=$(jq -r '.authorization_endpoint // empty' openid-configuration.json) + NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT=$(jq -r '.device_authorization_endpoint // empty' openid-configuration.json) + + if [[ -z "$NETBIRD_AUTH_AUTHORITY" || -z "$NETBIRD_AUTH_JWT_CERTS" ]]; then + echo "The OpenID configuration at $AUTH_OIDC_ENDPOINT is missing the issuer or jwks_uri field." > /dev/stderr + exit 1 + fi + return 0 +} + configure_domain() { if ! check_nb_domain "$NETBIRD_DOMAIN"; then + require_interactive "NETBIRD_DOMAIN" NETBIRD_DOMAIN=$(read_nb_domain) fi @@ -398,48 +766,159 @@ configure_domain() { return 0 } -configure_reverse_proxy() { - # Prompt for reverse proxy type - REVERSE_PROXY_TYPE=$(read_reverse_proxy_type) - - # Handle built-in Traefik prompts (option 0) - if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then - TRAEFIK_ACME_EMAIL=$(read_traefik_acme_email) - ENABLE_PROXY=$(read_enable_proxy) - if [[ "$ENABLE_PROXY" == "true" ]]; then - ENABLE_CROWDSEC=$(read_enable_crowdsec) +configure_idp() { + if [[ "$NON_INTERACTIVE" != "true" ]]; then + IDP_MODE=$(read_idp_mode) + if [[ "$IDP_MODE" == "external" ]]; then + AUTH_OIDC_ENDPOINT=$(read_oidc_endpoint) + AUTH_CLIENT_ID=$(read_oidc_client_id) + AUTH_AUDIENCE=$(read_oidc_audience "$AUTH_CLIENT_ID") + AUTH_CLIENT_SECRET=$(read_oidc_client_secret) fi fi - # Handle external Traefik-specific prompts (option 1) - if [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then - TRAEFIK_EXTERNAL_NETWORK=$(read_traefik_network) - TRAEFIK_ENTRYPOINT=$(read_traefik_entrypoint) - TRAEFIK_CERTRESOLVER=$(read_traefik_certresolver) - fi - - # Handle port binding for external proxy options (2-5) - if [[ "$REVERSE_PROXY_TYPE" -ge 2 ]]; then - BIND_LOCALHOST_ONLY=$(read_port_binding_preference) - fi - - # Handle Docker network prompts for external proxies (options 2-4) - case "$REVERSE_PROXY_TYPE" in - 2) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Nginx") ;; - 3) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Nginx Proxy Manager") ;; - 4) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Caddy") ;; - *) ;; # No network prompt for other options + case "$IDP_MODE" in + embedded) ;; + external) + if [[ -z "$AUTH_OIDC_ENDPOINT" ]]; then + require_interactive "NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT" + fi + if [[ -z "$AUTH_CLIENT_ID" ]]; then + require_interactive "NETBIRD_AUTH_CLIENT_ID" + fi + AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH_CLIENT_ID} + ;; + *) + echo "Invalid NETBIRD_IDP_MODE: $IDP_MODE (expected embedded or external)" > /dev/stderr + exit 1 + ;; esac return 0 } +configure_architecture() { + if [[ "$IDP_MODE" == "external" ]]; then + # The combined netbird-server only supports the built-in IdP, so an external + # provider requires the split architecture + if [[ "$NON_INTERACTIVE" == "true" && "$ARCHITECTURE" == "combined" && -n "$NETBIRD_ARCHITECTURE" ]]; then + echo "NETBIRD_ARCHITECTURE=combined is not supported with NETBIRD_IDP_MODE=external." > /dev/stderr + echo "The combined netbird-server only supports the built-in IdP; use NETBIRD_ARCHITECTURE=split." > /dev/stderr + exit 1 + fi + if [[ "$NON_INTERACTIVE" != "true" ]]; then + echo "" > /dev/stderr + echo "Using your own OIDC provider requires the split architecture" > /dev/stderr + echo "(separate management, signal and relay containers)." > /dev/stderr + fi + ARCHITECTURE="split" + elif [[ "$NON_INTERACTIVE" != "true" ]]; then + ARCHITECTURE=$(read_architecture) + fi + + case "$ARCHITECTURE" in + combined) + SERVER_SERVICE="netbird-server" + ;; + split) + SERVER_SERVICE="management" + ;; + *) + echo "Invalid NETBIRD_ARCHITECTURE: $ARCHITECTURE (expected combined or split)" > /dev/stderr + exit 1 + ;; + esac + return 0 +} + +configure_datastore() { + if [[ "$ARCHITECTURE" == "split" && "$NON_INTERACTIVE" != "true" ]]; then + STORE_ENGINE=$(read_store_engine) + if [[ "$STORE_ENGINE" == "postgres" ]]; then + POSTGRES_DSN=$(read_postgres_dsn) + fi + fi + + case "$STORE_ENGINE" in + sqlite) ;; + postgres) + if [[ -z "$POSTGRES_DSN" ]]; then + if [[ "$ARCHITECTURE" == "combined" ]]; then + echo "NETBIRD_STORE_CONFIG_ENGINE=postgres with the combined architecture requires NETBIRD_STORE_ENGINE_POSTGRES_DSN." > /dev/stderr + exit 1 + fi + DEPLOY_POSTGRES="true" + fi + ;; + *) + echo "Invalid NETBIRD_STORE_CONFIG_ENGINE: $STORE_ENGINE (expected sqlite or postgres)" > /dev/stderr + exit 1 + ;; + esac + return 0 +} + +configure_reverse_proxy() { + if [[ "$NON_INTERACTIVE" != "true" ]]; then + REVERSE_PROXY_TYPE=$(read_reverse_proxy_type) + + # Handle built-in Traefik prompts (option 0) + if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then + TRAEFIK_ACME_EMAIL=$(read_traefik_acme_email) + # NetBird Proxy and CrowdSec are only wired up for the combined server + if [[ "$ARCHITECTURE" == "combined" ]]; then + ENABLE_PROXY=$(read_enable_proxy) + if [[ "$ENABLE_PROXY" == "true" ]]; then + ENABLE_CROWDSEC=$(read_enable_crowdsec) + fi + fi + fi + + # Handle external Traefik-specific prompts (option 1) + if [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then + TRAEFIK_EXTERNAL_NETWORK=$(read_traefik_network) + TRAEFIK_ENTRYPOINT=$(read_traefik_entrypoint) + TRAEFIK_CERTRESOLVER=$(read_traefik_certresolver) + fi + + # Handle port binding for external proxy options (2-5) + if [[ "$REVERSE_PROXY_TYPE" -ge 2 ]]; then + BIND_LOCALHOST_ONLY=$(read_port_binding_preference) + fi + + # Handle Docker network prompts for external proxies (options 2-4) + case "$REVERSE_PROXY_TYPE" in + 2) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Nginx") ;; + 3) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Nginx Proxy Manager") ;; + 4) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Caddy") ;; + *) ;; # No network prompt for other options + esac + fi + + if [[ ! "$REVERSE_PROXY_TYPE" =~ ^[0-5]$ ]]; then + echo "Invalid NETBIRD_REVERSE_PROXY_TYPE: $REVERSE_PROXY_TYPE (expected 0-5)" > /dev/stderr + exit 1 + fi + if [[ "$REVERSE_PROXY_TYPE" == "0" && -z "$TRAEFIK_ACME_EMAIL" ]]; then + require_interactive "NETBIRD_TRAEFIK_ACME_EMAIL" + fi + if [[ "$ARCHITECTURE" == "split" && "$ENABLE_PROXY" == "true" ]]; then + echo "The NetBird Proxy service is currently only supported with the combined architecture; disabling it." > /dev/stderr + ENABLE_PROXY="false" + ENABLE_CROWDSEC="false" + fi + return 0 +} + check_existing_installation() { - if [[ -f config.yaml ]]; then + if [[ -f config.yaml || -f management.json ]]; then echo "Generated files already exist, if you want to reinitialize the environment, please remove them first." echo "You can use the following commands:" echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes" - echo " rm -f docker-compose.yml dashboard.env config.yaml proxy.env traefik-dynamic.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt && rm -rf crowdsec/" + echo " rm -f docker-compose.yml dashboard.env config.yaml management.json openid-configuration.json proxy.env traefik-dynamic.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt && rm -rf crowdsec/" echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard." + echo "" + echo "To re-render configuration for an existing deployment instead, run:" + echo " ./getting-started.sh --non-interactive" exit 1 fi return 0 @@ -451,7 +930,11 @@ generate_configuration_files() { # Render docker-compose and proxy config based on selection case "$REVERSE_PROXY_TYPE" in 0) - render_docker_compose_traefik_builtin > docker-compose.yml + if [[ "$ARCHITECTURE" == "split" ]]; then + render_docker_compose_split_traefik_builtin > docker-compose.yml + else + render_docker_compose_traefik_builtin > docker-compose.yml + fi if [[ "$ENABLE_PROXY" == "true" ]]; then # Create placeholder proxy.env so docker-compose can validate # This will be overwritten with the actual token after netbird-server starts @@ -465,22 +948,23 @@ generate_configuration_files() { fi ;; 1) - render_docker_compose_traefik > docker-compose.yml + if [[ "$ARCHITECTURE" == "split" ]]; then + render_docker_compose_split_traefik > docker-compose.yml + else + render_docker_compose_traefik > docker-compose.yml + fi ;; - 2) - render_docker_compose_exposed_ports > docker-compose.yml - render_nginx_conf > nginx-netbird.conf - ;; - 3) - render_docker_compose_exposed_ports > docker-compose.yml - render_npm_advanced_config > npm-advanced-config.txt - ;; - 4) - render_docker_compose_exposed_ports > docker-compose.yml - render_external_caddyfile > caddyfile-netbird.txt - ;; - 5) - render_docker_compose_exposed_ports > docker-compose.yml + 2|3|4|5) + if [[ "$ARCHITECTURE" == "split" ]]; then + render_docker_compose_split_exposed_ports > docker-compose.yml + else + render_docker_compose_exposed_ports > docker-compose.yml + fi + case "$REVERSE_PROXY_TYPE" in + 2) render_nginx_conf > nginx-netbird.conf ;; + 3) render_npm_advanced_config > npm-advanced-config.txt ;; + 4) render_external_caddyfile > caddyfile-netbird.txt ;; + esac ;; *) echo "Invalid reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr @@ -490,7 +974,29 @@ generate_configuration_files() { # Common files for all configurations render_dashboard_env > dashboard.env - render_combined_yaml > config.yaml + if [[ "$ARCHITECTURE" == "split" ]]; then + render_management_json | jq . > management.json + else + render_combined_yaml > config.yaml + fi + return 0 +} + +print_render_only_summary() { + echo "" + echo "$MSG_SEPARATOR" + echo " CONFIGURATION RENDERED" + echo "$MSG_SEPARATOR" + echo "" + echo "Generated files (services were NOT started):" + for f in docker-compose.yml dashboard.env config.yaml management.json setup.env \ + nginx-netbird.conf npm-advanced-config.txt caddyfile-netbird.txt traefik-dynamic.yaml; do + [[ -f "$f" ]] && echo " - $f" + done + echo "" + echo "Start the deployment with:" + echo " $DOCKER_COMPOSE_COMMAND up -d" + print_post_setup_instructions return 0 } @@ -603,9 +1109,11 @@ start_services_and_show_instructions() { # External proxies (nginx, external Caddy, other) - need manual config first print_post_setup_instructions - echo "" - echo -n "Press Enter when your reverse proxy is configured (or Ctrl+C to exit)... " - read -r < /dev/tty + if [[ "$NON_INTERACTIVE" != "true" ]]; then + echo "" + echo -n "Press Enter when your reverse proxy is configured (or Ctrl+C to exit)... " + read -r < /dev/tty + fi echo -e "$MSG_STARTING_SERVICES" $DOCKER_COMPOSE_COMMAND up -d @@ -621,18 +1129,48 @@ start_services_and_show_instructions() { } init_environment() { - # Check if docker compose is installed using check_docker_compose function - DOCKER_COMPOSE_COMMAND=$(check_docker_compose) - check_docker_sock_perms - - initialize_default_values - configure_domain - configure_reverse_proxy + if [[ "$RENDER_ONLY" == "true" ]]; then + # Rendering does not need a working Docker daemon; fall back for hint text + if command -v docker-compose &> /dev/null; then + DOCKER_COMPOSE_COMMAND="docker-compose" + else + DOCKER_COMPOSE_COMMAND="docker compose" + fi + else + DOCKER_COMPOSE_COMMAND=$(check_docker_compose) + check_docker_sock_perms + fi check_jq - check_existing_installation + initialize_default_values + if [[ "$NON_INTERACTIVE" == "true" ]]; then + load_setup_env + fi + + configure_domain + configure_idp + configure_architecture + configure_datastore + configure_reverse_proxy + ensure_secrets + + if [[ "$NON_INTERACTIVE" != "true" ]]; then + check_existing_installation + fi + + if [[ "$IDP_MODE" == "external" ]]; then + fetch_oidc_configuration + fi + + write_setup_env generate_configuration_files + + if [[ "$RENDER_ONLY" == "true" ]]; then + print_render_only_summary + return 0 + fi + start_services_and_show_instructions return 0 } @@ -885,26 +1423,200 @@ server: - "$TRAEFIK_IP/32" store: - engine: "sqlite" + engine: "$STORE_ENGINE" +$(if [[ "$STORE_ENGINE" == "postgres" ]]; then echo " dsn: \"$POSTGRES_DSN\""; fi) encryptionKey: "$DATASTORE_ENCRYPTION_KEY" EOF return 0 } +render_management_json() { + local mgmt_issuer mgmt_audience mgmt_keys_location mgmt_oidc_endpoint mgmt_signkey_refresh + local signal_proto="http" + if [[ "$NETBIRD_HTTP_PROTOCOL" == "https" ]]; then + signal_proto="https" + fi + + if [[ "$IDP_MODE" == "embedded" ]]; then + mgmt_issuer="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth2" + mgmt_audience="netbird-dashboard" + mgmt_keys_location="${mgmt_issuer}/keys" + mgmt_oidc_endpoint="${mgmt_issuer}/.well-known/openid-configuration" + mgmt_signkey_refresh="true" + else + mgmt_issuer="$NETBIRD_AUTH_AUTHORITY" + mgmt_audience="$AUTH_AUDIENCE" + mgmt_keys_location="$NETBIRD_AUTH_JWT_CERTS" + mgmt_oidc_endpoint="$AUTH_OIDC_ENDPOINT" + mgmt_signkey_refresh="false" + fi + + local trusted_proxies="[]" + if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then + trusted_proxies="[\"$TRAEFIK_IP/32\"]" + fi + + cat < ${upstream_host}:${MANAGEMENT_HOST_PORT}" - echo " (HTTP with WebSocket upgrade, extended timeout)" - echo "" - echo " Native gRPC (signal + management):" - echo " /signalexchange.SignalExchange/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" - echo " /management.ManagementService/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" - echo " (gRPC/h2c - plaintext HTTP/2)" - echo "" - echo " HTTP (API + embedded IdP):" - echo " /api/*, /oauth2/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" - echo "" - echo " Dashboard (catch-all):" - echo " /* -> ${upstream_host}:${DASHBOARD_HOST_PORT}" + if [[ "$ARCHITECTURE" == "split" ]]; then + echo "Configure your reverse proxy with these routes:" + echo "" + echo " WebSocket (relay + gRPC fallbacks):" + echo " /relay* -> ${upstream_host}:${RELAY_HOST_PORT}" + echo " /ws-proxy/signal -> ${upstream_host}:${SIGNAL_HOST_PORT}" + echo " /ws-proxy/management -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (HTTP with WebSocket upgrade, extended timeout)" + echo "" + echo " Native gRPC:" + echo " /signalexchange.SignalExchange/* -> ${upstream_host}:${SIGNAL_HOST_PORT}" + echo " /management.ManagementService/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (gRPC/h2c - plaintext HTTP/2)" + echo "" + echo " HTTP (API + IdP):" + echo " /api/*, /oauth2/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo "" + echo " Dashboard (catch-all):" + echo " /* -> ${upstream_host}:${DASHBOARD_HOST_PORT}" + else + echo "Configure your reverse proxy with these routes (all go to the same backend):" + echo "" + echo " WebSocket (relay, signal, management WS proxy):" + echo " /relay*, /ws-proxy/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (HTTP with WebSocket upgrade, extended timeout)" + echo "" + echo " Native gRPC (signal + management):" + echo " /signalexchange.SignalExchange/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " /management.ManagementService/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (gRPC/h2c - plaintext HTTP/2)" + echo "" + echo " HTTP (API + embedded IdP):" + echo " /api/*, /oauth2/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo "" + echo " Dashboard (catch-all):" + echo " /* -> ${upstream_host}:${DASHBOARD_HOST_PORT}" + fi echo "" echo "IMPORTANT: gRPC routes require HTTP/2 (h2c) upstream support." echo "WebSocket and gRPC connections need extended timeouts (recommend 1 day)." @@ -1567,4 +3124,42 @@ print_post_setup_instructions() { return 0 } +print_usage() { + cat < /dev/stderr + print_usage > /dev/stderr + exit 1 + ;; + esac +done + init_environment diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example index 382e1e18c..ef10d0b9b 100644 --- a/infrastructure_files/setup.env.example +++ b/infrastructure_files/setup.env.example @@ -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:///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" diff --git a/infrastructure_files/tests/test-render.sh b/infrastructure_files/tests/test-render.sh new file mode 100755 index 000000000..8b59dd973 --- /dev/null +++ b/infrastructure_files/tests/test-render.sh @@ -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" < "$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 < "$REJECT_DIR/setup.env" < 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"