#!/bin/bash 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 # 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_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 } 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_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 } wait_management_proxy() { local proxy_container="${1:-traefik}" set +e 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 break fi if [[ $counter -eq 60 ]]; then echo "" echo "Taking too long. Checking logs..." $DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container" $DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server 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 # 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 break fi if [[ $counter -eq 60 ]]; then echo "" echo "Taking too long. Checking logs..." $DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server 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_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 # Docker images DASHBOARD_IMAGE="netbirdio/dashboard:latest" # Combined server replaces separate signal, relay, and management containers NETBIRD_SERVER_IMAGE="netbirdio/netbird-server:latest" NETBIRD_PROXY_IMAGE="netbirdio/reverse-proxy:latest" # 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 port (management + signal + relay) BIND_LOCALHOST_ONLY="true" EXTERNAL_PROXY_NETWORK="" # Traefik static IP within the internal bridge network TRAEFIK_IP="172.30.0.10" # NetBird Proxy configuration ENABLE_PROXY="false" PROXY_TOKEN="" return 0 } configure_domain() { if ! check_nb_domain "$NETBIRD_DOMAIN"; then 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_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) 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 return 0 } check_existing_installation() { if [[ -f config.yaml ]]; 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" echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard." 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) render_docker_compose_traefik_builtin > docker-compose.yml 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 fi ;; 1) render_docker_compose_traefik > docker-compose.yml ;; 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 ;; *) echo "Invalid reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr exit 1 ;; esac # Common files for all configurations render_dashboard_env > dashboard.env render_combined_yaml > config.yaml 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) echo "Starting core services..." $DOCKER_COMPOSE_COMMAND up -d traefik dashboard netbird-server 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." # Generate proxy.env with the token 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_direct 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 echo "" echo -n "Press Enter when your reverse proxy is configured (or Ctrl+C to exit)... " read -r < /dev/tty 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() { initialize_default_values configure_domain configure_reverse_proxy check_jq DOCKER_COMPOSE_COMMAND=$(check_docker_compose) check_existing_installation generate_configuration_files 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 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" 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: - netbird-server 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:" 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}" 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 } init_environment