Files
proxmox/scripts/request-npmplus-certificates.sh
defiQUG bea1903ac9
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
Sync all local changes: docs, config, scripts, submodule refs, verification evidence
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 15:46:06 -08:00

219 lines
9.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
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
# Request SSL certificates for proxy hosts in NPMplus that do NOT already have one.
# Skips hosts with certificate_id set to avoid duplicate/inactive cert spam.
# Uses .env for NPM_URL, NPM_EMAIL, NPM_PASSWORD when run from repo root.
# Optional args: PROXMOX_HOST CONTAINER_ID NPM_URL NPM_EMAIL NPM_PASSWORD SSL_EMAIL
# Env: FIRST_ONLY=1 or FIRST_ONLY=true request cert for only the first host without one (verify before running for rest).
# Cleanup guide: docs/04-configuration/NPMPLUS_TLS_CLEANUP.md
set -euo pipefail
# Load IP configuration
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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# 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 .env (set +u so values with $ in them don't trigger unbound variable)
if [ -f "$PROJECT_ROOT/.env" ]; then
set +u
set -a
# shellcheck source=/dev/null
source "$PROJECT_ROOT/.env" 2>/dev/null || true
set +a
set -u
[ -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"
fi
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
log_error() { echo -e "${RED}[✗]${NC} $1"; }
PROXMOX_HOST="${1:-192.168.11.11}"
CONTAINER_ID="${2:-10233}"
# Default .167: NPMplus (VMID 10233) at ${IP_NPMPLUS:-${IP_NPMPLUS:-192.168.11.167}}:81; set NPM_URL in .env or pass as 3rd arg to override
NPM_URL="${3:-${NPM_URL:-https://${IP_NPMPLUS}:81}}"
NPM_EMAIL="${4:-${NPM_EMAIL:-admin@example.org}}"
NPM_PASSWORD="${5:-${NPM_PASSWORD:-}}"
if [ -z "$NPM_PASSWORD" ]; then
log_error "NPM_PASSWORD is required. Set it in .env or pass as 5th argument"
exit 1
fi
SSL_EMAIL="${6:-${SSL_EMAIL:-nsatoshi2007@hotmail.com}}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔒 NPMplus Certificate Request"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Authenticate (use jq to build JSON so password is safely escaped)
log_info "Authenticating to NPMplus API..."
AUTH_JSON=$(jq -n --arg identity "$NPM_EMAIL" --arg secret "$NPM_PASSWORD" '{identity:$identity,secret:$secret}')
TOKEN_RESPONSE=$(curl -s -k -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 '.error.message // "Unknown error"' 2>/dev/null || echo "Unknown error")
log_error "Failed to authenticate: $ERROR_MSG"
log_info "Response: $TOKEN_RESPONSE"
log_info ""
log_info "Trying to reset password or check credentials..."
exit 1
fi
log_success "Authenticated successfully"
echo ""
# Get all proxy hosts
log_info "Fetching proxy hosts..."
PROXY_HOSTS_JSON=$(curl -s -k -X GET "$NPM_URL/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json")
PROXY_COUNT=$(echo "$PROXY_HOSTS_JSON" | jq -r 'length' 2>/dev/null || echo "0")
log_info "Found $PROXY_COUNT proxy hosts"
echo ""
if [ "$PROXY_COUNT" = "0" ]; then
log_warn "No proxy hosts found"
exit 0
fi
# Build list of hosts that need a certificate (id|domain, one per line)
NEED_CERT_LIST=$(echo "$PROXY_HOSTS_JSON" | jq -r '.[] | select(.certificate_id == null or .certificate_id == 0) | "\(.id)|\(.domain_names[0] // "")"' 2>/dev/null | while IFS='|' read -r id domain; do
[ -z "$domain" ] || [ "$domain" = "null" ] && continue
echo "$domain" | grep -q "test.*example.com" && continue
echo "${id}|${domain}"
done)
NEED_CERT_COUNT=$(echo "$NEED_CERT_LIST" | grep -c . 2>/dev/null || echo "0")
if [ "$NEED_CERT_COUNT" = "0" ]; then
log_success "No proxy hosts need a certificate (all have one)."
exit 0
fi
# Optional: only process domains matching this grep pattern (e.g. "rpc-fireblocks|ws.rpc-fireblocks")
if [ -n "${CERT_DOMAINS_FILTER:-}" ]; then
NEED_CERT_LIST=$(echo "$NEED_CERT_LIST" | grep -E "$CERT_DOMAINS_FILTER" || true)
NEED_CERT_COUNT=$(echo "$NEED_CERT_LIST" | grep -c . 2>/dev/null || echo "0")
log_info "CERT_DOMAINS_FILTER=$CERT_DOMAINS_FILTER $NEED_CERT_COUNT host(s) to process"
[ "$NEED_CERT_COUNT" = "0" ] && log_warn "No hosts match filter; nothing to do." && exit 0
echo ""
fi
# FIRST_ONLY: process only the first host (verify renewal/working before adding rest)
FIRST_ONLY="${FIRST_ONLY:-0}"
if [ "$FIRST_ONLY" = "1" ] || [ "$FIRST_ONLY" = "true" ] || [ "$FIRST_ONLY" = "yes" ]; then
NEED_CERT_LIST=$(echo "$NEED_CERT_LIST" | head -n 1)
log_warn "FIRST_ONLY=1 processing only the first host without a cert. Verify renewal date and that it works, then run without FIRST_ONLY for the rest."
echo ""
fi
# Try to get DNS (Cloudflare) credential_id so we use same method as UI (DNS challenge)
CREDENTIAL_ID=""
for path in "/api/nginx/letsencrypt-credentials" "/api/letsencrypt-credentials"; do
CRED_JSON=$(curl -s -k -X GET "$NPM_URL$path" -H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "[]")
if echo "$CRED_JSON" | jq -e 'type == "array" and length > 0' >/dev/null 2>&1; then
CREDENTIAL_ID=$(echo "$CRED_JSON" | jq -r '.[0].id // .[0].credential_id // empty' 2>/dev/null)
[ -n "$CREDENTIAL_ID" ] && [ "$CREDENTIAL_ID" != "null" ] && break
fi
done
if [ -n "$CREDENTIAL_ID" ] && [ "$CREDENTIAL_ID" != "null" ]; then
log_info "Using DNS challenge (credential_id: $CREDENTIAL_ID)"
else
log_info "No DNS credential found request will use Let's Encrypt defaults (HTTP or NPM default). Add Cloudflare credential in NPM UI for DNS."
fi
echo ""
# Process each host that needs a cert
success_count=0
fail_count=0
while IFS='|' read -r host_id domain; do
[ -z "$host_id" ] || [ -z "$domain" ] && continue
log_info "Processing: $domain (Host ID: $host_id)"
# Request certificate. NPM API accepts only domain_names + provider (extra keys cause "must NOT have additional properties").
# For DNS (Cloudflare) and correct expiry, request certs in NPM UI: Hosts → host → SSL → Request new SSL Certificate → DNS Challenge, Cloudflare.
log_info " Requesting SSL certificate..."
CERT_RESPONSE=$(curl -s -k -X POST "$NPM_URL/api/nginx/certificates" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg domain "$domain" '{ domain_names: [$domain], provider: "letsencrypt" }')")
NEW_CERT_ID=$(echo "$CERT_RESPONSE" | jq -r '.id // empty' 2>/dev/null || echo "")
if [ -z "$NEW_CERT_ID" ] || [ "$NEW_CERT_ID" = "null" ]; then
ERROR=$(echo "$CERT_RESPONSE" | jq -r '.error.message // .error // "Unknown error"' 2>/dev/null || echo "Unknown error")
log_warn " Certificate request failed: $ERROR"
log_info " Certificate may be processing in background"
fail_count=$((fail_count + 1))
else
log_success " Certificate requested (ID: $NEW_CERT_ID)"
# Update proxy host to use certificate
log_info " Assigning certificate to proxy host..."
UPDATE_RESPONSE=$(curl -s -k -X PUT "$NPM_URL/api/nginx/proxy-hosts/$host_id" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"certificate_id\": $NEW_CERT_ID,
\"ssl_forced\": true,
\"http2_support\": true,
\"hsts_enabled\": true,
\"hsts_subdomains\": true
}")
UPDATE_ID=$(echo "$UPDATE_RESPONSE" | jq -r '.id // empty' 2>/dev/null || echo "")
if [ -n "$UPDATE_ID" ] && [ "$UPDATE_ID" != "null" ]; then
log_success " ✓ Certificate assigned to proxy host"
success_count=$((success_count + 1))
else
ERROR=$(echo "$UPDATE_RESPONSE" | jq -r '.error.message // .error // "Unknown error"' 2>/dev/null || echo "Unknown error")
log_warn " Failed to assign certificate: $ERROR"
fail_count=$((fail_count + 1))
fi
fi
echo ""
sleep 2 # Rate limiting
done <<< "$NEED_CERT_LIST"
# Summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log_info "Summary:"
log_success " Successful: $success_count"
log_warn " Failed: $fail_count"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
log_info "Note: Certificate requests may take a few minutes to process"
log_info "Check NPMplus UI to verify certificate status"
if [ "$FIRST_ONLY" = "1" ] || [ "$FIRST_ONLY" = "true" ] || [ "$FIRST_ONLY" = "yes" ]; then
log_info "After verifying renewal date and that the cert works: run ./scripts/request-npmplus-certificates.sh (no FIRST_ONLY) for the remaining hosts."
fi
echo ""