#!/bin/bash set -e # 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' # Constants for repeated string literals readonly MSG_STARTING_SERVICES="\nStarting NetBird services\n" readonly MSG_DONE="\nDone!\n" readonly MSG_NEXT_STEPS="Next steps:" readonly MSG_SEPARATOR="==========================================" ############################################ # Utility Functions ############################################ check_docker_sock_perms() { local sock="${DOCKER_HOST:-unix:///var/run/docker.sock}" sock="${sock#unix://}" if [[ ! -S "$sock" ]]; then return 0 fi if [[ ! -r "$sock" ]] || [[ ! -w "$sock" ]]; then local group if [[ "${OSTYPE}" == "darwin"* ]]; then group="$(stat -f '%Sg' "$sock")" else group="$(stat -c '%G' "$sock")" fi echo "Cannot access Docker socket: $sock" > /dev/stderr echo "" > /dev/stderr echo "Socket permissions:" > /dev/stderr ls -l "$sock" > /dev/stderr echo "" > /dev/stderr if [[ "$group" == "docker" ]]; then echo "Your user may need to be added to the '$group' group:" > /dev/stderr echo " sudo usermod -aG $group \"$USER\"" > /dev/stderr echo "Then log out and back in, or run this for the current shell:" > /dev/stderr echo " newgrp $group" > /dev/stderr echo "Note: newgrp is temporary; usermod is the permanent group change." > /dev/stderr else echo "The Docker socket is owned by the '$group' group, which is not the standard 'docker' group." > /dev/stderr echo "For safety, this script will not suggest adding your user to '$group'." > /dev/stderr echo "Instead, either run this script with appropriate privileges (for example, via sudo) or follow Docker's post-install steps to configure access via the 'docker' group:" > /dev/stderr echo " https://docs.docker.com/engine/install/linux-postinstall/" > /dev/stderr fi exit 1 fi return 0 } check_docker_compose() { if command -v docker-compose &> /dev/null then echo "docker-compose" return fi if docker compose --help &> /dev/null then echo "docker compose" return fi echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr exit 1 } check_jq() { if ! command -v jq &> /dev/null then echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr exit 1 fi return 0 } get_main_ip_address() { if [[ "$OSTYPE" == "darwin"* ]]; then interface=$(route -n get default | grep 'interface:' | awk '{print $2}') ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}') else interface=$(ip route | grep default | awk '{print $5}' | head -n 1) ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1) fi echo "$ip_address" return 0 } check_nb_domain() { DOMAIN=$1 if [[ "$DOMAIN-x" == "-x" ]]; then echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr return 1 fi if [[ "$DOMAIN" == "netbird.example.com" ]]; then echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr return 1 fi return 0 } read_nb_domain() { READ_NETBIRD_DOMAIN="" echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr read -r READ_NETBIRD_DOMAIN < /dev/tty if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then read_nb_domain fi echo "$READ_NETBIRD_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 echo " [0] Traefik (recommended - automatic TLS, included in Docker Compose)" > /dev/stderr echo " [1] Existing Traefik (labels for external Traefik instance)" > /dev/stderr echo " [2] Nginx (generates config template)" > /dev/stderr echo " [3] Nginx Proxy Manager (generates config + instructions)" > /dev/stderr echo " [4] External Caddy (generates Caddyfile snippet)" > /dev/stderr echo " [5] Other/Manual (displays setup documentation)" > /dev/stderr echo "" > /dev/stderr echo -n "Enter choice [0-5] (default: 0): " > /dev/stderr read -r CHOICE < /dev/tty if [[ -z "$CHOICE" ]]; then CHOICE="0" fi if [[ ! "$CHOICE" =~ ^[0-5]$ ]]; then echo "Invalid choice. Please enter a number between 0 and 5." > /dev/stderr read_reverse_proxy_type return fi echo "$CHOICE" return 0 } read_traefik_network() { echo "" > /dev/stderr echo "If you have an existing Traefik instance, enter its external network name." > /dev/stderr echo -n "External network (leave empty to create 'netbird' network): " > /dev/stderr read -r NETWORK < /dev/tty echo "$NETWORK" return 0 } read_traefik_entrypoint() { echo "" > /dev/stderr echo "Enter the name of your Traefik HTTPS entrypoint." > /dev/stderr echo -n "HTTPS entrypoint name (default: websecure): " > /dev/stderr read -r ENTRYPOINT < /dev/tty if [[ -z "$ENTRYPOINT" ]]; then ENTRYPOINT="websecure" fi echo "$ENTRYPOINT" return 0 } read_traefik_certresolver() { echo "" > /dev/stderr echo "Enter the name of your Traefik certificate resolver (for automatic TLS)." > /dev/stderr echo "Leave empty if you handle TLS termination elsewhere or use a wildcard cert." > /dev/stderr echo -n "Certificate resolver name (e.g., letsencrypt): " > /dev/stderr read -r RESOLVER < /dev/tty echo "$RESOLVER" return 0 } read_port_binding_preference() { echo "" > /dev/stderr echo "Should container ports be bound to localhost only (127.0.0.1)?" > /dev/stderr echo "Choose 'yes' if your reverse proxy runs on the same host (more secure)." > /dev/stderr echo -n "Bind to localhost only? [Y/n]: " > /dev/stderr read -r CHOICE < /dev/tty if [[ "$CHOICE" =~ ^[Nn]$ ]]; then echo "false" else echo "true" fi return 0 } read_proxy_docker_network() { local proxy_name="$1" echo "" > /dev/stderr echo "Is ${proxy_name} running in Docker?" > /dev/stderr echo "If yes, enter the Docker network ${proxy_name} is on (NetBird will join it)." > /dev/stderr echo -n "Docker network (leave empty if not in Docker): " > /dev/stderr read -r NETWORK < /dev/tty echo "$NETWORK" return 0 } read_enable_proxy() { echo "" > /dev/stderr echo "Do you want to enable the NetBird Proxy service?" > /dev/stderr echo "The proxy allows you to selectively expose internal NetBird network resources" > /dev/stderr echo "to the internet. You control which resources are exposed through the dashboard." > /dev/stderr echo -n "Enable proxy? [y/N]: " > /dev/stderr read -r CHOICE < /dev/tty if [[ "$CHOICE" =~ ^[Yy]$ ]]; then echo "true" else echo "false" fi return 0 } read_enable_crowdsec() { echo "" > /dev/stderr echo "Do you want to enable CrowdSec IP reputation blocking?" > /dev/stderr echo "CrowdSec checks client IPs against a community threat intelligence database" > /dev/stderr echo "and blocks known malicious sources before they reach your services." > /dev/stderr echo "A local CrowdSec LAPI container will be added to your deployment." > /dev/stderr echo -n "Enable CrowdSec? [y/N]: " > /dev/stderr read -r CHOICE < /dev/tty if [[ "$CHOICE" =~ ^[Yy]$ ]]; then echo "true" else echo "false" fi return 0 } read_traefik_acme_email() { echo "" > /dev/stderr echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr echo -n "Email address: " > /dev/stderr read -r EMAIL < /dev/tty if [[ -z "$EMAIL" ]]; then echo "Email is required for Let's Encrypt." > /dev/stderr read_traefik_acme_email return fi echo "$EMAIL" return 0 } get_bind_address() { if [[ "$BIND_LOCALHOST_ONLY" == "true" ]]; then echo "127.0.0.1" else echo "0.0.0.0" fi return 0 } get_upstream_host() { # Always return 127.0.0.1 for health checks and upstream targets # Cannot use 0.0.0.0 as a connection target echo "127.0.0.1" 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 set +e if [[ "$proxy_container" == "detect-traefik" ]]; then proxy_container=$(docker ps --format "{{.ID}}\t{{.Image}}\t{{.Ports}}" \ | awk -F'\t' '$2 ~ /traefik/ && $3 ~ /:(80|443)->/ {print $1; exit}') if [[ -z "$proxy_container" ]]; then echo "Warning: could not auto-detect Traefik container, log output will be skipped on timeout." > /dev/stderr else use_docker_logs=true fi fi echo -n "Waiting for NetBird server to become ready" counter=1 while true; do if management_ready "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"; then break fi if [[ $counter -eq 60 ]]; then echo "" echo "Taking too long. Checking logs..." if [[ -n "$proxy_container" ]]; then if [[ "$use_docker_logs" == "true" ]]; then docker logs --tail=20 "$proxy_container" else $DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container" fi fi $DOCKER_COMPOSE_COMMAND logs --tail=20 "$SERVER_SERVICE" fi echo -n " ." sleep 2 counter=$((counter + 1)) done echo " done" set -e return 0 } wait_management_direct() { set +e local upstream_host=$(get_upstream_host) echo -n "Waiting for NetBird server to become ready" counter=1 while true; do 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 "$SERVER_SERVICE" fi echo -n " ." sleep 2 counter=$((counter + 1)) done echo " done" set -e return 0 } ############################################ # Initialization and Configuration ############################################ initialize_default_values() { NETBIRD_PORT=80 NETBIRD_HTTP_PROTOCOL="http" NETBIRD_RELAY_PROTO="rel" 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"} # Reverse proxy configuration REVERSE_PROXY_TYPE="0" TRAEFIK_EXTERNAL_NETWORK="" TRAEFIK_ENTRYPOINT="websecure" TRAEFIK_CERTRESOLVER="" TRAEFIK_ACME_EMAIL="" DASHBOARD_HOST_PORT="8080" 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" # NetBird Proxy configuration ENABLE_PROXY="false" PROXY_TOKEN="" # CrowdSec configuration ENABLE_CROWDSEC="false" CROWDSEC_BOUNCER_KEY="" 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 } render_setup_env_idp_manager_vars() { local var [[ -n "${NETBIRD_MGMT_IDP:-}" ]] && printf '%s=%q\n' "NETBIRD_MGMT_IDP" "$NETBIRD_MGMT_IDP" [[ -n "${NETBIRD_MGMT_IDP_SIGNKEY_REFRESH:-}" ]] && printf '%s=%q\n' "NETBIRD_MGMT_IDP_SIGNKEY_REFRESH" "$NETBIRD_MGMT_IDP_SIGNKEY_REFRESH" [[ -n "${NETBIRD_IDP_MGMT_CLIENT_ID:-}" ]] && printf '%s=%q\n' "NETBIRD_IDP_MGMT_CLIENT_ID" "$NETBIRD_IDP_MGMT_CLIENT_ID" [[ -n "${NETBIRD_IDP_MGMT_CLIENT_SECRET:-}" ]] && printf '%s=%q\n' "NETBIRD_IDP_MGMT_CLIENT_SECRET" "$NETBIRD_IDP_MGMT_CLIENT_SECRET" for var in ${!NETBIRD_IDP_MGMT_EXTRA_*}; do printf '%s=%q\n' "$var" "${!var}" done 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 if [[ "$NETBIRD_DOMAIN" == "use-ip" ]]; then NETBIRD_DOMAIN=$(get_main_ip_address) BASE_DOMAIN=$NETBIRD_DOMAIN else NETBIRD_PORT=443 NETBIRD_HTTP_PROTOCOL="https" NETBIRD_RELAY_PROTO="rels" BASE_DOMAIN=$(echo $NETBIRD_DOMAIN | sed -E 's/^[^.]+\.//') fi return 0 } 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 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 || -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 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 } generate_configuration_files() { echo Rendering initial files... # Render docker-compose and proxy config based on selection case "$REVERSE_PROXY_TYPE" in 0) 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 echo "# Placeholder - will be updated with token after netbird-server starts" > proxy.env echo "NB_PROXY_TOKEN=placeholder" >> proxy.env # TCP ServersTransport for PROXY protocol v2 to the proxy backend render_traefik_dynamic > traefik-dynamic.yaml if [[ "$ENABLE_CROWDSEC" == "true" ]]; then mkdir -p crowdsec fi fi ;; 1) if [[ "$ARCHITECTURE" == "split" ]]; then render_docker_compose_split_traefik > docker-compose.yml else render_docker_compose_traefik > docker-compose.yml fi ;; 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 exit 1 ;; esac # Common files for all configurations render_dashboard_env > dashboard.env 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 } start_services_and_show_instructions() { # For built-in Traefik, start containers immediately # For NPM, start containers first (NPM needs services running to create proxy) # For other external proxies, show instructions first and wait for user confirmation if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then # Built-in Traefik - two-phase startup if proxy is enabled echo -e "$MSG_STARTING_SERVICES" if [[ "$ENABLE_PROXY" == "true" ]]; then # Phase 1: Start core services (without proxy) local core_services="traefik dashboard netbird-server" if [[ "$ENABLE_CROWDSEC" == "true" ]]; then core_services="$core_services crowdsec" fi echo "Starting core services..." $DOCKER_COMPOSE_COMMAND up -d $core_services sleep 3 wait_management_proxy traefik # Phase 2: Create proxy token and start proxy echo "" echo "Creating proxy access token..." # Use docker exec with bash to run the token command directly PROXY_TOKEN=$($DOCKER_COMPOSE_COMMAND exec -T netbird-server \ /go/bin/netbird-server token create --name "default-proxy" --config /etc/netbird/config.yaml 2>/dev/null | grep "^Token:" | awk '{print $2}') if [[ -z "$PROXY_TOKEN" ]]; then echo "ERROR: Failed to create proxy token. Check netbird-server logs." > /dev/stderr $DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server exit 1 fi echo "Proxy token created successfully." if [[ "$ENABLE_CROWDSEC" == "true" ]]; then echo "Registering CrowdSec bouncer..." local cs_retries=0 while ! $DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli lapi status >/dev/null 2>&1; do cs_retries=$((cs_retries + 1)) if [[ $cs_retries -ge 30 ]]; then echo "WARNING: CrowdSec did not become ready. Skipping CrowdSec setup." > /dev/stderr echo "You can register a bouncer manually later with:" > /dev/stderr echo " docker exec netbird-crowdsec cscli bouncers add netbird-proxy -o raw" > /dev/stderr ENABLE_CROWDSEC="false" break fi sleep 2 done if [[ "$ENABLE_CROWDSEC" == "true" ]]; then CROWDSEC_BOUNCER_KEY=$($DOCKER_COMPOSE_COMMAND exec -T crowdsec \ cscli bouncers add netbird-proxy -o raw 2>/dev/null) if [[ -z "$CROWDSEC_BOUNCER_KEY" ]]; then echo "WARNING: Failed to create CrowdSec bouncer key. Skipping CrowdSec setup." > /dev/stderr ENABLE_CROWDSEC="false" else echo "CrowdSec bouncer registered." fi fi fi render_proxy_env > proxy.env # Start proxy service echo "Starting proxy service..." $DOCKER_COMPOSE_COMMAND up -d proxy else # No proxy - start all services at once $DOCKER_COMPOSE_COMMAND up -d sleep 3 wait_management_proxy traefik fi echo -e "$MSG_DONE" print_post_setup_instructions elif [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then # External Traefik - start containers, then show instructions # Traefik discovers services via Docker labels, so containers must be running echo -e "$MSG_STARTING_SERVICES" $DOCKER_COMPOSE_COMMAND up -d sleep 3 wait_management_proxy detect-traefik echo -e "$MSG_DONE" print_post_setup_instructions echo "" echo "NetBird containers are running. Once Traefik is connected, access the dashboard at:" echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" elif [[ "$REVERSE_PROXY_TYPE" == "3" ]]; then # NPM - start containers first, then show instructions # NPM requires backend services to be running before creating proxy hosts echo -e "$MSG_STARTING_SERVICES" $DOCKER_COMPOSE_COMMAND up -d sleep 3 wait_management_direct echo -e "$MSG_DONE" print_post_setup_instructions echo "" echo "NetBird containers are running. Configure NPM as shown above, then access:" echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" else # External proxies (nginx, external Caddy, other) - need manual config first print_post_setup_instructions 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 sleep 3 wait_management_direct echo -e "$MSG_DONE" echo "NetBird is now running. Access the dashboard at:" echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" fi return 0 } init_environment() { 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 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 } ############################################ # Configuration File Renderers ############################################ render_docker_compose_traefik_builtin() { # Generate proxy service section and Traefik dynamic config if enabled local proxy_service="" local proxy_volumes="" local crowdsec_service="" local crowdsec_volumes="" local traefik_file_provider="" local traefik_dynamic_volume="" if [[ "$ENABLE_PROXY" == "true" ]]; then traefik_file_provider=' - "--providers.file.filename=/etc/traefik/dynamic.yaml"' traefik_dynamic_volume=" - ./traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro" local proxy_depends=" netbird-server: condition: service_started" if [[ "$ENABLE_CROWDSEC" == "true" ]]; then proxy_depends=" netbird-server: condition: service_started crowdsec: condition: service_healthy" fi proxy_service=" # NetBird Proxy - exposes internal resources to the internet proxy: image: $NETBIRD_PROXY_IMAGE container_name: netbird-proxy ports: - 51820:51820/udp restart: unless-stopped networks: [netbird] depends_on:${proxy_depends} env_file: - ./proxy.env volumes: - netbird_proxy_certs:/certs labels: # TCP passthrough for any unmatched domain (proxy handles its own TLS) - traefik.enable=true - traefik.tcp.routers.proxy-passthrough.entrypoints=websecure - traefik.tcp.routers.proxy-passthrough.rule=HostSNI(\`*\`) - traefik.tcp.routers.proxy-passthrough.tls.passthrough=true - traefik.tcp.routers.proxy-passthrough.service=proxy-tls - traefik.tcp.routers.proxy-passthrough.priority=1 - traefik.tcp.services.proxy-tls.loadbalancer.server.port=8443 - traefik.tcp.services.proxy-tls.loadbalancer.serverstransport=pp-v2@file logging: driver: \"json-file\" options: max-size: \"500m\" max-file: \"2\" " proxy_volumes=" netbird_proxy_certs:" if [[ "$ENABLE_CROWDSEC" == "true" ]]; then crowdsec_service=" crowdsec: image: $CROWDSEC_IMAGE container_name: netbird-crowdsec restart: unless-stopped networks: [netbird] environment: COLLECTIONS: crowdsecurity/linux volumes: - ./crowdsec:/etc/crowdsec - crowdsec_db:/var/lib/crowdsec/data healthcheck: test: ["CMD", "cscli", "lapi", "status"] interval: 10s timeout: 5s retries: 15 labels: - traefik.enable=false logging: driver: \"json-file\" options: max-size: \"500m\" max-file: \"2\" " crowdsec_volumes=" crowdsec_db:" fi fi cat <" echo " Get your enrollment key at: https://app.crowdsec.net" echo "" fi fi return 0 } print_traefik_instructions() { echo "" echo "$MSG_SEPARATOR" echo " TRAEFIK SETUP" echo "$MSG_SEPARATOR" echo "" echo "NetBird containers are configured with Traefik labels." echo "" echo "Configuration:" echo " Entrypoint: $TRAEFIK_ENTRYPOINT" if [[ -n "$TRAEFIK_CERTRESOLVER" ]]; then echo " Certificate resolver: $TRAEFIK_CERTRESOLVER" fi if [[ -n "$TRAEFIK_EXTERNAL_NETWORK" ]]; then echo " Network: $TRAEFIK_EXTERNAL_NETWORK (external)" else echo " Network: netbird" fi echo "" echo "$MSG_NEXT_STEPS" echo " - Ensure Traefik is running and configured" if [[ -n "$TRAEFIK_EXTERNAL_NETWORK" ]]; then echo " - Traefik must be on the '$TRAEFIK_EXTERNAL_NETWORK' network" fi echo " - Entrypoint '$TRAEFIK_ENTRYPOINT' must be defined" if [[ -n "$TRAEFIK_CERTRESOLVER" ]]; then echo " - Certificate resolver '$TRAEFIK_CERTRESOLVER' must be configured" fi echo " - Disable read timeout on the entrypoint for gRPC streams:" echo " --entrypoints.$TRAEFIK_ENTRYPOINT.transport.respondingTimeouts.readTimeout=0" echo " - HTTP to HTTPS redirect (recommended)" return 0 } print_nginx_instructions() { local bind_addr=$(get_bind_address) echo "" echo "$MSG_SEPARATOR" echo " NGINX SETUP" echo "$MSG_SEPARATOR" echo "" echo "Generated: nginx-netbird.conf" echo "" echo "IMPORTANT: Nginx requires manual TLS certificate setup." echo "You'll need to obtain SSL/TLS certificates and configure the paths in the" echo "generated config file. The config includes examples for common certificate sources." echo "" if [[ -n "$EXTERNAL_PROXY_NETWORK" ]]; then echo "NetBird containers have joined the '$EXTERNAL_PROXY_NETWORK' Docker network." echo "The config uses container names for upstream servers." echo "" echo "$MSG_NEXT_STEPS" echo " 1. Ensure your Nginx container has access to SSL certificates" echo " (mount certificate directory as volume if needed)" echo " 2. Edit nginx-netbird.conf and update SSL certificate paths" echo " The config includes examples for certbot, acme.sh, and custom certs" echo " 3. Include the config in your Nginx container's configuration" echo " 4. Reload Nginx" else echo "$MSG_NEXT_STEPS" echo " 1. Obtain SSL/TLS certificates (Let's Encrypt recommended)" echo " 2. Edit nginx-netbird.conf and update certificate paths" echo " 3. Install to /etc/nginx/sites-available/ (Debian) or /etc/nginx/conf.d/ (RHEL)" echo " 4. Test and reload: nginx -t && systemctl reload nginx" echo "" echo "For detailed TLS setup instructions, see:" echo "https://docs.netbird.io/selfhosted/reverse-proxy#tls-certificate-setup-for-nginx" echo "" print_container_ports "${bind_addr}" fi return 0 } print_npm_instructions() { local bind_addr=$(get_bind_address) local upstream_host=$(get_upstream_host) echo "" echo "$MSG_SEPARATOR" echo " NGINX PROXY MANAGER SETUP" echo "$MSG_SEPARATOR" echo "" echo "Generated: npm-advanced-config.txt" echo "" if [[ -n "$EXTERNAL_PROXY_NETWORK" ]]; then echo "NetBird containers have joined the '$EXTERNAL_PROXY_NETWORK' Docker network." echo "" echo "In NPM, create a Proxy Host:" echo " Domain: $NETBIRD_DOMAIN" echo " Forward Hostname: netbird-dashboard" echo " Forward Port: 80" echo " Block Common Exploits: enabled" echo "" echo " SSL tab:" echo " - Request or select existing certificate" echo " - Enable 'HTTP/2 Support' (REQUIRED for gRPC)" echo "" echo " Advanced tab:" echo " - Paste contents of npm-advanced-config.txt" else print_container_ports "${bind_addr}" echo "" echo "In NPM, create a Proxy Host:" echo " Domain: $NETBIRD_DOMAIN" echo " Forward Hostname/IP: ${upstream_host}" echo " Forward Port: ${DASHBOARD_HOST_PORT}" echo " Block Common Exploits: enabled" echo "" echo " SSL tab:" echo " - Request or select existing certificate" echo " - Enable 'HTTP/2 Support' (REQUIRED for gRPC)" echo "" echo " Advanced tab:" echo " - Paste contents of npm-advanced-config.txt" fi return 0 } print_external_caddy_instructions() { local bind_addr=$(get_bind_address) echo "" echo "$MSG_SEPARATOR" echo " EXTERNAL CADDY SETUP" echo "$MSG_SEPARATOR" echo "" echo "Generated: caddyfile-netbird.txt" echo "" if [[ -n "$EXTERNAL_PROXY_NETWORK" ]]; then echo "NetBird containers have joined the '$EXTERNAL_PROXY_NETWORK' Docker network." echo "The config uses container names for upstream servers." echo "" echo "$MSG_NEXT_STEPS" echo " 1. Add the contents of caddyfile-netbird.txt to your Caddyfile" echo " 2. Reload Caddy" else echo "$MSG_NEXT_STEPS" echo " 1. Add the contents of caddyfile-netbird.txt to your Caddyfile" echo " 2. Reload Caddy: caddy reload --config /path/to/Caddyfile" echo "" print_container_ports "${bind_addr}" fi return 0 } print_manual_instructions() { local bind_addr=$(get_bind_address) local upstream_host=$(get_upstream_host) echo "" echo "$MSG_SEPARATOR" echo " MANUAL REVERSE PROXY SETUP" echo "$MSG_SEPARATOR" echo "" print_container_ports "${bind_addr}" echo "" 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)." return 0 } print_post_setup_instructions() { case "$REVERSE_PROXY_TYPE" in 0) print_builtin_traefik_instructions ;; 1) print_traefik_instructions ;; 2) print_nginx_instructions ;; 3) print_npm_instructions ;; 4) print_external_caddy_instructions ;; 5) print_manual_instructions ;; *) echo "Unknown reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr ;; esac return 0 } print_usage() { cat < /dev/stderr print_usage > /dev/stderr exit 1 ;; esac done init_environment