960 lines
33 KiB
Bash
Executable file
960 lines
33 KiB
Bash
Executable file
#!/bin/bash
|
||
|
||
set -e
|
||
|
||
# ============================================
|
||
# CONFIG
|
||
# ============================================
|
||
TRAEFIK_NETWORK="traefik_traefik"
|
||
TRAEFIK_CERTRESOLVER="letsencrypt"
|
||
NETBIRD_DOMAIN="netbird.rozic-dev.com"
|
||
export NETBIRD_DOMAIN
|
||
|
||
# ============================================
|
||
# ERROR HANDLING HELPERS
|
||
# ============================================
|
||
handle_request_command_status() {
|
||
PARSED_RESPONSE=$1
|
||
FUNCTION_NAME=$2
|
||
RESPONSE=$3
|
||
if [[ $PARSED_RESPONSE -ne 0 ]]; then
|
||
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
handle_zitadel_request_response() {
|
||
PARSED_RESPONSE=$1
|
||
FUNCTION_NAME=$2
|
||
RESPONSE=$3
|
||
if [[ $PARSED_RESPONSE == "null" ]]; then
|
||
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
}
|
||
|
||
# ============================================
|
||
# DEPENDENCY CHECKS
|
||
# ============================================
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
# ============================================
|
||
# WAIT HELPERS
|
||
# ============================================
|
||
wait_pat() {
|
||
PAT_PATH=$1
|
||
set +e
|
||
while true; do
|
||
if [[ -f "$PAT_PATH" ]]; then
|
||
break
|
||
fi
|
||
echo -n " ."
|
||
sleep 1
|
||
done
|
||
echo " done"
|
||
set -e
|
||
}
|
||
|
||
wait_api() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
set +e
|
||
counter=1
|
||
while true; do
|
||
FLAGS="-s"
|
||
if [[ $counter -eq 120 ]]; then
|
||
FLAGS="-v"
|
||
echo ""
|
||
fi
|
||
curl $FLAGS --connect-timeout 5 -o /dev/null \
|
||
http://localhost:8085/auth/v1/users/me \
|
||
-H "Authorization: Bearer $PAT" 2>/dev/null
|
||
if [[ $? -eq 0 ]]; then
|
||
break
|
||
fi
|
||
if [[ $counter -eq 120 ]]; then
|
||
echo "Zitadel still not ready via http://localhost:8085 after 120s – checking logs..."
|
||
docker logs netbird-compose-zitadel-1 --tail 50
|
||
exit 1
|
||
fi
|
||
echo -n " ."
|
||
sleep 2
|
||
counter=$((counter + 1))
|
||
done
|
||
echo " done"
|
||
set -e
|
||
}
|
||
|
||
# ============================================
|
||
# ZITADEL API HELPERS
|
||
# ============================================
|
||
create_new_project() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
PROJECT_NAME="NETBIRD"
|
||
RESPONSE=$(
|
||
curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name": "'"$PROJECT_NAME"'"}'
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
create_new_application() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
PROJECT_ID=$3
|
||
APPLICATION_NAME=$4
|
||
BASE_REDIRECT_URL1=$5
|
||
BASE_REDIRECT_URL2=$6
|
||
LOGOUT_URL=$7
|
||
ZITADEL_DEV_MODE=$8
|
||
DEVICE_CODE=$9
|
||
|
||
if [[ $DEVICE_CODE == "true" ]]; then
|
||
GRANT_TYPES='["OIDC_GRANT_TYPE_AUTHORIZATION_CODE","OIDC_GRANT_TYPE_DEVICE_CODE","OIDC_GRANT_TYPE_REFRESH_TOKEN"]'
|
||
else
|
||
GRANT_TYPES='["OIDC_GRANT_TYPE_AUTHORIZATION_CODE","OIDC_GRANT_TYPE_REFRESH_TOKEN"]'
|
||
fi
|
||
|
||
RESPONSE=$(
|
||
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"name": "'"$APPLICATION_NAME"'",
|
||
"redirectUris": [
|
||
"'"$BASE_REDIRECT_URL1"'",
|
||
"'"$BASE_REDIRECT_URL2"'"
|
||
],
|
||
"postLogoutRedirectUris": [
|
||
"'"$LOGOUT_URL"'"
|
||
],
|
||
"RESPONSETypes": [
|
||
"OIDC_RESPONSE_TYPE_CODE"
|
||
],
|
||
"grantTypes": '"$GRANT_TYPES"',
|
||
"appType": "OIDC_APP_TYPE_USER_AGENT",
|
||
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
|
||
"version": "OIDC_VERSION_1_0",
|
||
"devMode": '"$ZITADEL_DEV_MODE"',
|
||
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
|
||
"accessTokenRoleAssertion": true,
|
||
"skipNativeAppSuccessPage": true
|
||
}'
|
||
)
|
||
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
create_service_user() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
RESPONSE=$(
|
||
curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"userName": "netbird-service-account",
|
||
"name": "Netbird Service Account",
|
||
"description": "Netbird Service Account for IDP management",
|
||
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
|
||
}'
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
create_service_user_secret() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
USER_ID=$3
|
||
RESPONSE=$(
|
||
curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{}'
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
|
||
handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
|
||
SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
|
||
handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
|
||
}
|
||
|
||
add_organization_user_manager() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
USER_ID=$3
|
||
RESPONSE=$(
|
||
curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"userId": "'"$USER_ID"'",
|
||
"roles": [
|
||
"ORG_USER_MANAGER"
|
||
]
|
||
}'
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
create_admin_user() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
USERNAME=$3
|
||
PASSWORD=$4
|
||
RESPONSE=$(
|
||
curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"userName": "'"$USERNAME"'",
|
||
"profile": {
|
||
"firstName": "Zitadel",
|
||
"lastName": "Admin"
|
||
},
|
||
"email": {
|
||
"email": "'"$USERNAME"'",
|
||
"isEmailVerified": true
|
||
},
|
||
"password": "'"$PASSWORD"'",
|
||
"passwordChangeRequired": true
|
||
}'
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
add_instance_admin() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
USER_ID=$3
|
||
RESPONSE=$(
|
||
curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"userId": "'"$USER_ID"'",
|
||
"roles": [
|
||
"IAM_OWNER"
|
||
]
|
||
}'
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
delete_auto_service_user() {
|
||
INSTANCE_URL=$1
|
||
PAT=$2
|
||
RESPONSE=$(
|
||
curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
|
||
handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"
|
||
RESPONSE=$(
|
||
curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"
|
||
RESPONSE=$(
|
||
curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
|
||
-H "Authorization: Bearer $PAT" \
|
||
-H "Content-Type: application/json" \
|
||
)
|
||
# Log the full response for debugging
|
||
echo "$RESPONSE" >> zitadel_api.log
|
||
# Check if response is valid JSON
|
||
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" >> zitadel_api.log
|
||
echo "ERROR: Invalid response from Zitadel API: $RESPONSE" > /dev/stderr
|
||
exit 1
|
||
fi
|
||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
|
||
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
|
||
echo "$PARSED_RESPONSE"
|
||
}
|
||
|
||
# ============================================
|
||
# TURN IP
|
||
# ============================================
|
||
get_turn_external_ip() {
|
||
TURN_EXTERNAL_IP_CONFIG="#external-ip="
|
||
IP=$(curl -s -4 https://jsonip.com | jq -r '.ip')
|
||
if [[ "x-$IP" != "x-" ]]; then
|
||
TURN_EXTERNAL_IP_CONFIG="external-ip=$IP"
|
||
fi
|
||
echo "$TURN_EXTERNAL_IP_CONFIG"
|
||
}
|
||
|
||
# ============================================
|
||
# MAIN
|
||
# ============================================
|
||
main() {
|
||
# Clear log file
|
||
> zitadel_api.log
|
||
|
||
echo "Initializing NetBird with Traefik..."
|
||
|
||
check_jq
|
||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||
|
||
if [ -f zitadel.env ]; 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 zitadel.env zdb.env dashboard.env management.json relay.env turnserver.conf machinekey/zitadel-admin-sa.token"
|
||
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||
exit 1
|
||
fi
|
||
|
||
# === Generate passwords & secrets ===
|
||
ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
|
||
POSTGRES_ROOT_PASSWORD="$(openssl rand -base64 32 | sed 's/=//g')@"
|
||
POSTGRES_ZITADEL_PASSWORD="$(openssl rand -base64 32 | sed 's/=//g')@"
|
||
TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
|
||
NETBIRD_RELAY_AUTH_SECRET=$(openssl rand -base64 32 | sed 's/=//g')
|
||
ZITADEL_ADMIN_USERNAME="admin@$NETBIRD_DOMAIN"
|
||
ZITADEL_ADMIN_PASSWORD="$(openssl rand -base64 32 | sed 's/=//g')@"
|
||
TURN_EXTERNAL_IP_CONFIG=$(get_turn_external_ip)
|
||
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
|
||
else
|
||
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
|
||
fi
|
||
|
||
echo "Generating configuration files..."
|
||
|
||
# === zitadel.env ===
|
||
cat > zitadel.env <<EOF
|
||
ZITADEL_LOG_LEVEL=debug
|
||
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
|
||
ZITADEL_EXTERNALSECURE=true
|
||
ZITADEL_TLS_ENABLED=false
|
||
ZITADEL_EXTERNALPORT=443
|
||
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
|
||
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
|
||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
|
||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
|
||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
|
||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
|
||
ZITADEL_DATABASE_POSTGRES_HOST=zdb
|
||
ZITADEL_DATABASE_POSTGRES_PORT=5432
|
||
ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel
|
||
ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel
|
||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=$POSTGRES_ZITADEL_PASSWORD
|
||
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable
|
||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root
|
||
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=$POSTGRES_ROOT_PASSWORD
|
||
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable
|
||
NETBIRD_DOMAIN=$NETBIRD_DOMAIN
|
||
EOF
|
||
|
||
# === zdb.env ===
|
||
cat > zdb.env <<EOF
|
||
POSTGRES_USER=root
|
||
POSTGRES_PASSWORD=$POSTGRES_ROOT_PASSWORD
|
||
EOF
|
||
|
||
# === turnserver.conf ===
|
||
cat > turnserver.conf <<EOF
|
||
listening-port=3478
|
||
$TURN_EXTERNAL_IP_CONFIG
|
||
tls-listening-port=5349
|
||
min-port=49152
|
||
max-port=65535
|
||
fingerprint
|
||
lt-cred-mech
|
||
user=self:$TURN_PASSWORD
|
||
realm=wiretrustee.com
|
||
cert=/etc/coturn/certs/cert.pem
|
||
pkey=/etc/coturn/private/privkey.pem
|
||
log-file=stdout
|
||
no-software-attribute
|
||
pidfile="/var/tmp/turnserver.pid"
|
||
no-cli
|
||
EOF
|
||
|
||
# === relay.env ===
|
||
cat > relay.env <<EOF
|
||
NB_LOG_LEVEL=info
|
||
NB_LISTEN_ADDRESS=:33080
|
||
NB_EXPOSED_ADDRESS=rels://$NETBIRD_DOMAIN:443/relay
|
||
NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET
|
||
NETBIRD_DOMAIN=$NETBIRD_DOMAIN
|
||
EOF
|
||
|
||
# Temporary empty files
|
||
echo "" > dashboard.env
|
||
echo "" > management.json
|
||
|
||
# === docker-compose.yml template ===
|
||
cat > docker-compose.yml <<'EOF'
|
||
services:
|
||
# UI dashboard
|
||
dashboard:
|
||
image: netbirdio/dashboard:latest
|
||
restart: unless-stopped
|
||
networks:
|
||
- netbird
|
||
- traefik_traefik
|
||
env_file:
|
||
- ./dashboard.env
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.docker.network=traefik_default
|
||
- traefik.http.services.netbird-dashboard.loadbalancer.server.port=80
|
||
- traefik.http.routers.netbird-dashboard.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`)
|
||
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
|
||
- traefik.http.routers.netbird-dashboard.tls=true
|
||
- traefik.http.routers.netbird-dashboard.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-dashboard.priority=50
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
# Signal
|
||
signal:
|
||
image: netbirdio/signal:latest
|
||
restart: unless-stopped
|
||
networks:
|
||
- netbird
|
||
- traefik_traefik
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.docker.network=traefik_default
|
||
- traefik.http.services.netbird-signal.loadbalancer.server.port=10000
|
||
- traefik.http.services.netbird-signal.loadbalancer.server.scheme=h2c
|
||
- traefik.http.routers.netbird-signal.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/signalexchange.SignalExchange/`)
|
||
- traefik.http.routers.netbird-signal.entrypoints=websecure
|
||
- traefik.http.routers.netbird-signal.service=netbird-signal
|
||
- traefik.http.routers.netbird-signal.tls=true
|
||
- traefik.http.routers.netbird-signal.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-signal.priority=200
|
||
# WebSocket route for signal - pass through without stripping prefix
|
||
- traefik.http.services.netbird-signal-ws.loadbalancer.server.port=80
|
||
- traefik.http.routers.netbird-signal-ws.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/ws-proxy/signal`)
|
||
- traefik.http.routers.netbird-signal-ws.entrypoints=websecure
|
||
- traefik.http.routers.netbird-signal-ws.service=netbird-signal-ws
|
||
- traefik.http.routers.netbird-signal-ws.tls=true
|
||
- traefik.http.routers.netbird-signal-ws.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-signal-ws.priority=300
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
# Relay
|
||
relay:
|
||
image: netbirdio/relay:latest
|
||
restart: unless-stopped
|
||
networks:
|
||
- netbird
|
||
- traefik_traefik
|
||
env_file:
|
||
- ./relay.env
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.docker.network=traefik_default
|
||
- traefik.http.services.netbird-relay.loadbalancer.server.port=33080
|
||
- traefik.http.routers.netbird-relay.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/relay`)
|
||
- traefik.http.routers.netbird-relay.entrypoints=websecure
|
||
- traefik.http.routers.netbird-relay.tls=true
|
||
- traefik.http.routers.netbird-relay.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-relay.priority=200
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
# Management
|
||
management:
|
||
image: netbirdio/management:latest
|
||
restart: unless-stopped
|
||
networks:
|
||
- netbird
|
||
- traefik_traefik
|
||
volumes:
|
||
- netbird_management:/var/lib/netbird
|
||
- ./management.json:/etc/netbird/management.json
|
||
command: [
|
||
"--port", "80",
|
||
"--log-file", "console",
|
||
"--log-level", "info",
|
||
"--disable-anonymous-metrics=false",
|
||
"--single-account-mode-domain=netbird.selfhosted",
|
||
"--dns-domain=netbird.selfhosted",
|
||
"--idp-sign-key-refresh-enabled"
|
||
]
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.docker.network=traefik_default
|
||
- traefik.http.services.netbird-management.loadbalancer.server.port=80
|
||
- traefik.http.services.netbird-management-grpc.loadbalancer.server.port=80
|
||
- traefik.http.services.netbird-management-grpc.loadbalancer.server.scheme=h2c
|
||
# REST API
|
||
- traefik.http.routers.netbird-api.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/api`)
|
||
- traefik.http.routers.netbird-api.entrypoints=websecure
|
||
- traefik.http.routers.netbird-api.service=netbird-management
|
||
- traefik.http.routers.netbird-api.tls=true
|
||
- traefik.http.routers.netbird-api.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-api.priority=200
|
||
# gRPC
|
||
- traefik.http.routers.netbird-management-grpc.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/management.ManagementService/`)
|
||
- traefik.http.routers.netbird-management-grpc.entrypoints=websecure
|
||
- traefik.http.routers.netbird-management-grpc.service=netbird-management-grpc
|
||
- traefik.http.routers.netbird-management-grpc.tls=true
|
||
- traefik.http.routers.netbird-management-grpc.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-management-grpc.priority=200
|
||
# WebSocket route for management
|
||
- traefik.http.routers.netbird-management-ws.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/ws-proxy/management`)
|
||
- traefik.http.routers.netbird-management-ws.entrypoints=websecure
|
||
- traefik.http.routers.netbird-management-ws.service=netbird-management
|
||
- traefik.http.routers.netbird-management-ws.tls=true
|
||
- traefik.http.routers.netbird-management-ws.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.netbird-management-ws.priority=300
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
# Coturn
|
||
coturn:
|
||
image: coturn/coturn
|
||
restart: unless-stopped
|
||
volumes:
|
||
- ./turnserver.conf:/etc/turnserver.conf:ro
|
||
network_mode: host
|
||
command:
|
||
- -c /etc/turnserver.conf
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
# Zitadel - identity provider
|
||
zitadel:
|
||
restart: 'always'
|
||
image: 'ghcr.io/zitadel/zitadel:v2.59.3'
|
||
command: 'start-from-init --masterkeyFromEnv --tlsMode external'
|
||
ports:
|
||
- "8085:8080" # <-- added
|
||
env_file:
|
||
- ./zitadel.env
|
||
environment:
|
||
ZITADEL_PROJECTIONS_MAXFAILURES: "3"
|
||
ZITADEL_PROJECTIONS_RETRYDELAY: "2s"
|
||
ZITADEL_PROJECTIONS_MAXPARALLELPROJECTIONS: "4"
|
||
ZITADEL_PROJECTIONS_TARGETS1_PARALLEL_PREFILLS: "2"
|
||
ZITADEL_PROJECTIONS_TARGETS1_BATCHSIZE: "500"
|
||
depends_on:
|
||
zdb:
|
||
condition: 'service_healthy'
|
||
volumes:
|
||
- ./machinekey:/machinekey
|
||
- netbird_zitadel_certs:/zdb-certs:ro
|
||
networks:
|
||
- netbird
|
||
- traefik_traefik
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.docker.network=traefik_default
|
||
- traefik.http.services.zitadel.loadbalancer.server.port=8080
|
||
- traefik.http.services.zitadel.loadbalancer.server.scheme=h2c
|
||
# OIDC wellknown
|
||
- traefik.http.routers.zitadel-wellknown.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/.well-known`)
|
||
- traefik.http.routers.zitadel-wellknown.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-wellknown.service=zitadel
|
||
- traefik.http.routers.zitadel-wellknown.priority=300
|
||
- traefik.http.routers.zitadel-wellknown.tls=true
|
||
- traefik.http.routers.zitadel-wellknown.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# OAuth
|
||
- traefik.http.routers.zitadel-oauth.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/oauth`)
|
||
- traefik.http.routers.zitadel-oauth.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-oauth.service=zitadel
|
||
- traefik.http.routers.zitadel-oauth.priority=300
|
||
- traefik.http.routers.zitadel-oauth.tls=true
|
||
- traefik.http.routers.zitadel-oauth.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# OIDC
|
||
- traefik.http.routers.zitadel-oidc.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/oidc`)
|
||
- traefik.http.routers.zitadel-oidc.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-oidc.service=zitadel
|
||
- traefik.http.routers.zitadel-oidc.priority=300
|
||
- traefik.http.routers.zitadel-oidc.tls=true
|
||
- traefik.http.routers.zitadel-oidc.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# UI Console
|
||
- traefik.http.routers.zitadel-ui.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/ui`)
|
||
- traefik.http.routers.zitadel-ui.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-ui.service=zitadel
|
||
- traefik.http.routers.zitadel-ui.priority=300
|
||
- traefik.http.routers.zitadel-ui.tls=true
|
||
- traefik.http.routers.zitadel-ui.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# Device flow
|
||
- traefik.http.routers.zitadel-device.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/device`)
|
||
- traefik.http.routers.zitadel-device.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-device.service=zitadel
|
||
- traefik.http.routers.zitadel-device.priority=300
|
||
- traefik.http.routers.zitadel-device.tls=true
|
||
- traefik.http.routers.zitadel-device.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# Management API
|
||
- traefik.http.routers.zitadel-mgmt.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/management/v1`)
|
||
- traefik.http.routers.zitadel-mgmt.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-mgmt.service=zitadel
|
||
- traefik.http.routers.zitadel-mgmt.priority=300
|
||
- traefik.http.routers.zitadel-mgmt.tls=true
|
||
- traefik.http.routers.zitadel-mgmt.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# Admin API
|
||
- traefik.http.routers.zitadel-admin.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/admin/v1`)
|
||
- traefik.http.routers.zitadel-admin.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-admin.service=zitadel
|
||
- traefik.http.routers.zitadel-admin.priority=300
|
||
- traefik.http.routers.zitadel-admin.tls=true
|
||
- traefik.http.routers.zitadel-admin.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# gRPC endpoints
|
||
- traefik.http.routers.zitadel-grpc-auth.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/zitadel.auth.v1.AuthService/`)
|
||
- traefik.http.routers.zitadel-grpc-auth.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-grpc-auth.service=zitadel
|
||
- traefik.http.routers.zitadel-grpc-auth.priority=400
|
||
- traefik.http.routers.zitadel-grpc-auth.tls=true
|
||
- traefik.http.routers.zitadel-grpc-auth.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.zitadel-grpc-admin.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/zitadel.admin.v1.AdminService/`)
|
||
- traefik.http.routers.zitadel-grpc-admin.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-grpc-admin.service=zitadel
|
||
- traefik.http.routers.zitadel-grpc-admin.priority=400
|
||
- traefik.http.routers.zitadel-grpc-admin.tls=true
|
||
- traefik.http.routers.zitadel-grpc-admin.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.zitadel-grpc-mgmt.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/zitadel.management.v1.ManagementService/`)
|
||
- traefik.http.routers.zitadel-grpc-mgmt.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-grpc-mgmt.service=zitadel
|
||
- traefik.http.routers.zitadel-grpc-mgmt.priority=400
|
||
- traefik.http.routers.zitadel-grpc-mgmt.tls=true
|
||
- traefik.http.routers.zitadel-grpc-mgmt.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
# === CRITICAL ROUTE: /auth/v1 for PAT + wait_api ===
|
||
- traefik.http.routers.zitadel-auth-v1.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/auth/v1`)
|
||
- traefik.http.routers.zitadel-auth-v1.entrypoints=websecure
|
||
- traefik.http.routers.zitadel-auth-v1.service=zitadel
|
||
- traefik.http.routers.zitadel-auth-v1.tls=true
|
||
- traefik.http.routers.zitadel-auth-v1.tls.certresolver=NETBIRD_TRAEFIK_SSL
|
||
- traefik.http.routers.zitadel-auth-v1.priority=950
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
# Postgres for Zitadel
|
||
zdb:
|
||
restart: 'always'
|
||
networks: [netbird]
|
||
image: 'postgres:15-alpine'
|
||
env_file:
|
||
- ./zdb.env
|
||
volumes:
|
||
- netbird_zdb_data:/var/lib/postgresql/data:rw
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U root"]
|
||
interval: 5s
|
||
timeout: 60s
|
||
retries: 10
|
||
start_period: 5s
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "500m"
|
||
max-file: "2"
|
||
|
||
volumes:
|
||
netbird_zdb_data:
|
||
netbird_management:
|
||
netbird_zitadel_certs:
|
||
|
||
networks:
|
||
netbird:
|
||
driver: bridge
|
||
traefik_traefik:
|
||
external: true
|
||
name: traefik_default
|
||
EOF
|
||
|
||
# Replace placeholders
|
||
sed -i "s/NETBIRD_DOMAIN_PLACEHOLDER/${NETBIRD_DOMAIN}/g; s/NETBIRD_TRAEFIK_SSL/${TRAEFIK_CERTRESOLVER}/g" docker-compose.yml
|
||
|
||
# Create machinekey dir
|
||
mkdir -p machinekey
|
||
chmod 777 machinekey
|
||
|
||
echo "Starting database..."
|
||
$DOCKER_COMPOSE_COMMAND up -d zdb
|
||
|
||
echo "Waiting for database to be ready..."
|
||
sleep 30
|
||
|
||
echo "Starting Zitadel..."
|
||
$DOCKER_COMPOSE_COMMAND up -d zitadel
|
||
|
||
echo "Waiting for Zitadel to initialize..."
|
||
sleep 60
|
||
|
||
echo "Configuring Zitadel applications..."
|
||
|
||
INSTANCE_URL="https://$NETBIRD_DOMAIN"
|
||
TOKEN_PATH=./machinekey/zitadel-admin-sa.token
|
||
|
||
echo -n "Waiting for Zitadel's PAT to be created "
|
||
wait_pat "$TOKEN_PATH"
|
||
|
||
echo "Reading Zitadel PAT"
|
||
PAT=$(cat "$TOKEN_PATH")
|
||
if [ "$PAT" = "null" ]; then
|
||
echo "Failed getting Zitadel PAT"
|
||
exit 1
|
||
fi
|
||
|
||
echo -n "Waiting for Zitadel to become ready "
|
||
wait_api "$INSTANCE_URL" "$PAT"
|
||
|
||
echo "Creating Zitadel project"
|
||
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")
|
||
|
||
echo "Creating Dashboard application"
|
||
DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "$PROJECT_ID" "Dashboard" "https://$NETBIRD_DOMAIN/nb-auth" "https://$NETBIRD_DOMAIN/nb-silent-auth" "https://$NETBIRD_DOMAIN/" "false" "false")
|
||
|
||
echo "Creating CLI application"
|
||
CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "$PROJECT_ID" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true" "true")
|
||
|
||
echo "Creating service user"
|
||
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
|
||
create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
|
||
add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
|
||
|
||
echo "Creating admin user"
|
||
HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")
|
||
add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID"
|
||
|
||
echo "Cleaning up auto service user"
|
||
DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
|
||
if [ "$DATE" = "null" ]; then
|
||
echo "Failed deleting auto service user"
|
||
echo "Please remove it manually"
|
||
fi
|
||
|
||
echo "Generating NetBird configuration..."
|
||
|
||
# dashboard.env
|
||
cat > dashboard.env <<EOF
|
||
NETBIRD_MGMT_API_ENDPOINT=https://$NETBIRD_DOMAIN
|
||
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://$NETBIRD_DOMAIN
|
||
AUTH_AUDIENCE=$DASHBOARD_APPLICATION_CLIENT_ID
|
||
AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
|
||
AUTH_AUTHORITY=https://$NETBIRD_DOMAIN
|
||
USE_AUTH0=false
|
||
AUTH_SUPPORTED_SCOPES=openid profile email offline_access
|
||
AUTH_REDIRECT_URI=/nb-auth
|
||
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||
NGINX_SSL_PORT=443
|
||
LETSENCRYPT_DOMAIN=none
|
||
NETBIRD_DOMAIN=$NETBIRD_DOMAIN
|
||
EOF
|
||
|
||
# management.json
|
||
cat > management.json <<EOF
|
||
{
|
||
"Stuns": [
|
||
{
|
||
"Proto": "udp",
|
||
"URI": "stun:$NETBIRD_DOMAIN:3478"
|
||
}
|
||
],
|
||
"TURNConfig": {
|
||
"Turns": [
|
||
{
|
||
"Proto": "udp",
|
||
"URI": "turn:$NETBIRD_DOMAIN:3478",
|
||
"Username": "self",
|
||
"Password": "$TURN_PASSWORD"
|
||
}
|
||
],
|
||
"TimeBasedCredentials": false
|
||
},
|
||
"Relay": {
|
||
"Addresses": ["rels://$NETBIRD_DOMAIN:443/relay"],
|
||
"CredentialsTTL": "24h",
|
||
"Secret": "$NETBIRD_RELAY_AUTH_SECRET"
|
||
},
|
||
"Signal": {
|
||
"Proto": "https",
|
||
"URI": "$NETBIRD_DOMAIN:443"
|
||
},
|
||
"HttpConfig": {
|
||
"AuthIssuer": "https://$NETBIRD_DOMAIN",
|
||
"AuthAudience": "$DASHBOARD_APPLICATION_CLIENT_ID",
|
||
"OIDCConfigEndpoint":"https://$NETBIRD_DOMAIN/.well-known/openid-configuration"
|
||
},
|
||
"IdpManagerConfig": {
|
||
"ManagerType": "zitadel",
|
||
"ClientConfig": {
|
||
"Issuer": "https://$NETBIRD_DOMAIN",
|
||
"TokenEndpoint": "https://$NETBIRD_DOMAIN/oauth/v2/token",
|
||
"ClientID": "$SERVICE_USER_CLIENT_ID",
|
||
"ClientSecret": "$SERVICE_USER_CLIENT_SECRET",
|
||
"GrantType": "client_credentials"
|
||
},
|
||
"ExtraConfig": {
|
||
"ManagementEndpoint": "https://$NETBIRD_DOMAIN/management/v1"
|
||
}
|
||
},
|
||
"DeviceAuthorizationFlow": {
|
||
"Provider": "hosted",
|
||
"ProviderConfig": {
|
||
"Audience": "$CLI_APPLICATION_CLIENT_ID",
|
||
"ClientID": "$CLI_APPLICATION_CLIENT_ID",
|
||
"Scope": "openid"
|
||
}
|
||
},
|
||
"PKCEAuthorizationFlow": {
|
||
"ProviderConfig": {
|
||
"Audience": "$CLI_APPLICATION_CLIENT_ID",
|
||
"ClientID": "$CLI_APPLICATION_CLIENT_ID",
|
||
"Scope": "openid profile email offline_access",
|
||
"RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
|
||
}
|
||
}
|
||
}
|
||
EOF
|
||
|
||
echo "Starting all NetBird services..."
|
||
$DOCKER_COMPOSE_COMMAND up -d
|
||
|
||
echo -e "\nDone!\n"
|
||
echo "You can access the NetBird dashboard at https://$NETBIRD_DOMAIN"
|
||
echo "Login with the following credentials:"
|
||
echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
|
||
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
|
||
echo "NetBird_URL: https://$NETBIRD_DOMAIN" | tee -a .env
|
||
echo "Zitadel_console: https://$NETBIRD_DOMAIN/ui/console" | tee -a .env
|
||
echo ""
|
||
echo "Zitadel console: https://$NETBIRD_DOMAIN/ui/console"
|
||
echo ""
|
||
echo "Note: The admin password will require changing on first login."
|
||
}
|
||
|
||
main "$@"
|