#!/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 ""