Files
netbird/infrastructure_files/getting-started-enterprise.sh
T

1274 lines
41 KiB
Bash
Executable File

#!/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.
# Add features to an existing install with --enable-proxy or --enable-traffic-events.
SED_STRIP_PADDING='s/=//g'
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
STACK_FILES=(.env docker-compose.yml config.yaml)
# Host directory of a custom TLS certificate mounted at /certs, see
# https://docs.netbird.io/selfhosted/enterprise/getting-started#appendix-using-a-custom-tls-certificate
CUSTOM_TLS_CERTS=""
PROXY_TOKEN_ID=""
# Static IP for Traefik inside the compose bridge network. The management
# server trusts X-Forwarded-* headers from this address only.
TRAEFIK_IP="172.30.0.10"
LICENSE_VERDICT="unknown"
LICENSE_LOG_LINES=""
check_docker_compose() {
if ! command -v docker &> /dev/null && ! command -v docker-compose &> /dev/null; then
echo "Docker 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
fi
if docker compose version &> /dev/null; then
echo "docker compose"
return
fi
if command -v docker-compose &> /dev/null && docker-compose version &> /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/compose/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
}
die() {
echo "$1" > /dev/stderr
exit 1
}
# env_get KEY [DEFAULT] prints KEY's value from .env, or DEFAULT if unset.
env_get() {
local value
value=$(sed -n "s/^$1=//p" .env | tail -n 1)
echo "${value:-$2}"
}
# merge_env upserts the KEY=VALUE lines from stdin into .env, in place.
merge_env() {
local merged
merged=$(awk -F= 'NR == FNR { v[$1] = $0; o[++n] = $1; next }
$1 in v { print v[$1]; delete v[$1]; next }
{ print }
END { for (i = 1; i <= n; i++) if (o[i] in v) print v[o[i]] }' - .env)
printf '%s\n' "$merged" > .env
}
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 "Traefik 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_letsencrypt_email() {
if [[ -n "${NETBIRD_LETSENCRYPT_EMAIL:-}" ]]; then
echo "$NETBIRD_LETSENCRYPT_EMAIL"
return
fi
local value=""
echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr
echo -n "Email address: " > /dev/stderr
read -r value < /dev/tty
if [[ -z "$value" ]]; then
echo "Email is required for Let's Encrypt." > /dev/stderr
read_letsencrypt_email
return
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 "<prompt>" [<default y|n>]
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
}
read_crowdsec_option() {
echo ""
echo "CrowdSec:"
echo " Checks client IPs against a community threat intelligence database and"
echo " blocks known malicious sources before they reach services exposed through"
echo " the proxy. Adds a CrowdSec container to the stack."
NETBIRD_CROWDSEC=$(read_yes_no "Enable CrowdSec" "n")
}
# Gate the install on explicit acceptance of the NetBird On-Premise EULA.
require_eula_acceptance() {
cat > /dev/stderr <<EOF
──────────────────────────────────────────────────────────────────────
NetBird On-Premise End User License Agreement
──────────────────────────────────────────────────────────────────────
NetBird's on-premise software is commercial software, licensed and not
sold. Your installation, deployment and use are governed by the NetBird
On-Premise End User License Agreement (the "EULA"). Please read the EULA
in full before continuing:
${NETBIRD_EULA_URL}
By typing "accept" and continuing the installation, you confirm that you
have read and agree to the EULA, that you are authorized to accept it on
behalf of your organization (the "Customer"), and that the Software is
used for business purposes only.
──────────────────────────────────────────────────────────────────────
EOF
if [[ "${NB_ACCEPT_EULA:-}" == "yes" ]]; then
echo "EULA accepted via NB_ACCEPT_EULA=yes." > /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
}
wait_for_license_verdict() {
local counter=0
local logs=""
echo -n "Waiting for the server to validate the license"
while [[ $counter -lt 60 ]]; do
logs=$($DOCKER_COMPOSE_COMMAND logs --no-color --tail=all netbird-server 2>/dev/null || true)
if grep -qi "license invalidated" <<< "$logs"; then
echo " rejected"
LICENSE_VERDICT="rejected"
LICENSE_LOG_LINES=$(grep -i "license" <<< "$logs" | tail -n 5 || true)
return 0
fi
if grep -qi "license validated" <<< "$logs"; then
echo " ok"
LICENSE_VERDICT="ok"
return 0
fi
echo -n " ."
sleep 2
counter=$((counter + 1))
done
echo " no verdict in 120s"
LICENSE_VERDICT="unknown"
LICENSE_LOG_LINES=$(grep -iE "failed to validate license|error validating license" <<< "$logs" | tail -n 3 || true)
return 0
}
report_license_verdict() {
if [[ "$LICENSE_VERDICT" == "ok" ]]; then
return 0
fi
if [[ "$LICENSE_VERDICT" == "unknown" ]]; then
echo ""
echo " ⚠ The server logged no license verdict within 120s."
if [[ -n "$LICENSE_LOG_LINES" ]]; then
echo " It was still reporting validation errors:"
while IFS= read -r line; do
[[ -n "$line" ]] && echo " $line"
done <<< "$LICENSE_LOG_LINES"
fi
echo ""
echo " Check the verdict with:"
echo ""
echo " $DOCKER_COMPOSE_COMMAND logs netbird-server | grep -i license"
return 0
fi
local unreachable="false"
if grep -qi "couldn't be validated with the license server" <<< "$LICENSE_LOG_LINES"; then
unreachable="true"
fi
echo ""
if [[ "$unreachable" == "true" ]]; then
echo " ⚠ The server could not validate the license:"
else
echo " ⚠ The server rejected the license key:"
fi
while IFS= read -r line; do
[[ -n "$line" ]] && echo " $line"
done <<< "$LICENSE_LOG_LINES"
echo ""
echo " The stack is up, and only the license check did not pass."
echo ""
if [[ "$unreachable" == "true" ]]; then
echo " The license server could not be reached, so the key itself was"
echo " never checked. Confirm this host has outbound access to the"
echo " license server, then restart:"
else
echo " Check the reason the server gave above, verify that"
echo " NETBIRD_LICENSE_KEY in .env matches the key you were issued,"
echo " then restart:"
fi
echo ""
echo " $DOCKER_COMPOSE_COMMAND up -d"
return 0
}
# up_all_but_proxy skips the proxy, which needs a token from the running server.
up_all_but_proxy() {
local services
services=$($DOCKER_COMPOSE_COMMAND config --services | grep -vx proxy)
# shellcheck disable=SC2086
$DOCKER_COMPOSE_COMMAND up -d $services
}
wait_crowdsec() {
$DOCKER_COMPOSE_COMMAND up -d crowdsec || return 1
for _ in {1..60}; do
$DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli lapi status &> /dev/null && return 0
sleep 2
done
return 1
}
admin_token() {
$DOCKER_COMPOSE_COMMAND run --rm --no-deps -T netbird-server admin token "$@" --config /etc/netbird/config.yaml
}
# revoke_proxy_token revokes the token this run minted if the proxy never started.
# On failure the ID is kept, so rollback retries it.
revoke_proxy_token() {
[[ -n "$PROXY_TOKEN_ID" ]] || return 0
if admin_token revoke "$PROXY_TOKEN_ID" > /dev/null; then
PROXY_TOKEN_ID=""
return 0
fi
echo "Could not revoke the unused proxy token ${PROXY_TOKEN_ID}. Revoke it with:" > /dev/stderr
echo " $DOCKER_COMPOSE_COMMAND run --rm netbird-server admin token revoke ${PROXY_TOKEN_ID} --config /etc/netbird/config.yaml" > /dev/stderr
}
# start_proxy mints the proxy token and CrowdSec bouncer key, then starts the proxy.
start_proxy() {
local out token key
echo "Creating the proxy access token ..."
out=$(admin_token create --name default-proxy) || true
token=$(awk '/^Token:/ {print $2}' <<< "$out")
PROXY_TOKEN_ID=$(awk '/^Token ID:/ {print $3}' <<< "$out")
[[ -n "$token" ]] || die "Could not create the proxy access token. Check the netbird-server logs, then re-run with --enable-proxy."
if [[ "$NETBIRD_CROWDSEC" == "yes" ]]; then
echo "Registering the CrowdSec bouncer ..."
if wait_crowdsec; then
# "add" fails if an earlier attempt already registered the bouncer.
$DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli bouncers delete netbird-proxy &> /dev/null || true
key=$($DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli bouncers add netbird-proxy -o raw) || true
fi
if [[ -z "$key" ]]; then
revoke_proxy_token
die "Could not register the CrowdSec bouncer. Check the crowdsec logs, then re-run with --enable-proxy."
fi
fi
# A stored token marks the proxy as set up, so it is cleared again on failure.
{
echo "NETBIRD_PROXY_TOKEN=${token}"
if [[ -n "$key" ]]; then echo "NETBIRD_CROWDSEC_BOUNCER_KEY=${key}"; fi
} | merge_env
if ! $DOCKER_COMPOSE_COMMAND up -d proxy; then
revoke_proxy_token
echo "NETBIRD_PROXY_TOKEN=" | merge_env
die "Could not start the proxy. Check the proxy logs, then re-run with --enable-proxy."
fi
PROXY_TOKEN_ID=""
}
print_proxy_notes() {
echo ""
echo "NetBird Proxy:"
echo " Every domain other than ${NETBIRD_DOMAIN} is passed through to the proxy,"
echo " which issues its own TLS certificates. Point proxy domains at this host:"
echo ""
echo " *.${NETBIRD_DOMAIN} CNAME ${NETBIRD_DOMAIN}"
echo ""
echo " Open 51820/udp (optional) for peer-to-peer proxy connections."
if [[ "$NETBIRD_CROWDSEC" == "yes" ]]; then
echo " CrowdSec is running. Enable it per service in the dashboard under Access Control."
fi
}
init_environment() {
check_openssl
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
if [[ -f .env ]] || [[ -f docker-compose.yml ]] || [[ -f config.yaml ]]; then
echo "Generated files already exist in $(pwd)."
echo "To add the proxy or traffic events to this installation, re-run with"
echo "--enable-proxy or --enable-traffic-events."
echo ""
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 -rf .env docker-compose.yml config.yaml traefik"
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 ""
echo "NetBird Proxy:"
echo " Exposes selected resources from your NetBird network to the internet."
echo " You choose which resources are exposed from the dashboard."
NETBIRD_PROXY=$(read_yes_no "Enable the NetBird Proxy" "n")
NETBIRD_CROWDSEC="no"
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
read_crowdsec_option
fi
echo ""
NETBIRD_DOMAIN=$(read_nb_domain)
echo ""
NETBIRD_LETSENCRYPT_EMAIL=$(read_letsencrypt_email)
echo ""
NETBIRD_LICENSE_KEY=$(read_secret "Enter license key (input hidden)")
POSTGRES_USER="netbird"
POSTGRES_DB="netbird"
POSTGRES_PASSWORD=$(rand_secret)
NETBIRD_ENCRYPTION_KEY=$(rand_b64_key)
NETBIRD_SESSION_COOKIE_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 " Proxy: ${NETBIRD_PROXY}"
echo " CrowdSec: ${NETBIRD_CROWDSEC}"
echo " Domain: ${NETBIRD_DOMAIN}"
echo " ACME email: ${NETBIRD_LETSENCRYPT_EMAIL}"
echo ""
echo "Rendering files into $(pwd) ..."
install -m 600 /dev/null .env
render_env >> .env
render_docker_compose > docker-compose.yml
mkdir -p traefik
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
render_traefik_proxy > traefik/proxy.yaml
fi
install -m 600 /dev/null config.yaml
render_config_yaml >> config.yaml
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 ..."
up_all_but_proxy
echo ""
wait_for_license_verdict
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
echo ""
start_proxy
fi
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"
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
print_proxy_notes
fi
echo ""
echo "Tail logs:"
echo " cd $(pwd) && $DOCKER_COMPOSE_COMMAND logs -f netbird-server traefik"
report_license_verdict
if [[ "$LICENSE_VERDICT" == "rejected" ]]; then
exit 1
fi
}
# service_block NAME prints a service's definition from the compose file on stdin.
service_block() {
local name="$1"
awk -v s=" ${name}:" '$0 == s { p = 1; print; next } p && (/^[^ ]/ || /^ [^ ]/) { exit } p'
}
# enable_features adds the proxy and/or traffic events to the install in the
# current directory, restoring the backed-up files if any step fails.
enable_features() {
local want_proxy="$1" want_flow="$2" f compose
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
for f in "${STACK_FILES[@]}"; do
[[ -f "$f" ]] || die "$f not found in $(pwd). Run this from an existing installation directory."
done
grep -q '^# Generated by getting-started-enterprise.sh' .env || die ".env was not generated by getting-started-enterprise.sh."
# Installs from before the move to Traefik run Caddy and can't be re-rendered.
[[ -n "$(env_get NETBIRD_TRAEFIK_IP)" ]] || die "This installation predates the Traefik layout and can't be updated in place."
NETBIRD_DOMAIN=$(env_get NETBIRD_DOMAIN)
NETBIRD_LICENSE_SERVER_BASE_URL=$(env_get NETBIRD_LICENSE_SERVER_BASE_URL)
NETBIRD_TRAFFIC_FLOW=$(env_get NETBIRD_TRAFFIC_FLOW_ENABLED no)
NETBIRD_PROXY=$(env_get NETBIRD_PROXY_ENABLED no)
NETBIRD_CROWDSEC=$(env_get NETBIRD_CROWDSEC_ENABLED no)
# A custom certificate counts only once Traefik mounts it and ACME is already gone,
# so the re-render never removes a working Let's Encrypt setup.
local traefik_block
traefik_block=$(service_block traefik < docker-compose.yml)
if ! grep -q certificatesresolvers <<< "$traefik_block"; then
CUSTOM_TLS_CERTS=$(awk '/:\/certs:ro$/ { sub(/^ *- /, ""); sub(/:\/certs:ro$/, ""); print; exit }' <<< "$traefik_block")
fi
if [[ "$want_flow" == "yes" && "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
echo "Traffic events are already enabled."
want_flow="no"
fi
# No token means an earlier proxy setup failed, so let it run again.
if [[ "$want_proxy" == "yes" && -n "$(env_get NETBIRD_PROXY_TOKEN)" ]]; then
echo "The NetBird Proxy is already enabled."
want_proxy="no"
fi
if [[ "$want_flow" == "no" && "$want_proxy" == "no" ]]; then
exit 0
fi
if [[ "$want_flow" == "yes" ]]; then
NETBIRD_TRAFFIC_FLOW="yes"
fi
# A retry keeps the CrowdSec choice made at install time.
if [[ "$want_proxy" == "yes" && "$NETBIRD_PROXY" != "yes" ]]; then
read_crowdsec_option
fi
if [[ "$want_proxy" == "yes" ]]; then
NETBIRD_PROXY="yes"
fi
compose=$(render_docker_compose)
echo ""
echo "Changes to docker-compose.yml:"
printf '%s\n' "$compose" | diff -u docker-compose.yml - || true
# Lines the new file drops are most likely local edits, so don't default to applying.
local lost s restarts="" apply="y"
lost=$(printf '%s\n' "$compose" | awk 'NR == FNR { keep[$0]; next } !($0 in keep)' - docker-compose.yml)
if [[ -n "$lost" ]]; then
echo ""
echo "These lines are not in the new docker-compose.yml and will be lost:"
printf '%s\n' "$lost"
apply="n"
fi
for s in $($DOCKER_COMPOSE_COMMAND config --services); do
if [[ "$(service_block "$s" < docker-compose.yml)" != "$(printf '%s\n' "$compose" | service_block "$s")" ]] \
|| [[ "$s" == "netbird-server" && "$want_flow" == "yes" ]]; then
restarts+=" $s"
fi
done
echo ""
if [[ -n "$restarts" ]]; then
echo "These services will restart:${restarts}"
fi
if [[ "$(read_yes_no "Apply these changes?" "$apply")" != "yes" ]]; then
echo "Aborted."
exit 0
fi
BACKUP_SUFFIX=".bak.$(date -u +%Y%m%d%H%M%S)"
for f in "${STACK_FILES[@]}" traefik/proxy.yaml; do
if [[ -f "$f" ]]; then cp -p "$f" "$f$BACKUP_SUFFIX"; fi
done
trap rollback EXIT
printf '%s\n' "$compose" > docker-compose.yml
{
echo "NETBIRD_TRAFFIC_FLOW_ENABLED=${NETBIRD_TRAFFIC_FLOW}"
echo "NETBIRD_PROXY_ENABLED=${NETBIRD_PROXY}"
echo "NETBIRD_CROWDSEC_ENABLED=${NETBIRD_CROWDSEC}"
if [[ "$want_flow" == "yes" ]]; then render_env_flow; fi
if [[ "$want_proxy" == "yes" ]]; then render_env_proxy; fi
} | merge_env
if [[ "$want_flow" == "yes" ]] && ! grep -q '^ trafficFlow:' config.yaml; then
render_config_flow >> config.yaml
fi
mkdir -p traefik
if [[ "$want_proxy" == "yes" ]]; then
render_traefik_proxy > traefik/proxy.yaml
fi
up_all_but_proxy
if [[ "$want_flow" == "yes" ]]; then
# Compose does not notice changes to the bind-mounted config.yaml.
$DOCKER_COMPOSE_COMMAND restart netbird-server
fi
if [[ "$want_proxy" == "yes" ]]; then
start_proxy
fi
trap - EXIT
echo ""
echo "Done. The previous files are kept with the ${BACKUP_SUFFIX} suffix."
if [[ "$want_flow" == "yes" ]]; then
echo ""
echo "Traffic events still have to be turned on from the dashboard settings."
echo " See https://docs.netbird.io/manage/activity/traffic-events-logging"
fi
if [[ "$want_proxy" == "yes" ]]; then
print_proxy_notes
fi
}
rollback() {
local f
echo "" > /dev/stderr
echo "Enabling failed. Restoring the previous configuration ..." > /dev/stderr
revoke_proxy_token
# Files without a backup were created by this run.
for f in "${STACK_FILES[@]}" traefik/proxy.yaml; do
if [[ -f "$f$BACKUP_SUFFIX" ]]; then cp -p "$f$BACKUP_SUFFIX" "$f"; else rm -f "$f"; fi
done
$DOCKER_COMPOSE_COMMAND up -d --remove-orphans
$DOCKER_COMPOSE_COMMAND restart netbird-server
}
# ------------------------------------------------------------------
# Renderers
# ------------------------------------------------------------------
render_env() {
cat <<EOF
# Generated by getting-started-enterprise.sh
# Holds all configuration and secrets for the stack. Mode 600.
# NetBird On-Premise EULA acceptance
NETBIRD_EULA_ACCEPTED=yes
NETBIRD_EULA_ACCEPTED_AT=${NETBIRD_EULA_ACCEPTED_AT}
NETBIRD_EULA_URL=${NETBIRD_EULA_URL}
# Features (change with --enable-proxy or --enable-traffic-events, not by hand)
NETBIRD_TRAFFIC_FLOW_ENABLED=${NETBIRD_TRAFFIC_FLOW}
NETBIRD_PROXY_ENABLED=${NETBIRD_PROXY}
NETBIRD_CROWDSEC_ENABLED=${NETBIRD_CROWDSEC}
# Domain
NETBIRD_DOMAIN=${NETBIRD_DOMAIN}
# Reverse proxy (Traefik)
NETBIRD_LETSENCRYPT_EMAIL=${NETBIRD_LETSENCRYPT_EMAIL}
NETBIRD_TRAEFIK_TAG=${NETBIRD_TRAEFIK_TAG:-v3.6}
NETBIRD_TRAEFIK_IP=${TRAEFIK_IP}
# Image tags. Default to "latest"
NETBIRD_DASHBOARD_TAG=${NETBIRD_DASHBOARD_TAG:-latest}
NETBIRD_SERVER_TAG=${NETBIRD_SERVER_TAG:-latest}
EOF
if [[ "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
render_env_flow
fi
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
printf '\n# NetBird Proxy (token and bouncer key are filled in after startup)\n'
render_env_proxy
fi
cat <<EOF
# License keys
EOF
if [[ -n "${NETBIRD_LICENSE_SERVER_BASE_URL:-}" ]]; then
cat <<EOF
NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
EOF
fi
cat <<EOF
NETBIRD_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
EOF
cat <<EOF
# Postgres
POSTGRES_USER=${POSTGRES_USER}
POSTGRES_DB=${POSTGRES_DB}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
NETBIRD_STORE_ENGINE_POSTGRES_DSN=${POSTGRES_DSN}
# Relay
NETBIRD_RELAY_ENDPOINT=${NETBIRD_RELAY_ENDPOINT}
NETBIRD_RELAY_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET}
# Datastore encryption
NETBIRD_ENCRYPTION_KEY=${NETBIRD_ENCRYPTION_KEY}
# Dashboard OIDC scopes
NETBIRD_AUTH_SUPPORTED_SCOPES=${NETBIRD_AUTH_SUPPORTED_SCOPES:-openid profile email groups}
EOF
}
render_env_flow() {
echo "NETBIRD_ENRICHER_TAG=${NETBIRD_ENRICHER_TAG:-latest}"
echo "NETBIRD_RECEIVER_TAG=${NETBIRD_RECEIVER_TAG:-latest}"
}
render_env_proxy() {
echo "NETBIRD_PROXY_TAG=${NETBIRD_PROXY_TAG:-latest}"
echo "NETBIRD_PROXY_TOKEN="
if [[ "$NETBIRD_CROWDSEC" == "yes" ]]; then
echo "NETBIRD_CROWDSEC_TAG=${NETBIRD_CROWDSEC_TAG:-v1.7.7}"
echo "NETBIRD_CROWDSEC_BOUNCER_KEY="
fi
}
render_docker_compose() {
{
render_compose_header
render_compose_common
render_compose_server
if [[ "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
render_compose_flow
fi
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
render_compose_proxy
fi
render_compose_postgres
render_compose_footer
} | if [[ -n "${NETBIRD_LICENSE_SERVER_BASE_URL:-}" ]]; then cat; else sed '/NETBIRD_LICENSE_SERVER_BASE_URL/d'; fi \
| if [[ -n "$CUSTOM_TLS_CERTS" ]]; then sed -e '/certificatesresolvers/d' -e '/certresolver/d'; else cat; fi
}
render_compose_header() {
cat <<'EOF'
x-default: &default
restart: unless-stopped
logging:
driver: json-file
options:
max-size: '500m'
max-file: '2'
services:
EOF
}
render_compose_common() {
cat <<'EOF'
# Reverse proxy with automatic TLS via Let's Encrypt. Routes are declared as
# labels on the services below and picked up through the Docker provider.
traefik:
<<: *default
image: traefik:${NETBIRD_TRAEFIK_TAG}
container_name: netbird-traefik
networks:
netbird:
ipv4_address: ${NETBIRD_TRAEFIK_IP}
command:
# Logging
- "--log.level=INFO"
- "--accesslog=true"
# Docker provider
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=netbird"
# Entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.allowACMEByPass=true"
# readTimeout bounds the whole request, and gRPC streams / relay WebSockets
# never end one; idleTimeout would close the keep-alive connection they
# are reused over. Entrypoint-wide is the only scope Traefik offers here.
# writeTimeout is left alone: it already defaults to 0.
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
- "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
# HTTP to HTTPS redirect
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Let's Encrypt ACME
- "--certificatesresolvers.letsencrypt.acme.email=${NETBIRD_LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
# Dynamic config in ./traefik: the proxy transport and an optional custom certificate
- "--providers.file.directory=/etc/traefik/dynamic"
ports:
- '443:443'
- '80:80'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- netbird_traefik_letsencrypt:/letsencrypt
- ./traefik:/etc/traefik/dynamic:ro
EOF
if [[ -n "$CUSTOM_TLS_CERTS" ]]; then
echo " - ${CUSTOM_TLS_CERTS}:/certs:ro"
fi
cat <<'EOF'
labels:
- traefik.enable=true
# Shared security headers, referenced by every NetBird router below. A
# label-declared middleware only exists while its container runs, so this
# lives on Traefik itself: declaring it on an app container would drop
# every router referencing it whenever that container restarts.
- traefik.http.middlewares.nb-security.headers.stsSeconds=3600
- traefik.http.middlewares.nb-security.headers.stsIncludeSubdomains=true
- traefik.http.middlewares.nb-security.headers.contentTypeNosniff=true
- traefik.http.middlewares.nb-security.headers.browserXssFilter=true
- traefik.http.middlewares.nb-security.headers.referrerPolicy=strict-origin-when-cross-origin
- traefik.http.middlewares.nb-security.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN
# Empty value strips the header. Only the dashboard's nginx sets one; the
# server emits none. Do not quote it — "" would send a literal Server: "".
- traefik.http.middlewares.nb-security.headers.customResponseHeaders.Server=
dashboard:
<<: *default
image: ghcr.io/netbirdio/dashboard-cloud:${NETBIRD_DASHBOARD_TAG}
container_name: netbird-dashboard
networks: [netbird]
labels:
- traefik.enable=true
# Dashboard catch-all: lowest priority so every route below wins
- traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN}`)
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
- traefik.http.routers.netbird-dashboard.tls=true
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-dashboard.middlewares=nb-security@docker
- traefik.http.routers.netbird-dashboard.service=dashboard
- traefik.http.routers.netbird-dashboard.priority=1
- traefik.http.services.dashboard.loadbalancer.server.port=80
environment:
- NETBIRD_MGMT_API_ENDPOINT=https://${NETBIRD_DOMAIN}
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${NETBIRD_DOMAIN}
- AUTH_AUDIENCE=netbird-dashboard
- AUTH_CLIENT_ID=netbird-dashboard
- AUTH_CLIENT_SECRET=
- AUTH_AUTHORITY=https://${NETBIRD_DOMAIN}/oauth2
- USE_AUTH0=false
- AUTH_SUPPORTED_SCOPES=${NETBIRD_AUTH_SUPPORTED_SCOPES}
- AUTH_REDIRECT_URI=/nb-auth
- AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
- NETBIRD_TOKEN_SOURCE=accessToken
- NGINX_SSL_PORT=443
- LETSENCRYPT_DOMAIN=
- LETSENCRYPT_EMAIL=
EOF
}
render_compose_server() {
cat <<'EOF'
netbird-server:
<<: *default
image: ghcr.io/netbirdio/netbird-server-cloud:${NETBIRD_SERVER_TAG}
container_name: netbird-server
networks: [netbird]
depends_on:
dashboard:
condition: service_started
postgres:
condition: service_healthy
ports:
- '3478:3478/udp'
volumes:
- netbird_data:/var/lib/netbird
- ./config.yaml:/etc/netbird/config.yaml
command: ["--config", "/etc/netbird/config.yaml"]
labels:
- traefik.enable=true
# Signal + Management gRPC (needs an h2c backend for HTTP/2 cleartext)
- traefik.http.routers.netbird-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
- traefik.http.routers.netbird-grpc.entrypoints=websecure
- traefik.http.routers.netbird-grpc.tls=true
- traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-grpc.middlewares=nb-security@docker
- traefik.http.routers.netbird-grpc.service=netbird-server-h2c
- traefik.http.routers.netbird-grpc.priority=100
# Relay WebSocket, management API, and the embedded IdP
- traefik.http.routers.netbird-backend.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))
- traefik.http.routers.netbird-backend.entrypoints=websecure
- traefik.http.routers.netbird-backend.tls=true
- traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-backend.middlewares=nb-security@docker
- traefik.http.routers.netbird-backend.service=netbird-server
- traefik.http.routers.netbird-backend.priority=100
# Services
- traefik.http.services.netbird-server.loadbalancer.server.port=80
- traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
- traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c
environment:
- NB_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
- NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
EOF
}
render_compose_flow() {
cat <<'EOF'
nats:
<<: *default
image: nats:2
container_name: netbird-nats
networks: [netbird]
volumes:
- netbird_nats_data:/data
command: ["-m", "8222", "--jetstream", "--store_dir", "/data"]
enricher:
<<: *default
image: ghcr.io/netbirdio/flow-enricher-cloud:${NETBIRD_ENRICHER_TAG}
container_name: netbird-enricher
networks: [netbird]
depends_on:
postgres:
condition: service_healthy
nats:
condition: service_started
volumes:
- netbird_enricher:/var/lib/netbird
environment:
- NB_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
- NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
- NB_DATADIR=/var/lib/netbird
- NB_MANAGEMENT_STORE_ENGINE=postgres
- NB_MANAGEMENT_POSTGRES_DSN=${NETBIRD_STORE_ENGINE_POSTGRES_DSN}
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=${NETBIRD_STORE_ENGINE_POSTGRES_DSN}
- NB_TRAFFIC_EVENT_POSTGRES_DSN=${NETBIRD_STORE_ENGINE_POSTGRES_DSN}
- NB_TRAFFIC_EVENT_STORE_ENGINE=postgres
- NB_MANAGEMENT_STORE_KEY=${NETBIRD_ENCRYPTION_KEY}
- NB_FLOW_ADAPTER_TYPE=nats
- NB_FLOW_NATS_ENDPOINTS=nats://nats:4222
- NB_FLOW_NATS_STREAM=traffic-events
- NB_METRICS_PORT=9091
- NB_PERSISTENCE_RETENTION_PERIOD=168h
receiver:
<<: *default
image: ghcr.io/netbirdio/flow-receiver-cloud:${NETBIRD_RECEIVER_TAG}
container_name: netbird-receiver
networks: [netbird]
depends_on:
nats:
condition: service_started
environment:
- NB_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
- NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
- NB_FLOW_LISTEN_PORT=80
- NB_FLOW_ADAPTER_TYPE=nats
- NB_FLOW_NATS_ENDPOINTS=nats://nats:4222
- NB_FLOW_NATS_STREAM=traffic-events
- NB_FLOW_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET}
labels:
- traefik.enable=true
# Flow receiver gRPC (h2c backend)
- traefik.http.routers.netbird-flow.rule=Host(`${NETBIRD_DOMAIN}`) && PathPrefix(`/flow.FlowService/`)
- traefik.http.routers.netbird-flow.entrypoints=websecure
- traefik.http.routers.netbird-flow.tls=true
- traefik.http.routers.netbird-flow.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-flow.middlewares=nb-security@docker
- traefik.http.routers.netbird-flow.service=netbird-flow-h2c
- traefik.http.routers.netbird-flow.priority=100
- traefik.http.services.netbird-flow-h2c.loadbalancer.server.port=80
- traefik.http.services.netbird-flow-h2c.loadbalancer.server.scheme=h2c
EOF
}
render_compose_proxy() {
cat <<'EOF'
# Traefik passes TLS for every other domain through to the proxy, which issues
# its own certificates. PROXY protocol v2 preserves the client IP.
proxy:
<<: *default
image: ghcr.io/netbirdio/reverse-proxy:${NETBIRD_PROXY_TAG}
container_name: netbird-proxy
networks: [netbird]
depends_on:
netbird-server:
condition: service_started
ports:
- '51820:51820/udp'
volumes:
- netbird_proxy_certs:/certs
labels:
- traefik.enable=true
- traefik.tcp.routers.proxy-passthrough.rule=HostSNI(`*`)
- traefik.tcp.routers.proxy-passthrough.entrypoints=websecure
- 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
environment:
# Plaintext gRPC over the internal network, not the public address.
- NB_PROXY_MANAGEMENT_ADDRESS=http://netbird-server:80
- NB_PROXY_ALLOW_INSECURE=true
- NB_PROXY_TOKEN=${NETBIRD_PROXY_TOKEN}
- NB_PROXY_DOMAIN=${NETBIRD_DOMAIN}
- NB_PROXY_ADDRESS=:8443
- NB_PROXY_CERTIFICATE_DIRECTORY=/certs
- NB_PROXY_ACME_CERTIFICATES=true
- NB_PROXY_FORWARDED_PROTO=https
- NB_PROXY_PROXY_PROTOCOL=true
- NB_PROXY_TRUSTED_PROXIES=${NETBIRD_TRAEFIK_IP}
EOF
if [[ "$NETBIRD_CROWDSEC" == "yes" ]]; then
cat <<'EOF'
- NB_PROXY_CROWDSEC_API_URL=http://crowdsec:8080
- NB_PROXY_CROWDSEC_API_KEY=${NETBIRD_CROWDSEC_BOUNCER_KEY}
crowdsec:
<<: *default
image: crowdsecurity/crowdsec:${NETBIRD_CROWDSEC_TAG}
container_name: netbird-crowdsec
networks: [netbird]
environment:
- COLLECTIONS=crowdsecurity/linux
volumes:
- netbird_crowdsec_config:/etc/crowdsec
- netbird_crowdsec_data:/var/lib/crowdsec/data
healthcheck:
test: ["CMD", "cscli", "lapi", "status"]
interval: 10s
timeout: 5s
retries: 15
EOF
fi
echo ""
}
render_compose_postgres() {
cat <<'EOF'
postgres:
<<: *default
image: postgres:17
container_name: netbird-postgres
networks: [netbird]
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
volumes:
- netbird_postgres:/var/lib/postgresql/data
EOF
}
render_compose_footer() {
cat <<'EOF'
volumes:
netbird_data:
EOF
if [[ "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
cat <<'EOF'
netbird_nats_data:
netbird_enricher:
EOF
fi
if [[ "$NETBIRD_PROXY" == "yes" ]]; then
echo " netbird_proxy_certs:"
fi
if [[ "$NETBIRD_CROWDSEC" == "yes" ]]; then
printf ' %s:\n' netbird_crowdsec_config netbird_crowdsec_data
fi
cat <<'EOF'
netbird_postgres:
netbird_traefik_letsencrypt:
networks:
netbird:
name: netbird
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
gateway: 172.30.0.1
EOF
}
render_config_yaml() {
cat <<EOF
# NetBird Enterprise server configuration.
# Generated by getting-started-enterprise.sh. Mode 600.
server:
listenAddress: ":80"
exposedAddress: "https://${NETBIRD_DOMAIN}:443"
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: "info"
logFile: "console"
# TLS is terminated by Traefik in front; leave this block empty.
tls:
certFile: ""
keyFile: ""
letsencrypt:
enabled: false
authSecret: "${NETBIRD_RELAY_AUTH_SECRET}"
dataDir: "/var/lib/netbird/"
disableAnonymousMetrics: false
disableGeoliteUpdate: false
auth:
issuer: "https://${NETBIRD_DOMAIN}/oauth2"
localAuthDisabled: false
signKeyRefreshEnabled: false
sessionCookieEncryptionKey: "${NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY}"
dashboardRedirectURIs:
- "https://${NETBIRD_DOMAIN}/nb-auth"
- "https://${NETBIRD_DOMAIN}/nb-silent-auth"
cliRedirectURIs:
- "http://localhost:53000/"
# Trust X-Forwarded-* only from the Traefik container's static address. Both
# keys must stay in step with the ipv4_address pinned in docker-compose.yml:
# trustedPeers restricts which sources may supply forwarded headers. Leaving
# it unset trusts all IPv4 and IPv6 sources.
reverseProxy:
trustedPeers:
- "${TRAEFIK_IP}/32"
trustedHTTPProxies:
- "${TRAEFIK_IP}/32"
store:
engine: "postgres"
dsn: "${POSTGRES_DSN}"
encryptionKey: "${NETBIRD_ENCRYPTION_KEY}"
activityStore:
engine: "postgres"
dsn: "${POSTGRES_DSN}"
EOF
if [[ "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
render_config_flow
fi
}
render_config_flow() {
cat <<EOF
trafficFlow:
enabled: true
address: "https://${NETBIRD_DOMAIN}:443"
interval: "60s"
EOF
}
render_traefik_proxy() {
cat <<'EOF'
tcp:
serversTransports:
pp-v2:
proxyProtocol:
version: 2
EOF
}
usage() {
cat <<EOF
Usage: $0 [--enable-proxy] [--enable-traffic-events]
Without flags, bootstraps a new NetBird Enterprise stack in the current
directory. With flags, turns the feature on for the existing installation in
the current directory.
--enable-proxy add the NetBird Proxy, optionally with CrowdSec
--enable-traffic-events add traffic events logging (NATS, receiver, enricher)
-h, --help show this help
EOF
}
main() {
local enable_proxy="no" enable_flow="no"
while [[ $# -gt 0 ]]; do
case "$1" in
--enable-proxy) enable_proxy="yes" ;;
--enable-traffic-events) enable_flow="yes" ;;
-h | --help) usage; exit 0 ;;
*) usage > /dev/stderr; exit 1 ;;
esac
shift
done
if [[ "$enable_proxy" == "no" && "$enable_flow" == "no" ]]; then
init_environment
else
enable_features "$enable_proxy" "$enable_flow"
fi
}
main "$@"