- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains - Omit embedded publish git dirs and empty placeholders from index Made-with: Cursor
429 lines
14 KiB
Bash
Executable File
429 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Fix NPMplus SSL gaps reported by audit-npmplus-ssl-all-instances.sh (primary instance by default).
|
|
# - Hosts with a cert but ssl_forced=false: enable Force SSL + HSTS only for non-RPC endpoints (see should_force_ssl).
|
|
# - Hosts with no cert: request Let's Encrypt cert via NPM API, assign to host (ssl_forced per same rule).
|
|
# - Hosts with expired certs: request a fresh cert (or reuse an already-valid one) and reassign it.
|
|
# - Hosts bound to the wrong certificate: request/reuse a matching cert and reassign it.
|
|
# Supports both JSON bearer-token auth and cookie-session auth returned by some
|
|
# NPMplus UIs.
|
|
# Skips: disabled hosts; domain names containing * (wildcard — request certs in UI); dry-run.
|
|
#
|
|
# Usage: bash scripts/nginx-proxy-manager/fix-npmplus-ssl-issues.sh [--dry-run]
|
|
# Env: NPM_URL (default https://IP_NPMPLUS:81), NPM_EMAIL, NPM_PASSWORD, NPM_CURL_MAX_TIME
|
|
# NPM_SSL_FIX_SLEEP_LE=5 seconds between LE certificate requests (default 5)
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
# shellcheck source=/dev/null
|
|
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
|
|
|
|
DRY_RUN=0
|
|
for _a in "$@"; do [[ "$_a" == "--dry-run" ]] && DRY_RUN=1; done
|
|
|
|
_orig_npm_url="${NPM_URL:-}"
|
|
_orig_npm_email="${NPM_EMAIL:-}"
|
|
_orig_npm_password="${NPM_PASSWORD:-}"
|
|
_orig_npm_instance_label="${NPM_INSTANCE_LABEL:-}"
|
|
if [ -f "$PROJECT_ROOT/.env" ]; then
|
|
set +u
|
|
# shellcheck source=/dev/null
|
|
source "$PROJECT_ROOT/.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"
|
|
[ -n "$_orig_npm_instance_label" ] && NPM_INSTANCE_LABEL="$_orig_npm_instance_label"
|
|
# shellcheck source=/dev/null
|
|
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
|
|
|
|
NPM_URL="${NPM_URL:-https://${IP_NPMPLUS:-192.168.11.167}:81}"
|
|
NPM_EMAIL="${NPM_EMAIL:-}"
|
|
NPM_CURL_MAX_TIME="${NPM_CURL_MAX_TIME:-300}"
|
|
NPM_SSL_FIX_SLEEP_LE="${NPM_SSL_FIX_SLEEP_LE:-5}"
|
|
|
|
password_for_label() {
|
|
case "$1" in
|
|
primary) echo "${NPM_PASSWORD_PRIMARY:-${NPM_PASSWORD:-}}" ;;
|
|
secondary) echo "${NPM_PASSWORD_SECONDARY:-${NPM_PASSWORD:-}}" ;;
|
|
alltra-hybx) echo "${NPM_PASSWORD_ALLTRA_HYBX:-${NPM_PASSWORD:-}}" ;;
|
|
fourth-dev) echo "${NPM_PASSWORD_FOURTH:-${NPM_PASSWORD:-}}" ;;
|
|
mifos) echo "${NPM_PASSWORD_MIFOS:-${NPM_PASSWORD:-}}" ;;
|
|
*) echo "${NPM_PASSWORD:-}" ;;
|
|
esac
|
|
}
|
|
|
|
instance_label_from_url() {
|
|
local url="${1:-}"
|
|
local label="${NPM_INSTANCE_LABEL:-}"
|
|
if [ -n "$label" ]; then
|
|
echo "$label"
|
|
return 0
|
|
fi
|
|
case "$url" in
|
|
*"${IP_NPMPLUS_SECONDARY:-192.168.11.168}"*) echo "secondary" ;;
|
|
*"${IP_NPMPLUS_ALLTRA_HYBX:-192.168.11.169}"*) echo "alltra-hybx" ;;
|
|
*"${IP_NPMPLUS_FOURTH:-192.168.11.170}"*) echo "fourth-dev" ;;
|
|
*"${IP_NPMPLUS_MIFOS:-192.168.11.171}"*) echo "mifos" ;;
|
|
*) echo "primary" ;;
|
|
esac
|
|
}
|
|
|
|
NPM_INSTANCE_LABEL="$(instance_label_from_url "$NPM_URL")"
|
|
NPM_PASSWORD="$(password_for_label "$NPM_INSTANCE_LABEL")"
|
|
|
|
if [ -z "$NPM_PASSWORD" ]; then
|
|
echo "NPM_PASSWORD required (.env or export)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
curl_npm() { curl -s -k -L --connect-timeout 10 --max-time "$NPM_CURL_MAX_TIME" "$@"; }
|
|
|
|
AUTH_TOKEN=""
|
|
AUTH_COOKIE_JAR=""
|
|
|
|
cleanup_auth() {
|
|
[ -n "${AUTH_COOKIE_JAR:-}" ] && [ -f "$AUTH_COOKIE_JAR" ] && rm -f "$AUTH_COOKIE_JAR"
|
|
}
|
|
trap cleanup_auth EXIT
|
|
|
|
extract_json_token() {
|
|
local body="${1:-}"
|
|
echo "$body" | jq -r '.token // empty' 2>/dev/null || true
|
|
}
|
|
|
|
cookie_has_session() {
|
|
local jar="$1"
|
|
[ -f "$jar" ] && grep -q $'\ttoken\t' "$jar"
|
|
}
|
|
|
|
npm_api() {
|
|
if [ -n "$AUTH_TOKEN" ]; then
|
|
curl_npm -H "Authorization: Bearer $AUTH_TOKEN" "$@"
|
|
else
|
|
curl_npm -b "$AUTH_COOKIE_JAR" -c "$AUTH_COOKIE_JAR" "$@"
|
|
fi
|
|
}
|
|
|
|
# 0 = force SSL OK for this host; 1 = keep ssl_forced false (JSON-RPC / WS edge)
|
|
should_force_ssl() {
|
|
local domain="${1,,}"
|
|
local port="${2:-0}"
|
|
case "$port" in 8545|8546) return 1 ;; esac
|
|
[[ "$domain" == *"*"* ]] && return 1
|
|
if [[ "$domain" =~ ^(rpc\.|ws\.|wss\.) ]]; then return 1; fi
|
|
if [[ "$domain" =~ rpc-http- ]]; then return 1; fi
|
|
if [[ "$domain" =~ rpc-ws- ]]; then return 1; fi
|
|
if [[ "$domain" =~ rpc-core ]]; then return 1; fi
|
|
if [[ "$domain" =~ rpc-fireblocks ]]; then return 1; fi
|
|
if [[ "$domain" =~ rpc\.public-0138 ]]; then return 1; fi
|
|
if [[ "$domain" == "rpc.d-bis.org" || "$domain" == "rpc2.d-bis.org" ]]; then return 1; fi
|
|
if [[ "$domain" =~ \.tw-core\. ]]; then return 1; fi
|
|
if [[ "$domain" =~ ^rpc\.defi-oracle ]]; then return 1; fi
|
|
if [[ "$domain" =~ ^wss\.defi-oracle ]]; then return 1; fi
|
|
return 0
|
|
}
|
|
|
|
log() { echo "[fix-npmplus-ssl] $*"; }
|
|
|
|
try_connect() {
|
|
curl_npm -o /dev/null -w "%{http_code}" "$1/" 2>/dev/null | grep -qE '^(200|301|302|401)$'
|
|
}
|
|
if ! try_connect "$NPM_URL"; then
|
|
http_url="${NPM_URL/https:/http:}"
|
|
try_connect "$http_url" && NPM_URL="$http_url" || { echo "Cannot reach NPM at $NPM_URL"; exit 1; }
|
|
fi
|
|
|
|
AUTH_JSON=$(jq -n --arg identity "$NPM_EMAIL" --arg secret "$NPM_PASSWORD" '{identity:$identity,secret:$secret}')
|
|
AUTH_COOKIE_JAR="$(mktemp)"
|
|
AUTH_RESPONSE=$(curl_npm -c "$AUTH_COOKIE_JAR" -X POST "$NPM_URL/api/tokens" -H "Content-Type: application/json" -d "$AUTH_JSON")
|
|
AUTH_TOKEN=$(extract_json_token "$AUTH_RESPONSE")
|
|
if [ -z "$AUTH_TOKEN" ] || [ "$AUTH_TOKEN" = "null" ]; then
|
|
AUTH_TOKEN=""
|
|
fi
|
|
if [ -z "$AUTH_TOKEN" ] && ! cookie_has_session "$AUTH_COOKIE_JAR"; then
|
|
echo "NPM auth failed for $NPM_URL" >&2
|
|
exit 1
|
|
fi
|
|
|
|
HOSTS_JSON=$(npm_api -X GET "$NPM_URL/api/nginx/proxy-hosts")
|
|
CERTS_JSON=$(npm_api -X GET "$NPM_URL/api/nginx/certificates")
|
|
if ! echo "$HOSTS_JSON" | jq -e 'type == "array"' >/dev/null 2>&1 || \
|
|
! echo "$CERTS_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
|
echo "NPM auth succeeded but proxy-host or certificate API payload was not usable for $NPM_URL" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# NPM PUT rejects full GET payloads ("additional properties"); use minimal SSL update for cert assignment.
|
|
cert_id_for_domain() {
|
|
local domain="$1"
|
|
echo "$CERTS_JSON" | jq -r --arg d "$domain" '
|
|
def parse_exp($c):
|
|
($c.expires_on // $c.meta.letsencrypt_expiry // $c.meta.expires_on // "") | tostring;
|
|
def epoch_if($s):
|
|
if $s == "" or $s == "null" then null
|
|
else
|
|
($s | gsub(" "; "T") | . + "Z" | fromdateiso8601? // null)
|
|
end;
|
|
[ .[] | select(.domain_names | type == "array" and any(.[]; . == $d)) |
|
|
{id, expires_at:(epoch_if(parse_exp(.)))} ] |
|
|
map(select(.expires_at == null or .expires_at > now)) |
|
|
.[0].id // empty
|
|
'
|
|
}
|
|
|
|
cert_is_expired() {
|
|
local cert_id="$1"
|
|
echo "$CERTS_JSON" | jq -e --argjson cid "$cert_id" '
|
|
def parse_exp($c):
|
|
($c.expires_on // $c.meta.letsencrypt_expiry // $c.meta.expires_on // "") | tostring;
|
|
def epoch_if($s):
|
|
if $s == "" or $s == "null" then null
|
|
else
|
|
($s | gsub(" "; "T") | . + "Z" | fromdateiso8601? // null)
|
|
end;
|
|
[ .[] | select(.id == $cid) ][0] as $c |
|
|
if $c == null then false
|
|
else
|
|
(epoch_if(parse_exp($c)) as $ep | ($ep != null and $ep < now))
|
|
end
|
|
' >/dev/null 2>&1
|
|
}
|
|
|
|
cert_matches_domain() {
|
|
local cert_id="$1"
|
|
local domain="$2"
|
|
echo "$CERTS_JSON" | jq -e --argjson cid "$cert_id" --arg d "$domain" '
|
|
def host_covered_by_cert($host; $names):
|
|
any(($names // [])[]; . == $host or (startswith("*.") and ($host | endswith("." + .[2:])) and ($host != .[2:])));
|
|
[ .[] | select(.id == $cid) ][0] as $c |
|
|
if $c == null then false
|
|
else host_covered_by_cert($d; ($c.domain_names // []))
|
|
end
|
|
' >/dev/null 2>&1
|
|
}
|
|
|
|
put_host_ssl() {
|
|
local hid="$1"
|
|
local force="$2"
|
|
local body resp upd cid
|
|
body=$(echo "$HOSTS_JSON" | jq --argjson id "$hid" --argjson force "$force" '
|
|
[.[] | select(.id == $id)][0] |
|
|
if . == null then empty
|
|
else
|
|
.ssl_forced = $force |
|
|
.http2_support = true |
|
|
.hsts_enabled = $force |
|
|
.hsts_subdomains = $force
|
|
end
|
|
')
|
|
if [ -z "$body" ] || [ "$body" = "null" ]; then
|
|
log "host id $hid not found in snapshot"
|
|
return 1
|
|
fi
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
log "dry-run: PUT proxy-hosts/$hid ssl_forced=$force"
|
|
return 0
|
|
fi
|
|
resp=$(npm_api -X PUT "$NPM_URL/api/nginx/proxy-hosts/$hid" \
|
|
-H "Content-Type: application/json" -d "$body")
|
|
if echo "$resp" | jq -e '.id' >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
cid=$(echo "$HOSTS_JSON" | jq -r --argjson id "$hid" '.[] | select(.id == $id) | .certificate_id // empty')
|
|
if [ -z "$cid" ] || [ "$cid" = "null" ]; then
|
|
log "PUT failed and no certificate_id for host $hid: $(echo "$resp" | head -c 250)"
|
|
return 1
|
|
fi
|
|
upd=$(jq -n --argjson cid "$cid" --argjson force "$force" \
|
|
'{certificate_id:$cid, ssl_forced:$force, http2_support:true, hsts_enabled:$force, hsts_subdomains:$force}')
|
|
resp=$(npm_api -X PUT "$NPM_URL/api/nginx/proxy-hosts/$hid" \
|
|
-H "Content-Type: application/json" -d "$upd")
|
|
if echo "$resp" | jq -e '.id' >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
log "PUT fallback failed: $(echo "$resp" | head -c 250)"
|
|
return 1
|
|
}
|
|
|
|
assign_minimal_ssl() {
|
|
local hid="$1"
|
|
local cid="$2"
|
|
local force="$3"
|
|
local upd uresp
|
|
upd=$(jq -n --argjson cid "$cid" --argjson f "$force" \
|
|
'{certificate_id:$cid, ssl_forced:$f, http2_support:true, hsts_enabled:$f, hsts_subdomains:$f}')
|
|
uresp=$(npm_api -X PUT "$NPM_URL/api/nginx/proxy-hosts/$hid" \
|
|
-H "Content-Type: application/json" -d "$upd")
|
|
if echo "$uresp" | jq -e '.id' >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
log "minimal PUT failed for host $hid: $(echo "$uresp" | head -c 280)"
|
|
return 1
|
|
}
|
|
|
|
prepare_host_for_certificate_request() {
|
|
local hid="$1"
|
|
local sf="$2"
|
|
local want_force="$3"
|
|
local domain="$4"
|
|
if [[ "$sf" == "$want_force" ]]; then
|
|
return 0
|
|
fi
|
|
log "pre-align SSL mode: $domain (host $hid, ssl_forced=$want_force)"
|
|
if put_host_ssl "$hid" "$want_force"; then
|
|
HOSTS_JSON=$(npm_api -X GET "$NPM_URL/api/nginx/proxy-hosts")
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
request_cert_and_assign() {
|
|
local hid="$1"
|
|
local domain="$2"
|
|
local force="$3"
|
|
local cert_resp new_id existing_id
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
log "dry-run: POST certificates + assign host $hid domain=$domain force=$force"
|
|
return 0
|
|
fi
|
|
existing_id=$(cert_id_for_domain "$domain")
|
|
new_id=""
|
|
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
|
|
log "reuse existing cert id=$existing_id for $domain"
|
|
new_id="$existing_id"
|
|
else
|
|
cert_resp=$(npm_api -X POST "$NPM_URL/api/nginx/certificates" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n --arg domain "$domain" '{domain_names:[$domain], provider:"letsencrypt"}')")
|
|
new_id=$(echo "$cert_resp" | jq -r '.id // empty')
|
|
if [ -z "$new_id" ] || [ "$new_id" = "null" ]; then
|
|
log "cert request failed for $domain: $(echo "$cert_resp" | jq -c '.' 2>/dev/null | head -c 350)"
|
|
return 1
|
|
fi
|
|
fi
|
|
if assign_minimal_ssl "$hid" "$new_id" "$force"; then
|
|
CERTS_JSON=$(npm_api -X GET "$NPM_URL/api/nginx/certificates")
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
n_force=0
|
|
n_force_fail=0
|
|
n_cert=0
|
|
n_cert_fail=0
|
|
n_skip=0
|
|
|
|
while IFS= read -r row; do
|
|
[ -z "$row" ] && continue
|
|
hid=$(echo "$row" | jq -r '.id')
|
|
domain=$(echo "$row" | jq -r '.domain_names[0] // empty')
|
|
cid=$(echo "$row" | jq -r '.certificate_id // 0')
|
|
sf=$(echo "$row" | jq -r '.ssl_forced // false')
|
|
en=$(echo "$row" | jq -r '.enabled // true')
|
|
port=$(echo "$row" | jq -r '.forward_port // 0')
|
|
[[ "$en" == "false" ]] && continue
|
|
[ -z "$domain" ] || [ "$domain" = "null" ] && continue
|
|
|
|
if should_force_ssl "$domain" "$port"; then
|
|
want_force=true
|
|
else
|
|
want_force=false
|
|
fi
|
|
|
|
if [[ "$cid" == "null" ]] || [[ -z "$cid" ]] || [[ "$cid" == "0" ]]; then
|
|
if [[ "$domain" == *"*"* ]]; then
|
|
log "skip wildcard (request cert in UI): $domain"
|
|
n_skip=$((n_skip + 1))
|
|
continue
|
|
fi
|
|
log "request cert: $domain (host $hid, ssl_forced=$want_force)"
|
|
if request_cert_and_assign "$hid" "$domain" "$want_force" "$row"; then
|
|
n_cert=$((n_cert + 1))
|
|
log "ok cert+assign: $domain"
|
|
else
|
|
n_cert_fail=$((n_cert_fail + 1))
|
|
fi
|
|
if [ "$DRY_RUN" = 0 ]; then
|
|
sleep "$NPM_SSL_FIX_SLEEP_LE"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if cert_is_expired "$cid"; then
|
|
if [[ "$domain" == *"*"* ]]; then
|
|
log "skip expired wildcard (renew in UI): $domain"
|
|
n_skip=$((n_skip + 1))
|
|
continue
|
|
fi
|
|
if ! prepare_host_for_certificate_request "$hid" "$sf" "$want_force" "$domain"; then
|
|
n_cert_fail=$((n_cert_fail + 1))
|
|
log "FAIL pre-align SSL mode before renew: $domain"
|
|
continue
|
|
fi
|
|
log "renew expired cert: $domain (host $hid, cert $cid, ssl_forced=$want_force)"
|
|
if request_cert_and_assign "$hid" "$domain" "$want_force"; then
|
|
n_cert=$((n_cert + 1))
|
|
log "ok renewed cert: $domain"
|
|
else
|
|
n_cert_fail=$((n_cert_fail + 1))
|
|
log "FAIL renew: $domain"
|
|
fi
|
|
if [ "$DRY_RUN" = 0 ]; then
|
|
sleep "$NPM_SSL_FIX_SLEEP_LE"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if ! cert_matches_domain "$cid" "$domain"; then
|
|
if [[ "$domain" == *"*"* ]]; then
|
|
log "skip mismatched wildcard cert (fix in UI): $domain"
|
|
n_skip=$((n_skip + 1))
|
|
continue
|
|
fi
|
|
if ! prepare_host_for_certificate_request "$hid" "$sf" "$want_force" "$domain"; then
|
|
n_cert_fail=$((n_cert_fail + 1))
|
|
log "FAIL pre-align SSL mode before mismatched-cert fix: $domain"
|
|
continue
|
|
fi
|
|
log "replace mismatched cert: $domain (host $hid, cert $cid, ssl_forced=$want_force)"
|
|
if request_cert_and_assign "$hid" "$domain" "$want_force"; then
|
|
n_cert=$((n_cert + 1))
|
|
log "ok reassigned matching cert: $domain"
|
|
else
|
|
n_cert_fail=$((n_cert_fail + 1))
|
|
log "FAIL reassign mismatched cert: $domain"
|
|
fi
|
|
if [ "$DRY_RUN" = 0 ]; then
|
|
sleep "$NPM_SSL_FIX_SLEEP_LE"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if [[ "$sf" == "true" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Has cert, ssl not forced — align with want_force
|
|
if [[ "$want_force" == "false" ]]; then
|
|
n_skip=$((n_skip + 1))
|
|
continue
|
|
fi
|
|
|
|
log "enable Force SSL: $domain (host $hid, cert $cid)"
|
|
if put_host_ssl "$hid" true; then
|
|
n_force=$((n_force + 1))
|
|
log "ok: $domain"
|
|
else
|
|
n_force_fail=$((n_force_fail + 1))
|
|
log "FAIL: $domain"
|
|
fi
|
|
done < <(echo "$HOSTS_JSON" | jq -c '.[]')
|
|
|
|
log "done: force_ssl_ok=$n_force force_ssl_fail=$n_force_fail new_certs_ok=$n_cert new_certs_fail=$n_cert_fail skipped_rpc_or_wildcard=$n_skip dry_run=$DRY_RUN"
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
log "Re-run without --dry-run to apply."
|
|
fi
|