commit 919f8829200a21436a042bc1266daae26a3e4853 Author: yblis Date: Sat Sep 6 17:40:59 2025 +0200 Create install-netbird-traefik.sh diff --git a/install-netbird-traefik.sh b/install-netbird-traefik.sh new file mode 100644 index 0000000..bad1a23 --- /dev/null +++ b/install-netbird-traefik.sh @@ -0,0 +1,830 @@ +#!/bin/bash + +set -e + +# Configuration +NETBIRD_DOMAIN="netbird.domain.fr" +export NETBIRD_DOMAIN +TRAEFIK_NETWORK="traefik_traefik" + +# Error handling functions +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 functions +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 45 ]]; then + FLAGS="-v" + echo "" + fi + + curl $FLAGS --fail --connect-timeout 1 -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT" + if [[ $? -eq 0 ]]; then + break + fi + if [[ $counter -eq 45 ]]; then + echo "" + echo "Unable to connect to Zitadel for more than 45s, please check the output above, your firewall rules and container logs" + exit 1 + fi + echo -n " ." + sleep 1 + counter=$((counter + 1)) + done + echo " done" + set -e +} + +# Zitadel API functions +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"'"}' + ) + 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 + APPLICATION_NAME=$3 + BASE_REDIRECT_URL1=$4 + BASE_REDIRECT_URL2=$5 + LOGOUT_URL=$6 + ZITADEL_DEV_MODE=$7 + DEVICE_CODE=$8 + + 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 + }' + ) + + 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" + }' + ) + 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 '{}' + ) + 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" + ] + }' + ) + 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 + }' + ) + 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" + ] + }' + ) + 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" \ + ) + 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" \ + ) + 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" \ + ) + 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" +} + +# Get external IP for TURN server +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 initialization function +main() { + echo "Initializing NetBird with Traefik..." + + # Check dependencies + check_jq + DOCKER_COMPOSE_COMMAND=$(check_docker_compose) + + # Check if files already exist + 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 and 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..." + + # Generate zitadel.env + cat > zitadel.env < zdb.env < turnserver.conf < relay.env < dashboard.env + echo "" > management.json + + # Generate docker-compose.yml + 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_traefik + - 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=https + - traefik.http.routers.netbird-dashboard.tls=true + - traefik.http.routers.netbird-dashboard.tls.certresolver=webssl + - 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_traefik + - 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=https + - traefik.http.routers.netbird-signal.tls=true + - traefik.http.routers.netbird-signal.tls.certresolver=webssl + - traefik.http.routers.netbird-signal.priority=200 + 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_traefik + - 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=https + - traefik.http.routers.netbird-relay.tls=true + - traefik.http.routers.netbird-relay.tls.certresolver=webssl + - 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_traefik + - 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=https + - traefik.http.routers.netbird-api.service=netbird-management + - traefik.http.routers.netbird-api.tls=true + - traefik.http.routers.netbird-api.tls.certresolver=webssl + - 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=https + - 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=webssl + - traefik.http.routers.netbird-management-grpc.priority=200 + 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.64.1' + command: 'start-from-init --masterkeyFromEnv --tlsMode external' + env_file: + - ./zitadel.env + 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_traefik + - traefik.http.services.zitadel.loadbalancer.server.port=8080 + # OIDC wellknown + - traefik.http.routers.zitadel-wellknown.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/.well-known`) + - traefik.http.routers.zitadel-wellknown.entrypoints=https + - 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=webssl + # OAuth + - traefik.http.routers.zitadel-oauth.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/oauth`) + - traefik.http.routers.zitadel-oauth.entrypoints=https + - 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=webssl + # OIDC + - traefik.http.routers.zitadel-oidc.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/oidc`) + - traefik.http.routers.zitadel-oidc.entrypoints=https + - 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=webssl + # UI Console + - traefik.http.routers.zitadel-ui.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/ui`) + - traefik.http.routers.zitadel-ui.entrypoints=https + - 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=webssl + # Device flow + - traefik.http.routers.zitadel-device.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/device`) + - traefik.http.routers.zitadel-device.entrypoints=https + - 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=webssl + # Management API + - traefik.http.routers.zitadel-mgmt.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/management/v1`) + - traefik.http.routers.zitadel-mgmt.entrypoints=https + - 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=webssl + # Auth API + - traefik.http.routers.zitadel-auth.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/auth/v1`) + - traefik.http.routers.zitadel-auth.entrypoints=https + - traefik.http.routers.zitadel-auth.service=zitadel + - traefik.http.routers.zitadel-auth.priority=300 + - traefik.http.routers.zitadel-auth.tls=true + - traefik.http.routers.zitadel-auth.tls.certresolver=webssl + # Admin API + - traefik.http.routers.zitadel-admin.rule=Host(`NETBIRD_DOMAIN_PLACEHOLDER`) && PathPrefix(`/admin/v1`) + - traefik.http.routers.zitadel-admin.entrypoints=https + - 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=webssl + logging: + driver: "json-file" + options: + max-size: "500m" + max-file: "2" + + # Postgres for Zitadel + zdb: + restart: 'always' + networks: [netbird] + image: 'postgres:16-alpine' + env_file: + - ./zdb.env + volumes: + - netbird_zdb_data:/var/lib/postgresql/data:rw + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"] + 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 +EOF +sed -i "s/NETBIRD_DOMAIN_PLACEHOLDER/${NETBIRD_DOMAIN}/g" docker-compose.yml + + # Create machinekey directory + 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 + + # Configuration automatique de Zitadel + 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" + + # Create project + echo "Creating Zitadel project" + PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT") + + # Create applications + echo "Creating Dashboard application" + DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "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" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true" "true") + + # Create service user + 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" + + # Create admin user + 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" + + # Clean up auto service user + 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 + + # Generate NetBird configuration + echo "Generating NetBird configuration..." + + # dashboard.env + cat > dashboard.env < management.json <