Files
proxmox/scripts/nginx-proxy-manager/update-npmplus-proxy-hosts-api.sh
defiQUG 7f3dcf2513 feat(sankofa): public web CT 7806, portal NPM/DNS defaults, Keycloak redirect helper
- Provision/sync scripts and systemd for corporate Next on 7806; IP_SANKOFA_PUBLIC_WEB for apex NPM
- Portal stack: NEXTAUTH_URL default portal.sankofa.nexus; NPM fleet + migrate + DNS ordering
- keycloak-sankofa-ensure-client-redirects.sh (KEYCLOAK_ADMIN_PASSWORD); .env.master.example hints
- Docs: task list, inventory, FQDN/E2E/EXPECTED_WEB_CONTENT, AGENTS pointers

Made-with: Cursor
2026-03-29 13:41:02 -07:00

428 lines
31 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Security: do not use `bash -x` or `curl -v` when debugging auth in production — logs may capture secrets.
# Auth failures: only a short error message is printed by default. For a redacted JSON snippet set NPM_DEBUG_AUTH=1.
set -euo pipefail
# Repo root (…/proxmox) — same as second block below; load IPs once from the right path
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Update existing NPMplus proxy hosts via API with correct VMIDs and IPs
# This script updates existing proxy hosts, not creates new ones.
# PUT payload includes only forward_* / websocket / block_exploits — existing certificate_id and ssl_forced are preserved by NPMplus.
set -e
# Preserve NPM credentials from environment so "export NPM_PASSWORD=...; ./script" works
_orig_npm_url="${NPM_URL:-}"
_orig_npm_email="${NPM_EMAIL:-}"
_orig_npm_password="${NPM_PASSWORD:-}"
# Load dotenv: repo root .env then smom-dbis-138/.env (operator creds)
if [ -f "$PROJECT_ROOT/.env" ]; then
set +u
# shellcheck source=/dev/null
source "$PROJECT_ROOT/.env"
set -u
fi
if [ -f "$PROJECT_ROOT/smom-dbis-138/.env" ]; then
set +u
# shellcheck source=/dev/null
source "$PROJECT_ROOT/smom-dbis-138/.env"
set -u
fi
[ -n "$_orig_npm_url" ] && NPM_URL="$_orig_npm_url"
[ -n "$_orig_npm_email" ] && NPM_EMAIL="$_orig_npm_email"
[ -n "$_orig_npm_password" ] && NPM_PASSWORD="$_orig_npm_password"
[ -f "$PROJECT_ROOT/config/ip-addresses.conf" ] && source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true
# Default: NPMplus (VMID 10233) — IP_NPMPLUS from config/ip-addresses.conf (prefer eth1 / .167); override with NPM_URL in .env
NPM_URL="${NPM_URL:-https://${IP_NPMPLUS}:81}"
NPM_EMAIL="${NPM_EMAIL:-nsatoshi2007@hotmail.com}"
NPM_PASSWORD="${NPM_PASSWORD:-}"
if [ -z "$NPM_PASSWORD" ]; then
echo "❌ NPM_PASSWORD is required. Set it in .env or export NPM_PASSWORD=..."
echo " Example: echo 'NPM_PASSWORD=your-password' >> $PROJECT_ROOT/.env"
exit 1
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔄 Updating NPMplus Proxy Hosts via API"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# NPMplus API can stall indefinitely without --max-time (override e.g. NPM_CURL_MAX_TIME=300)
NPM_CURL_MAX_TIME="${NPM_CURL_MAX_TIME:-120}"
# -L: port 81 often 301s HTTP→HTTPS; POST /api/tokens without -L returns 400 "Payload is undefined"
curl_npm() { curl -s -k -L --connect-timeout 10 --max-time "$NPM_CURL_MAX_TIME" "$@"; }
# Connection check (NPMplus is on LAN 192.168.11.x). Try HTTP if HTTPS fails; try alternate IP .166/.167 if unreachable.
echo "🔐 Authenticating to NPMplus..."
try_connect() { curl -s -k -L -o /dev/null --connect-timeout 5 --max-time 15 "$1" 2>/dev/null; }
if ! try_connect "$NPM_URL/"; then
# Try HTTP instead of HTTPS (NPM admin often listens on HTTP only on port 81)
http_url="${NPM_URL/https:/http:}"
if try_connect "$http_url/"; then
NPM_URL="$http_url"
echo " Using HTTP (HTTPS timed out): $NPM_URL"
else
alt_url=""
if [[ "$NPM_URL" == *"${IP_NPMPLUS_ETH0}"* ]]; then
alt_url="https://${IP_NPMPLUS}:81"
elif [[ "$NPM_URL" == *"${IP_NPMPLUS}"* ]] || [[ -n "${IP_NPMPLUS_ETH1:-}" && "$NPM_URL" == *"${IP_NPMPLUS_ETH1}"* ]]; then
alt_url="https://${IP_NPMPLUS_ETH0}:81"
fi
connected=""
if [ -n "$alt_url" ] && try_connect "$alt_url/"; then connected="$alt_url"; fi
if [ -z "$connected" ] && [ -n "$alt_url" ] && try_connect "${alt_url/https:/http:}/"; then connected="${alt_url/https:/http:}"; fi
if [ -n "$connected" ]; then
NPM_URL="$connected"
echo " Using alternate NPMplus URL: $NPM_URL"
else
echo "❌ Cannot connect to NPMplus at $NPM_URL"
[ -n "$alt_url" ] && echo " Tried alternate: $alt_url and ${alt_url/https:/http:}"
echo " Try in browser: http://${IP_NPMPLUS}:81 (not https). If not on LAN, use SSH tunnel: ssh -L 8181:${IP_NPMPLUS}:81 -N root@${PROXMOX_HOST_R630_01} then http://127.0.0.1:8181"
echo " Run this script from a host on the same LAN as NPMplus. Ensure container 10233 is running."
exit 1
fi
fi
fi
AUTH_JSON=$(jq -n --arg identity "$NPM_EMAIL" --arg secret "$NPM_PASSWORD" '{identity:$identity,secret:$secret}')
TOKEN_RESPONSE=$(curl_npm -X POST "$NPM_URL/api/tokens" \
-H "Content-Type: application/json" \
-d "$AUTH_JSON")
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token // empty' 2>/dev/null || echo "")
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
ERROR_MSG=$(echo "$TOKEN_RESPONSE" | jq -r '.message // .error.message // .error // "Unknown error"' 2>/dev/null || echo "")
echo "❌ Authentication failed: ${ERROR_MSG:-No token in response}"
if [ "${NPM_DEBUG_AUTH:-0}" = "1" ]; then
# Strip .token from body before preview (never print bearer tokens)
RESP_PREVIEW=$(echo "$TOKEN_RESPONSE" | jq -c 'del(.token) | del(.data)' 2>/dev/null | head -c 300)
[ -z "$RESP_PREVIEW" ] && RESP_PREVIEW=$(echo "$TOKEN_RESPONSE" | head -c 200)
echo " Debug (NPM_DEBUG_AUTH=1, token field stripped if jq OK): $RESP_PREVIEW"
fi
exit 1
fi
echo "✅ Authentication successful"
echo ""
# Get all proxy hosts
echo "📋 Fetching existing proxy hosts..."
PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $TOKEN")
if [ $? -ne 0 ]; then
echo "❌ Failed to fetch proxy hosts"
exit 1
fi
# Match proxy host id by domain (case-insensitive). Avoids "not found" when NPM stores different casing.
resolve_proxy_host_id() {
local domain="$1"
local json="${2:-$PROXY_HOSTS_JSON}"
echo "$json" | jq -r --arg d "$domain" '
.[] | select(.domain_names | type == "array") |
select(any(.domain_names[]; (. | tostring | ascii_downcase) == ($d | ascii_downcase))) |
.id' 2>/dev/null | head -n1
}
# www → apex redirect: only https://hostname[:port] (no path/query); rejects characters that could break nginx advanced_config.
validate_canonical_https_redirect() {
local url="$1"
local ctx="${2:-canonical_https}"
if [[ "$url" != https://* ]]; then
echo "$ctx: canonical_https must start with https:// (got: $url)"
return 1
fi
if [[ "$url" == *$'\n'* || "$url" == *$'\r'* || "$url" == *' '* || "$url" == *';'* || "$url" == *'$'* || "$url" == *'`'* ]]; then
echo "$ctx: canonical_https contains forbidden characters (no spaces, semicolons, dollar, backticks)"
return 1
fi
local rest="${url#https://}"
if [[ "$rest" == */* ]]; then
echo "$ctx: canonical_https must not include a path (got: $url)"
return 1
fi
if ! [[ "$rest" =~ ^[a-zA-Z0-9._-]+(:[0-9]{1,5})?$ ]]; then
echo "$ctx: canonical_https must be https://hostname or https://hostname:port (got: $url)"
return 1
fi
return 0
}
# Function to add proxy host (POST) when domain does not exist
# Optional 6th arg: canonical HTTPS apex for www-style hosts (sets advanced_config 301 → apex$request_uri)
add_proxy_host() {
local domain=$1
local forward_host=$2
local forward_port=$3
local websocket=$4
local block_exploits=${5:-false}
local canonical_https="${6:-}"
local adv_line=""
if [ -n "$canonical_https" ] && ! validate_canonical_https_redirect "$canonical_https" "add_proxy_host($domain)"; then
return 1
fi
if [ -n "$canonical_https" ]; then
adv_line="return 301 ${canonical_https}\$request_uri;"
fi
local payload
payload=$(jq -n \
--arg domain "$domain" \
--arg host "$forward_host" \
--argjson port "$forward_port" \
--argjson ws "$websocket" \
--argjson block_exploits "$([ "$block_exploits" = "true" ] && echo true || echo false)" \
--arg adv "$adv_line" \
'{
domain_names: [$domain],
forward_scheme: "http",
forward_host: $host,
forward_port: $port,
allow_websocket_upgrade: $ws,
block_exploits: $block_exploits,
certificate_id: null,
ssl_forced: false
} + (if $adv != "" then {advanced_config: $adv} else {} end)' 2>/dev/null)
if [ -z "$payload" ]; then
echo " ❌ Failed to build payload for $domain"
return 1
fi
local resp
resp=$(curl_npm -X POST "$NPM_URL/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$payload" 2>/dev/null)
local id
id=$(echo "$resp" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$id" ] && [ "$id" != "null" ]; then
if [ -n "$canonical_https" ]; then
echo " ✅ Added: $domain -> http://${forward_host}:${forward_port} (WebSocket: $websocket) + 301 → ${canonical_https}\$request_uri"
else
echo " ✅ Added: $domain -> http://${forward_host}:${forward_port} (WebSocket: $websocket)"
fi
return 0
else
local err
err=$(echo "$resp" | jq -r '.message // .error // "unknown"' 2>/dev/null)
echo " ❌ Add failed for $domain: $err"
if echo "$err" | grep -qiE 'already|in use|exist|duplicate|unique'; then
echo " ↪ Host likely exists; refreshing list and attempting PUT update..."
PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $TOKEN")
if update_proxy_host "$domain" "http://${forward_host}:${forward_port}" "$websocket" "$block_exploits" "$canonical_https"; then
echo " ✅ Updated after duplicate-create error: $domain"
return 0
fi
fi
return 1
fi
}
# Function to update proxy host
# block_exploits: set false for RPC hosts (JSON-RPC uses POST to /; block_exploits can cause 405)
# Optional 5th arg: canonical HTTPS URL (no path) — sets advanced_config to 301 redirect (www → apex)
update_proxy_host() {
local domain=$1
local target=$2
local websocket=$3
local block_exploits=${4:-true}
local canonical_https="${5:-}"
if [ -n "$canonical_https" ] && ! validate_canonical_https_redirect "$canonical_https" "update_proxy_host($domain)"; then
return 1
fi
# Parse target URL
local scheme=$(echo "$target" | sed -E 's|^([^:]+):.*|\1|')
local hostname=$(echo "$target" | sed -E 's|^[^/]+//([^:]+):.*|\1|')
local port=$(echo "$target" | sed -E 's|^[^:]+://[^:]+:([0-9]+).*|\1|')
# Handle https URLs
if [[ "$target" == https://* ]]; then
scheme="https"
hostname=$(echo "$target" | sed -E 's|^https://([^:]+):.*|\1|')
port=$(echo "$target" | sed -E 's|^https://[^:]+:([0-9]+).*|\1|' || echo "443")
fi
# Reject bad parses (e.g. https://:443 when forward IP env is empty) — NPM returns errors without .id and jq message is empty.
if [[ -z "$hostname" || "$hostname" == *"://"* || "$hostname" == *"/"* ]]; then
echo " ❌ Invalid forward target for $domain (check env / ip-addresses.conf): $target → host=[$hostname]"
return 1
fi
port="${port//[^0-9]/}"
if [[ -z "$port" || "$port" -lt 1 || "$port" -gt 65535 ]]; then
echo " ❌ Invalid forward port for $domain: $target (parsed port=$port)"
return 1
fi
# Get host ID (case-insensitive); refresh once if missing (stale list / race with other writers)
HOST_ID=$(resolve_proxy_host_id "$domain" "$PROXY_HOSTS_JSON")
if [ -z "$HOST_ID" ] || [ "$HOST_ID" = "null" ]; then
echo "📋 Refreshing proxy host list (retry lookup for $domain)..."
PROXY_HOSTS_JSON=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $TOKEN")
HOST_ID=$(resolve_proxy_host_id "$domain" "$PROXY_HOSTS_JSON")
fi
if [ -z "$HOST_ID" ] || [ "$HOST_ID" = "null" ]; then
echo "⚠️ Domain $domain not found (skipping)"
return 1
fi
echo "📋 Updating $domain (ID: $HOST_ID)..."
# Create minimal update payload - NPMplus API only accepts specific fields
# block_exploits must be false for RPC so POST to / is allowed (JSON-RPC); explicit false fixes 405
local be_json="false"
[ "$block_exploits" = "true" ] && be_json="true"
local adv_line=""
if [ -n "$canonical_https" ]; then
adv_line="return 301 ${canonical_https}\$request_uri;"
fi
UPDATE_PAYLOAD=$(jq -n \
--arg scheme "$scheme" \
--arg hostname "$hostname" \
--argjson port "$(echo "$port" | sed 's/[^0-9]//g' || echo "80")" \
--argjson websocket "$websocket" \
--argjson block_exploits "$be_json" \
--arg adv "$adv_line" \
'{
forward_scheme: $scheme,
forward_host: $hostname,
forward_port: $port,
allow_websocket_upgrade: $websocket,
block_exploits: $block_exploits
} + (if $adv != "" then {advanced_config: $adv} else {} end)' 2>/dev/null || echo "")
UPDATE_RESPONSE=$(curl_npm -X PUT "$NPM_URL/api/nginx/proxy-hosts/$HOST_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$UPDATE_PAYLOAD")
UPDATE_ID=$(echo "$UPDATE_RESPONSE" | jq -r '.id // empty' 2>/dev/null || echo "")
if [ -n "$UPDATE_ID" ] && [ "$UPDATE_ID" != "null" ]; then
if [ -n "$canonical_https" ]; then
echo " ✅ Updated: $scheme://$hostname:$port (WebSocket: $websocket) + 301 → ${canonical_https}\$request_uri"
else
echo " ✅ Updated: $scheme://$hostname:$port (WebSocket: $websocket)"
fi
return 0
else
ERROR=$(echo "$UPDATE_RESPONSE" | jq -r '.error.message // .message // .error // empty' 2>/dev/null || echo "")
[ -z "$ERROR" ] && ERROR=$(echo "$UPDATE_RESPONSE" | head -c 400 | tr -d '\r\n')
[ -z "$ERROR" ] && ERROR="(empty API response — timeout or connection error; try NPM_CURL_MAX_TIME=300)"
echo " ❌ Failed: $ERROR"
return 1
fi
}
# Update all domains
updated_count=0
failed_count=0
# Blockscout / SolaceScanScout (VMID 5000) - Port 80 (nginx serves web UI, proxies /api/* to 4000 internally)
update_proxy_host "explorer.d-bis.org" "http://${IP_BLOCKSCOUT}:80" false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1))
update_proxy_host "blockscout.defi-oracle.io" "http://${IP_BLOCKSCOUT}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "blockscout.defi-oracle.io" "${IP_BLOCKSCOUT}" 80 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# docs.d-bis.org — same backend as explorer; nginx on VMID 5000 must serve /transaction-explanation/ (see deploy README)
update_proxy_host "docs.d-bis.org" "http://${IP_BLOCKSCOUT}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "docs.d-bis.org" "${IP_BLOCKSCOUT}" 80 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# RPC hosts: block_exploits must be false so POST to / works (JSON-RPC). Add if missing to avoid 405.
update_proxy_host "rpc-http-pub.d-bis.org" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-http-pub.d-bis.org" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "rpc-ws-pub.d-bis.org" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-ws-pub.d-bis.org" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "rpc-http-prv.d-bis.org" "http://${RPC_CORE_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-http-prv.d-bis.org" "${RPC_CORE_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "rpc-ws-prv.d-bis.org" "http://${RPC_CORE_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-ws-prv.d-bis.org" "${RPC_CORE_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# ThirdWeb Admin Core RPC — VMID ${RPC_THIRDWEB_ADMIN_CORE_VMID:-2103} @ ${RPC_THIRDWEB_ADMIN_CORE} (HTTPS + WSS via NPMplus; block_exploits off for JSON-RPC POST)
RPC_THIRDWEB_ADMIN_CORE="${RPC_THIRDWEB_ADMIN_CORE:-192.168.11.217}"
update_proxy_host "rpc.tw-core.d-bis.org" "http://${RPC_THIRDWEB_ADMIN_CORE}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc.tw-core.d-bis.org" "${RPC_THIRDWEB_ADMIN_CORE}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "wss.tw-core.d-bis.org" "http://${RPC_THIRDWEB_ADMIN_CORE}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "wss.tw-core.d-bis.org" "${RPC_THIRDWEB_ADMIN_CORE}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Catch-all for foo.tw-core.d-bis.org → Besu HTTP JSON-RPC :8545 (exact rpc./wss. hosts above take precedence for nginx server_name)
update_proxy_host '*.tw-core.d-bis.org' "http://${RPC_THIRDWEB_ADMIN_CORE}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host '*.tw-core.d-bis.org' "${RPC_THIRDWEB_ADMIN_CORE}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# RPC Core-2 (Nathan) is on the THIRD NPMplus (192.168.11.169) — use add-rpc-core-2-npmplus-proxy.sh and update-npmplus-alltra-hybx-proxy-hosts.sh
# ThirdWeb / public-0138 edge (VMID 2400 nginx HTTPS) — default IP must match ALL_VMIDS_ENDPOINTS if env is unset
RPC_THIRDWEB_PRIMARY="${RPC_THIRDWEB_PRIMARY:-192.168.11.240}"
update_proxy_host "rpc.public-0138.defi-oracle.io" "https://${RPC_THIRDWEB_PRIMARY}:443" true false && updated_count=$((updated_count + 1)) || { sleep 2; echo " ↪ Retry rpc.public-0138.defi-oracle.io after transient NPM/API error..."; update_proxy_host "rpc.public-0138.defi-oracle.io" "https://${RPC_THIRDWEB_PRIMARY}:443" true false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1)); }
# rpc.defi-oracle.io / wss.defi-oracle.io → same backend as rpc-http-pub / rpc-ws-pub (VMID 2201)
update_proxy_host "rpc.defi-oracle.io" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc.defi-oracle.io" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "wss.defi-oracle.io" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "wss.defi-oracle.io" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# rpc.d-bis.org / rpc2.d-bis.org and WS variants → VMID 2201 (besu-rpc-public-1); add if missing to fix 405
update_proxy_host "rpc.d-bis.org" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc.d-bis.org" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "rpc2.d-bis.org" "http://${RPC_PUBLIC_1}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc2.d-bis.org" "${RPC_PUBLIC_1}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "ws.rpc.d-bis.org" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "ws.rpc.d-bis.org" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "ws.rpc2.d-bis.org" "http://${RPC_PUBLIC_1}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "ws.rpc2.d-bis.org" "${RPC_PUBLIC_1}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Fireblocks-dedicated RPC (VMID 2301)
update_proxy_host "rpc-fireblocks.d-bis.org" "http://${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}:8545" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "rpc-fireblocks.d-bis.org" "${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}" 8545 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "ws.rpc-fireblocks.d-bis.org" "http://${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}:8546" true false && updated_count=$((updated_count + 1)) || { add_proxy_host "ws.rpc-fireblocks.d-bis.org" "${RPC_FIREBLOCKS_1:-${RPC_PRIVATE_1}}" 8546 true false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "dbis-admin.d-bis.org" "http://${IP_DBIS_FRONTEND:-192.168.11.130}:80" false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1))
update_proxy_host "dbis-api.d-bis.org" "http://${IP_DBIS_API:-192.168.11.155}:3000" false && updated_count=$((updated_count + 1)) || failed_count=$((failed_count + 1))
update_proxy_host "dbis-api-2.d-bis.org" "http://${IP_DBIS_API_2:-192.168.11.156}:3000" false && updated_count=$((updated_count + 1)) || { add_proxy_host "dbis-api-2.d-bis.org" "${IP_DBIS_API_2:-192.168.11.156}" 3000 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "secure.d-bis.org" "http://${IP_DBIS_FRONTEND:-192.168.11.130}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "secure.d-bis.org" "${IP_DBIS_FRONTEND:-192.168.11.130}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# DApp (VMID 5801) — frontend-dapp for Chain 138 bridge
update_proxy_host "dapp.d-bis.org" "http://${IP_DAPP_LXC:-192.168.11.58}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "dapp.d-bis.org" "${IP_DAPP_LXC:-192.168.11.58}" 80 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# MIM4U - VMID 7810 (mim-web-1) @ ${IP_MIM_WEB:-192.168.11.37} - Web Frontend serves main site and proxies /api/* to 7811
update_proxy_host "mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "www.mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "www.mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "secure.mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "secure.mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "training.mim4u.org" "http://${IP_MIM_WEB:-192.168.11.37}:80" false && updated_count=$((updated_count + 1)) || { add_proxy_host "training.mim4u.org" "${IP_MIM_WEB:-192.168.11.37}" 80 false true && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Gov Portals xom-dev (VMID 7804) — public https:// at NPM (LE); upstream HTTP
IP_GOV_PORTALS_DEV="${IP_GOV_PORTALS_DEV:-192.168.11.54}"
update_proxy_host "dbis.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3001" false && updated_count=$((updated_count + 1)) || { add_proxy_host "dbis.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3001 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "iccc.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3002" false && updated_count=$((updated_count + 1)) || { add_proxy_host "iccc.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3002 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "omnl.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3003" false && updated_count=$((updated_count + 1)) || { add_proxy_host "omnl.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3003 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "xom.xom-dev.phoenix.sankofa.nexus" "http://${IP_GOV_PORTALS_DEV}:3004" false && updated_count=$((updated_count + 1)) || { add_proxy_host "xom.xom-dev.phoenix.sankofa.nexus" "${IP_GOV_PORTALS_DEV}" 3004 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Sankofa / Phoenix — not Blockscout (explorer.d-bis.org / IP_BLOCKSCOUT:80).
# Intent: sankofa.nexus = corporate marketing (IP_SANKOFA_PUBLIC_WEB); portal.sankofa.nexus + admin = client SSO (IP_SANKOFA_CLIENT_SSO); phoenix = division + API. dash = operator (set IP_SANKOFA_DASH to manage). See docs/02-architecture/EXPECTED_WEB_CONTENT.md.
# www.sankofa.nexus → 301 https://sankofa.nexus$request_uri; www.phoenix → phoenix; www.the-order → the-order (NPM advanced_config).
IP_SANKOFA_PORTAL="${IP_SANKOFA_PORTAL:-${IP_SERVICE_51:-192.168.11.51}}"
IP_SANKOFA_PHOENIX_API="${IP_SANKOFA_PHOENIX_API:-${IP_SERVICE_50:-192.168.11.50}}"
SANKOFA_PORTAL_PORT="${SANKOFA_PORTAL_PORT:-3000}"
SANKOFA_PHOENIX_API_PORT="${SANKOFA_PHOENIX_API_PORT:-4000}"
IP_SANKOFA_PUBLIC_WEB="${IP_SANKOFA_PUBLIC_WEB:-${IP_SANKOFA_PORTAL}}"
SANKOFA_PUBLIC_WEB_PORT="${SANKOFA_PUBLIC_WEB_PORT:-${SANKOFA_PORTAL_PORT}}"
IP_SANKOFA_CLIENT_SSO="${IP_SANKOFA_CLIENT_SSO:-${IP_SANKOFA_PORTAL}}"
SANKOFA_CLIENT_SSO_PORT="${SANKOFA_CLIENT_SSO_PORT:-${SANKOFA_PORTAL_PORT}}"
update_proxy_host "sankofa.nexus" "http://${IP_SANKOFA_PUBLIC_WEB}:${SANKOFA_PUBLIC_WEB_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "sankofa.nexus" "${IP_SANKOFA_PUBLIC_WEB}" "${SANKOFA_PUBLIC_WEB_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "www.sankofa.nexus" "http://${IP_SANKOFA_PUBLIC_WEB}:${SANKOFA_PUBLIC_WEB_PORT}" false false "https://sankofa.nexus" && updated_count=$((updated_count + 1)) || { add_proxy_host "www.sankofa.nexus" "${IP_SANKOFA_PUBLIC_WEB}" "${SANKOFA_PUBLIC_WEB_PORT}" false false "https://sankofa.nexus" && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "phoenix.sankofa.nexus" "http://${IP_SANKOFA_PHOENIX_API}:${SANKOFA_PHOENIX_API_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "phoenix.sankofa.nexus" "${IP_SANKOFA_PHOENIX_API}" "${SANKOFA_PHOENIX_API_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "www.phoenix.sankofa.nexus" "http://${IP_SANKOFA_PHOENIX_API}:${SANKOFA_PHOENIX_API_PORT}" false false "https://phoenix.sankofa.nexus" && updated_count=$((updated_count + 1)) || { add_proxy_host "www.phoenix.sankofa.nexus" "${IP_SANKOFA_PHOENIX_API}" "${SANKOFA_PHOENIX_API_PORT}" false false "https://phoenix.sankofa.nexus" && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Keycloak (CT 7802) — portal SSO; NPM must forward X-Forwarded-* (Keycloak KC_PROXY_HEADERS=xforwarded on upstream)
IP_KEYCLOAK="${IP_KEYCLOAK:-192.168.11.52}"
update_proxy_host "keycloak.sankofa.nexus" "http://${IP_KEYCLOAK}:8080" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "keycloak.sankofa.nexus" "${IP_KEYCLOAK}" 8080 false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# the-order.sankofa.nexus — public hostname for the Sovereign Military Order of Malta (OSJ) management portal (secure auth).
# Application source (operator workstation): repo the_order at ~/projects/the_order (e.g. /home/intlc/projects/the_order).
# Default upstream: VMID 10210 order-haproxy @ IP_ORDER_HAPROXY:80 (provision: scripts/deployment/provision-order-haproxy-10210.sh).
# If 10210 is down: THE_ORDER_UPSTREAM_IP=${IP_SANKOFA_PORTAL} THE_ORDER_UPSTREAM_PORT=${SANKOFA_PORTAL_PORT} (direct portal 7801).
# www.the-order.sankofa.nexus → 301 https://the-order.sankofa.nexus$request_uri (same pattern as www.sankofa / www.phoenix).
IP_ORDER_HAPROXY="${IP_ORDER_HAPROXY:-192.168.11.39}"
THE_ORDER_UPSTREAM_IP="${THE_ORDER_UPSTREAM_IP:-${IP_ORDER_HAPROXY}}"
THE_ORDER_UPSTREAM_PORT="${THE_ORDER_UPSTREAM_PORT:-80}"
# block_exploits false — same policy as sankofa.nexus portal (Next/API-friendly; avoid 405 on some POST paths)
update_proxy_host "the-order.sankofa.nexus" "http://${THE_ORDER_UPSTREAM_IP}:${THE_ORDER_UPSTREAM_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "the-order.sankofa.nexus" "${THE_ORDER_UPSTREAM_IP}" "${THE_ORDER_UPSTREAM_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "www.the-order.sankofa.nexus" "http://${THE_ORDER_UPSTREAM_IP}:${THE_ORDER_UPSTREAM_PORT}" false false "https://the-order.sankofa.nexus" && updated_count=$((updated_count + 1)) || { add_proxy_host "www.the-order.sankofa.nexus" "${THE_ORDER_UPSTREAM_IP}" "${THE_ORDER_UPSTREAM_PORT}" false false "https://the-order.sankofa.nexus" && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Sankofa Studio (FusionAI) — VMID 7805; UI at /studio/ on same origin (port 8000). Prefer IP_SANKOFA_STUDIO from ip-addresses.conf / .env
IP_SANKOFA_STUDIO="${IP_SANKOFA_STUDIO:-192.168.11.72}"
SANKOFA_STUDIO_PORT="${SANKOFA_STUDIO_PORT:-8000}"
# block_exploits false — studio UI/API may POST; align with portal policy (avoid spurious 405 from NPM WAF)
update_proxy_host "studio.sankofa.nexus" "http://${IP_SANKOFA_STUDIO}:${SANKOFA_STUDIO_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "studio.sankofa.nexus" "${IP_SANKOFA_STUDIO}" "${SANKOFA_STUDIO_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Client SSO hostnames (Next.js portal stack on 7801 typical). NEXTAUTH_URL / Keycloak redirects: https://portal.sankofa.nexus (and https://admin.sankofa.nexus).
update_proxy_host "portal.sankofa.nexus" "http://${IP_SANKOFA_CLIENT_SSO}:${SANKOFA_CLIENT_SSO_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "portal.sankofa.nexus" "${IP_SANKOFA_CLIENT_SSO}" "${SANKOFA_CLIENT_SSO_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
update_proxy_host "admin.sankofa.nexus" "http://${IP_SANKOFA_CLIENT_SSO}:${SANKOFA_CLIENT_SSO_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "admin.sankofa.nexus" "${IP_SANKOFA_CLIENT_SSO}" "${SANKOFA_CLIENT_SSO_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
# Operator systems dashboard — only when IP_SANKOFA_DASH is set (see config/ip-addresses.conf).
if [[ -n "${IP_SANKOFA_DASH:-}" ]]; then
SANKOFA_DASH_PORT="${SANKOFA_DASH_PORT:-3000}"
update_proxy_host "dash.sankofa.nexus" "http://${IP_SANKOFA_DASH}:${SANKOFA_DASH_PORT}" false false && updated_count=$((updated_count + 1)) || { add_proxy_host "dash.sankofa.nexus" "${IP_SANKOFA_DASH}" "${SANKOFA_DASH_PORT}" false false && updated_count=$((updated_count + 1)); } || failed_count=$((failed_count + 1))
else
echo " Skipping dash.sankofa.nexus (set IP_SANKOFA_DASH and SANKOFA_DASH_PORT to provision upstream)."
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 Summary"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Updated: $updated_count"
echo "❌ Failed: $failed_count"
echo ""