2026-02-12 15:46:57 -08:00
#!/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
2026-02-21 15:46:06 -08:00
# 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
2026-02-12 15:46:57 -08:00
# 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 ""