- 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
268 lines
11 KiB
Bash
Executable File
268 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Audit all documented NPMplus instances: proxy hosts without certificate_id or with expired certs.
|
|
# Requires LAN + NPM API auth. Supports both JSON bearer-token auth and the
|
|
# newer cookie-session auth returned by some NPMplus UIs.
|
|
# Default: NPM_EMAIL / NPM_PASSWORD for every instance.
|
|
# Optional per-instance overrides (same email, different password): NPM_PASSWORD_SECONDARY,
|
|
# NPM_PASSWORD_ALLTRA_HYBX, NPM_PASSWORD_FOURTH, NPM_PASSWORD_MIFOS (fallback: NPM_PASSWORD).
|
|
# ssl_not_forced = Let's Encrypt cert exists but "Force SSL" is off (common for JSON-RPC; browsers may still use HTTPS).
|
|
# cert_domain_mismatch = host is bound to a certificate whose SAN/domain list does not cover that hostname.
|
|
# Tunnel-backed hostnames (proxied CNAME → *.cfargotunnel.com) are excluded from origin-cert findings,
|
|
# because Cloudflare terminates public TLS and the tunnel origin uses noTLSVerify by design.
|
|
# Usage: bash scripts/verify/audit-npmplus-ssl-all-instances.sh [--json]
|
|
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
|
|
|
|
JSON_OUT=0
|
|
for _a in "$@"; do [[ "$_a" == "--json" ]] && JSON_OUT=1; done
|
|
|
|
_orig_npm_email="${NPM_EMAIL:-}"
|
|
_orig_npm_password="${NPM_PASSWORD:-}"
|
|
if [ -f "$PROJECT_ROOT/.env" ]; then
|
|
set +u
|
|
# shellcheck source=/dev/null
|
|
source "$PROJECT_ROOT/.env"
|
|
set -u
|
|
fi
|
|
[ -n "$_orig_npm_email" ] && NPM_EMAIL="$_orig_npm_email"
|
|
[ -n "$_orig_npm_password" ] && NPM_PASSWORD="$_orig_npm_password"
|
|
|
|
NPM_EMAIL="${NPM_EMAIL:-}"
|
|
NPM_PASSWORD="${NPM_PASSWORD:-}"
|
|
if [ -z "$NPM_PASSWORD" ]; then
|
|
echo "NPM_PASSWORD required (repo .env or export)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
CF_ZONE_ID="${CLOUDFLARE_ZONE_ID_D_BIS_ORG:-${CLOUDFLARE_ZONE_ID:-}}"
|
|
cf_api_get() {
|
|
local path="$1"
|
|
if [ -n "${CLOUDFLARE_API_TOKEN:-}" ]; then
|
|
curl -s "https://api.cloudflare.com/client/v4${path}" \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}"
|
|
elif [ -n "${CLOUDFLARE_API_KEY:-}" ] && [ -n "${CLOUDFLARE_EMAIL:-}" ]; then
|
|
curl -s "https://api.cloudflare.com/client/v4${path}" \
|
|
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
|
|
-H "X-Auth-Key: ${CLOUDFLARE_API_KEY}"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
is_tunnel_backed_host() {
|
|
local host="$1"
|
|
[ -n "$CF_ZONE_ID" ] || return 1
|
|
local resp
|
|
resp=$(cf_api_get "/zones/${CF_ZONE_ID}/dns_records?name=${host}" 2>/dev/null || true)
|
|
echo "$resp" | jq -e '
|
|
any(.result[]?;
|
|
.type == "CNAME"
|
|
and (.proxied == true)
|
|
and ((.content // "") | endswith(".cfargotunnel.com"))
|
|
)
|
|
' >/dev/null 2>&1
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
NPM_CURL_MAX_TIME="${NPM_CURL_MAX_TIME:-120}"
|
|
curl_npm() { curl -s -k -L --connect-timeout 8 --max-time "$NPM_CURL_MAX_TIME" "$@"; }
|
|
|
|
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"
|
|
}
|
|
|
|
auth_api_get() {
|
|
local npm_url="$1"
|
|
local token="$2"
|
|
local cookie_jar="$3"
|
|
local path="$4"
|
|
if [ -n "$token" ]; then
|
|
curl_npm -X GET "$npm_url$path" -H "Authorization: Bearer $token"
|
|
else
|
|
curl_npm -b "$cookie_jar" -c "$cookie_jar" -X GET "$npm_url$path"
|
|
fi
|
|
}
|
|
|
|
try_base_url() {
|
|
local base="$1"
|
|
curl_npm -o /dev/null -w "%{http_code}" "$base/" 2>/dev/null | grep -qE '^(200|301|302|401)$'
|
|
}
|
|
|
|
pick_working_url() {
|
|
local ip="$1"
|
|
local https="https://${ip}:81"
|
|
local http="http://${ip}:81"
|
|
if try_base_url "$https"; then echo "$https"; return 0; fi
|
|
if try_base_url "$http"; then echo "$http"; return 0; fi
|
|
return 1
|
|
}
|
|
|
|
# label|ip — URLs resolved at runtime (http vs https)
|
|
INSTANCES=(
|
|
"primary|${IP_NPMPLUS:-192.168.11.167}"
|
|
"secondary|${IP_NPMPLUS_SECONDARY:-192.168.11.168}"
|
|
"alltra-hybx|${IP_NPMPLUS_ALLTRA_HYBX:-192.168.11.169}"
|
|
"fourth-dev|${IP_NPMPLUS_FOURTH:-192.168.11.170}"
|
|
"mifos|${IP_NPMPLUS_MIFOS:-192.168.11.171}"
|
|
)
|
|
|
|
audit_one() {
|
|
local label="$1"
|
|
local npm_url="$2"
|
|
local pass="$3"
|
|
local auth_json token hosts_json certs_json auth_resp cookie_jar auth_mode tunnel_hosts
|
|
cookie_jar="$(mktemp)"
|
|
auth_json=$(jq -n --arg identity "$NPM_EMAIL" --arg secret "$pass" '{identity:$identity,secret:$secret}')
|
|
auth_resp=$(curl_npm -c "$cookie_jar" -X POST "$npm_url/api/tokens" -H "Content-Type: application/json" -d "$auth_json")
|
|
token=$(extract_json_token "$auth_resp")
|
|
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
|
auth_mode="bearer"
|
|
elif cookie_has_session "$cookie_jar"; then
|
|
auth_mode="cookie"
|
|
else
|
|
jq -n --arg l "$label" --arg u "$npm_url" \
|
|
'{instance:$l, npm_url:$u, status:"auth_failed", issues:[{type:"auth",detail:"no token"}]}'
|
|
rm -f "$cookie_jar"
|
|
return 0
|
|
fi
|
|
hosts_json=$(auth_api_get "$npm_url" "$token" "$cookie_jar" "/api/nginx/proxy-hosts")
|
|
certs_json=$(auth_api_get "$npm_url" "$token" "$cookie_jar" "/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
|
|
jq -n --arg l "$label" --arg u "$npm_url" --arg mode "$auth_mode" \
|
|
'{instance:$l, npm_url:$u, status:"auth_failed", auth_mode:$mode, issues:[{type:"auth",detail:"authenticated but API payload was not usable"}]}'
|
|
rm -f "$cookie_jar"
|
|
return 0
|
|
fi
|
|
|
|
tunnel_hosts='[]'
|
|
while IFS= read -r host; do
|
|
[ -n "$host" ] || continue
|
|
if is_tunnel_backed_host "$host"; then
|
|
tunnel_hosts=$(jq -n --argjson acc "$tunnel_hosts" --arg h "$host" '$acc + [$h]')
|
|
fi
|
|
done < <(echo "$hosts_json" | jq -r '.[] | select((.enabled // true) != false) | ((.domain_names // [])[0] // empty)')
|
|
|
|
echo "$hosts_json" "$certs_json" | jq -s --arg l "$label" --arg u "$npm_url" --arg mode "$auth_mode" --argjson tunnel_hosts "$tunnel_hosts" '
|
|
.[0] as $hosts | .[1] as $certs |
|
|
($certs | if type == "array" then . else [] end) as $cl |
|
|
($cl | map({(.id | tostring): .}) | add // {}) as $cmap |
|
|
(now | floor) as $now |
|
|
def host_covered_by_cert($host; $names):
|
|
any(($names // [])[]; . == $host or (startswith("*.") and ($host | endswith("." + .[2:])) and ($host != .[2:])));
|
|
def host_uses_tunnel($host):
|
|
any(($tunnel_hosts // [])[]; . == $host);
|
|
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;
|
|
[ $hosts[] | select((.enabled // true) != false) ] as $hen |
|
|
[ $hen[] | . as $h |
|
|
($h.domain_names // []) | join(",") as $dn |
|
|
(($h.domain_names // [])[0] // "") as $host0 |
|
|
($h.certificate_id // null) as $cid |
|
|
if host_uses_tunnel($host0) then
|
|
empty
|
|
elif $cid == null or $cid == 0 then
|
|
{domains:$dn, type:"no_certificate", ssl_forced:($h.ssl_forced // false), forward:(($h.forward_scheme//"") + "://" + ($h.forward_host//"") + ":" + (($h.forward_port|tostring)//""))}
|
|
else
|
|
($cmap[($cid|tostring)] // null) as $c |
|
|
if $c == null then
|
|
{domains:$dn, type:"missing_cert_record", certificate_id:$cid, ssl_forced:($h.ssl_forced // false)}
|
|
else
|
|
parse_exp($c) as $es |
|
|
epoch_if($es) as $ep |
|
|
if $ep != null and $ep < $now then
|
|
{domains:$dn, type:"expired", certificate_id:$cid, expires_on:$es, ssl_forced:($h.ssl_forced // false)}
|
|
elif (host_covered_by_cert(($h.domain_names // [])[0]; ($c.domain_names // [])) | not) then
|
|
{domains:$dn, type:"cert_domain_mismatch", certificate_id:$cid, expires_on:$es, cert_domains:($c.domain_names // []), ssl_forced:($h.ssl_forced // false)}
|
|
elif ($h.ssl_forced // false) != true then
|
|
{domains:$dn, type:"ssl_not_forced", certificate_id:$cid, expires_on:$es, ssl_forced:false}
|
|
else empty end
|
|
end
|
|
end
|
|
] as $issues |
|
|
{
|
|
instance: $l,
|
|
npm_url: $u,
|
|
auth_mode: $mode,
|
|
status: "ok",
|
|
proxy_hosts_enabled: ($hen | length),
|
|
certificates: ($cl | length),
|
|
issues: $issues
|
|
}
|
|
'
|
|
rm -f "$cookie_jar"
|
|
}
|
|
|
|
results='[]'
|
|
for row in "${INSTANCES[@]}"; do
|
|
label="${row%%|*}"
|
|
ip="${row#*|}"
|
|
if ! base=$(pick_working_url "$ip"); then
|
|
piece=$(jq -n --arg l "$label" --arg ip "$ip" \
|
|
'{instance:$l, ip:$ip, status:"unreachable", issues:[{type:"unreachable",detail:"no HTTP/HTTPS on :81"}]}')
|
|
results=$(jq -n --argjson acc "$results" --argjson p "$(echo "$piece" | jq -c .)" '$acc + [$p]')
|
|
continue
|
|
fi
|
|
pw=$(password_for_label "$label")
|
|
piece=$(audit_one "$label" "$base" "$pw")
|
|
results=$(jq -n --argjson acc "$results" --argjson p "$(echo "$piece" | jq -c .)" '$acc + [$p]')
|
|
done
|
|
|
|
if [ "$JSON_OUT" = 1 ]; then
|
|
echo "$results" | jq .
|
|
exit 0
|
|
fi
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "NPMplus SSL audit (all instances)"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
|
|
echo "$results" | jq -r '.[] |
|
|
"── \(.instance) (\(.npm_url // .ip // "?")) ──",
|
|
(if .status == "unreachable" then " UNREACHABLE: \(.issues[0].detail // .status)"
|
|
elif .status == "auth_failed" then " AUTH FAILED (wrong NPM_EMAIL/NPM_PASSWORD for this UI?)"
|
|
else
|
|
" Proxy hosts (enabled): \(.proxy_hosts_enabled // 0) | Certificate objects: \(.certificates // 0)",
|
|
(if (.issues | length) == 0 then " No issues: all enabled hosts have active cert + ssl_forced."
|
|
else
|
|
(.issues[] | " • [\(.type)] \(.domains)\(if .certificate_id then " (cert_id=\(.certificate_id))" else "" end)\(if .expires_on then " expires=\(.expires_on)" else "" end)\(if .cert_domains then " cert_domains=\(.cert_domains|join(","))" else "" end)\(if .forward then " → \(.forward)" else "" end)")
|
|
end)
|
|
end),
|
|
""'
|
|
|
|
issue_total=$(echo "$results" | jq '[.[].issues | length] | add')
|
|
if [ "${issue_total:-0}" -eq 0 ]; then
|
|
echo "Summary: no SSL gaps reported (or all instances unreachable/auth failed — see above)."
|
|
else
|
|
echo "Summary: $issue_total issue row(s) across instances (no_cert, expired, cert_domain_mismatch, ssl_not_forced, missing_cert_record)."
|
|
fi
|
|
echo ""
|
|
echo "Fix: NPM UI → Proxy Hosts → SSL, or scripts/nginx-proxy-manager/fix-npmplus-ssl-issues.sh per NPM_URL."
|