#!/bin/bash set -e set -o pipefail # NetBird Enterprise — Getting Started # Single-node bootstrap for a self-hosted NetBird Enterprise stack with the # embedded identity provider. Owner is created via first-login flow. SED_STRIP_PADDING='s/=//g' NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA" 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. See https://docs.docker.com/engine/install/" > /dev/stderr exit 1 } check_openssl() { if ! command -v openssl &> /dev/null; then echo "openssl is not installed or not in PATH." > /dev/stderr exit 1 fi } rand_secret() { openssl rand -base64 32 | sed "$SED_STRIP_PADDING" } rand_b64_key() { openssl rand -base64 32 } check_nb_domain() { local domain="$1" if [[ -z "$domain" ]]; then echo "The domain cannot be empty." > /dev/stderr return 1 fi if [[ "$domain" == "netbird.example.com" ]]; then echo "The domain cannot be netbird.example.com" > /dev/stderr return 1 fi if [[ "$domain" =~ ^[0-9.]+$ ]]; then echo "An IP address is not allowed. A real DNS-resolvable domain is required for TLS and the embedded IdP issuer." > /dev/stderr return 1 fi if [[ ! "$domain" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$ ]]; then echo "The value '$domain' is not a valid FQDN. A real DNS-resolvable domain is required for TLS and the embedded IdP issuer." > /dev/stderr return 1 fi return 0 } check_domain_resolves() { local domain="$1" if command -v getent &> /dev/null && getent hosts "$domain" &> /dev/null; then return 0; fi if command -v host &> /dev/null && host "$domain" &> /dev/null; then return 0; fi if command -v dig &> /dev/null && [[ -n "$(dig +short "$domain" 2>/dev/null)" ]]; then return 0; fi if command -v nslookup &> /dev/null && nslookup "$domain" &> /dev/null; then return 0; fi return 1 } read_nb_domain() { local value="" echo -n "Enter the FQDN for NetBird (must resolve via DNS, e.g. netbird.my-domain.com): " > /dev/stderr read -r value < /dev/tty if ! check_nb_domain "$value"; then read_nb_domain return fi if ! check_domain_resolves "$value"; then echo "" > /dev/stderr echo "Warning: '$value' does not resolve via DNS from this host." > /dev/stderr echo "Caddy will not be able to issue TLS certificates until it does." > /dev/stderr local confirm="" echo -n "Continue anyway? [y/N]: " > /dev/stderr read -r confirm < /dev/tty if [[ ! "$confirm" =~ ^[Yy]$ ]]; then read_nb_domain return fi fi echo "$value" } read_required() { local prompt="$1" local value="" while [[ -z "$value" ]]; do echo -n "$prompt: " > /dev/stderr read -r value < /dev/tty if [[ -z "$value" ]]; then echo "Value cannot be empty." > /dev/stderr fi done echo "$value" } read_secret() { local prompt="$1" local value="" while [[ -z "$value" ]]; do echo -n "$prompt: " > /dev/stderr read -rs value < /dev/tty echo "" > /dev/stderr if [[ -z "$value" ]]; then echo "Value cannot be empty." > /dev/stderr fi done echo "$value" } # read_yes_no "" [] read_yes_no() { local prompt="$1" local default="${2:-n}" local hint if [[ "$default" == "y" ]]; then hint="[Y/n]" else hint="[y/N]" fi echo -n "${prompt} ${hint}: " > /dev/stderr local ans="" read -r ans < /dev/tty if [[ -z "$ans" ]]; then ans="$default" fi case "$ans" in [Yy] | [Yy][Ee][Ss]) echo "yes" ;; *) echo "no" ;; esac } # Gate the install on explicit acceptance of the NetBird On-Premise EULA. require_eula_acceptance() { cat > /dev/stderr < /dev/stderr return 0 fi local ans="" echo -n 'Type "accept" to agree, or anything else to abort: ' > /dev/stderr read -r ans < /dev/tty if [[ "$ans" != "accept" ]]; then echo "" > /dev/stderr echo "EULA not accepted. Aborting installation." > /dev/stderr exit 1 fi echo "" > /dev/stderr } wait_postgres() { set +e echo -n "Waiting for postgres to become ready" local counter=1 while true; do if $DOCKER_COMPOSE_COMMAND exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" &> /dev/null; then break fi if [[ $counter -eq 60 ]]; then echo "" echo "Postgres is taking too long. Recent logs:" $DOCKER_COMPOSE_COMMAND logs --tail=20 postgres exit 1 fi echo -n " ." sleep 2 counter=$((counter + 1)) done echo " done" set -e } init_environment() { check_openssl DOCKER_COMPOSE_COMMAND=$(check_docker_compose) if [[ -f .env ]] || [[ -f docker-compose.yml ]] || [[ -f config.yaml ]] || [[ -f Caddyfile ]]; then echo "Generated files already exist in $(pwd)." echo "If you want to reinitialize the environment, please remove them first:" echo " $DOCKER_COMPOSE_COMMAND down --volumes # removes all containers and volumes" echo " rm -f .env docker-compose.yml Caddyfile config.yaml" echo "Be aware this will remove all data from the database." exit 1 fi require_eula_acceptance NETBIRD_EULA_ACCEPTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) echo "NetBird Enterprise bootstrap" echo "" echo "Traffic flow:" echo " Enables traffic events logging on the management server." echo " When enabled, the NetBird stack also runs NATS along with two" echo " additional containers: netbird-receiver (the traffic log receiver" echo " service) and netbird-enricher (the traffic log enricher service)." echo " It still has to be turned on from the dashboard settings afterwards." echo " See https://docs.netbird.io/manage/activity/traffic-events-logging" NETBIRD_TRAFFIC_FLOW=$(read_yes_no "Enable traffic flow" "n") echo "" NETBIRD_DOMAIN=$(read_nb_domain) echo "" NETBIRD_LICENSE_KEY=$(read_secret "Enter license key (input hidden)") GHCR_USERNAME="netbirdExtAccess1" GHCR_TOKEN=$(read_secret "Enter GHCR token (input hidden)") POSTGRES_USER="netbird" POSTGRES_DB="netbird" POSTGRES_PASSWORD=$(rand_secret) NETBIRD_ENCRYPTION_KEY=$(rand_b64_key) NETBIRD_RELAY_AUTH_SECRET=$(rand_secret) POSTGRES_DSN="host=postgres user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB} port=5432 sslmode=disable TimeZone=UTC" NETBIRD_RELAY_ENDPOINT="rels://${NETBIRD_DOMAIN}:443" echo "" echo "Selected:" echo " Traffic flow: ${NETBIRD_TRAFFIC_FLOW}" echo " Domain: ${NETBIRD_DOMAIN}" echo "" echo "Rendering files into $(pwd) ..." install -m 600 /dev/null .env render_env >> .env render_docker_compose > docker-compose.yml if [[ -z "${NETBIRD_LICENSE_SERVER_BASE_URL:-}" ]]; then sed -i.bak '/NETBIRD_LICENSE_SERVER_BASE_URL/d' docker-compose.yml && rm -f docker-compose.yml.bak fi render_caddyfile > Caddyfile install -m 600 /dev/null config.yaml render_config_yaml >> config.yaml echo "Logging in to ghcr.io ..." printf '%s' "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin unset GHCR_TOKEN echo "" echo "Pulling images ..." $DOCKER_COMPOSE_COMMAND pull echo "" echo "Starting postgres ..." $DOCKER_COMPOSE_COMMAND up -d postgres sleep 2 wait_postgres echo "" echo "Starting remaining services ..." $DOCKER_COMPOSE_COMMAND up -d echo "" echo "Done." echo "" echo "Dashboard: https://${NETBIRD_DOMAIN}" echo "" echo "Open the dashboard in a browser to complete the first-login owner setup." echo "All configuration and secrets are stored (mode 600) in $(pwd)/.env" echo "" echo "Tail logs:" echo " cd $(pwd) && $DOCKER_COMPOSE_COMMAND logs -f netbird-server caddy" } # ------------------------------------------------------------------ # Renderers # ------------------------------------------------------------------ render_env() { cat <