Files
proxmox/scripts/configure-cloudflare-api.sh
defiQUG cb47cce074 Complete markdown files cleanup and organization
- Organized 252 files across project
- Root directory: 187 → 2 files (98.9% reduction)
- Moved configuration guides to docs/04-configuration/
- Moved troubleshooting guides to docs/09-troubleshooting/
- Moved quick start guides to docs/01-getting-started/
- Moved reports to reports/ directory
- Archived temporary files
- Generated comprehensive reports and documentation
- Created maintenance scripts and guides

All files organized according to established standards.
2026-01-06 01:46:25 -08:00

472 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# Configure Cloudflare Tunnel Routes and DNS Records via API
# Usage: ./configure-cloudflare-api.sh
# Requires: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID environment variables
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
debug() { echo -e "${BLUE}[DEBUG]${NC} $1"; }
# Check for required tools
if ! command -v curl >/dev/null 2>&1; then
error "curl is required but not installed"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
error "jq is required but not installed. Install with: apt-get install jq"
exit 1
fi
# Load environment variables
if [[ -f "$SCRIPT_DIR/../.env" ]]; then
source "$SCRIPT_DIR/../.env"
fi
# Cloudflare API configuration (support multiple naming conventions)
CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-}"
CLOUDFLARE_ZONE_ID="${CLOUDFLARE_ZONE_ID:-}"
CLOUDFLARE_ACCOUNT_ID="${CLOUDFLARE_ACCOUNT_ID:-}"
CLOUDFLARE_EMAIL="${CLOUDFLARE_EMAIL:-}"
CLOUDFLARE_API_KEY="${CLOUDFLARE_API_KEY:-}"
DOMAIN="${DOMAIN:-${CLOUDFLARE_DOMAIN:-d-bis.org}}"
# Tunnel configuration (support multiple naming conventions)
# Prefer JWT token from installed service, then env vars
INSTALLED_TOKEN=""
if command -v ssh >/dev/null 2>&1; then
INSTALLED_TOKEN=$(ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@${PROXMOX_HOST:-192.168.11.10} \
"pct exec 102 -- cat /etc/systemd/system/cloudflared.service 2>/dev/null | grep -o 'tunnel run --token [^ ]*' | cut -d' ' -f3" 2>/dev/null || echo "")
fi
TUNNEL_TOKEN="${INSTALLED_TOKEN:-${TUNNEL_TOKEN:-${CLOUDFLARE_TUNNEL_TOKEN:-eyJhIjoiNTJhZDU3YTcxNjcxYzVmYzAwOWVkZjA3NDQ2NTgxOTYiLCJ0IjoiMTBhYjIyZGEtOGVhMy00ZTJlLWE4OTYtMjdlY2UyMjExYTA1IiwicyI6IlptRXlOMkkyTVRrdE1EZzFNeTAwTkRBNExXSXhaalF0Wm1KaE5XVmpaVEEzTVdGbCJ9}}}"
# RPC endpoint configuration
# Public endpoints route to VMID 2502 (NO JWT authentication)
# Private endpoints route to VMID 2501 (JWT authentication required)
declare -A RPC_ENDPOINTS=(
[rpc-http-pub]="https://192.168.11.252:443"
[rpc-ws-pub]="https://192.168.11.252:443"
[rpc-http-prv]="https://192.168.11.251:443"
[rpc-ws-prv]="https://192.168.11.251:443"
)
# API base URLs
CF_API_BASE="https://api.cloudflare.com/client/v4"
CF_ZERO_TRUST_API="https://api.cloudflare.com/client/v4/accounts"
# Function to make Cloudflare API request
cf_api_request() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
local url="${CF_API_BASE}${endpoint}"
local headers=()
if [[ -n "$CLOUDFLARE_API_TOKEN" ]]; then
headers+=("-H" "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
elif [[ -n "$CLOUDFLARE_API_KEY" ]]; then
# Global API Keys are typically 40 chars, API Tokens are longer
# If no email provided, assume it's an API Token
if [[ -z "$CLOUDFLARE_EMAIL" ]] || [[ ${#CLOUDFLARE_API_KEY} -gt 50 ]]; then
headers+=("-H" "Authorization: Bearer ${CLOUDFLARE_API_KEY}")
else
headers+=("-H" "X-Auth-Email: ${CLOUDFLARE_EMAIL}")
headers+=("-H" "X-Auth-Key: ${CLOUDFLARE_API_KEY}")
fi
else
error "Cloudflare API credentials not found!"
error "Set CLOUDFLARE_API_TOKEN or CLOUDFLARE_EMAIL + CLOUDFLARE_API_KEY"
exit 1
fi
headers+=("-H" "Content-Type: application/json")
local response
if [[ -n "$data" ]]; then
response=$(curl -s -X "$method" "$url" "${headers[@]}" -d "$data")
else
response=$(curl -s -X "$method" "$url" "${headers[@]}")
fi
# Check if response is valid JSON
if ! echo "$response" | jq -e . >/dev/null 2>&1; then
error "Invalid JSON response from API"
debug "Response: $response"
return 1
fi
# Check for API errors
local success=$(echo "$response" | jq -r '.success // false' 2>/dev/null)
if [[ "$success" != "true" ]]; then
local errors=$(echo "$response" | jq -r '.errors[]?.message // .error // "Unknown error"' 2>/dev/null | head -3)
if [[ -z "$errors" ]]; then
errors="API request failed (check response)"
fi
error "API request failed: $errors"
debug "Response: $response"
return 1
fi
echo "$response"
}
# Function to get zone ID from domain
get_zone_id() {
if [[ -n "$CLOUDFLARE_ZONE_ID" ]]; then
echo "$CLOUDFLARE_ZONE_ID"
return 0
fi
info "Getting zone ID for domain: $DOMAIN"
local response=$(cf_api_request "GET" "/zones?name=${DOMAIN}")
local zone_id=$(echo "$response" | jq -r '.result[0].id // empty')
if [[ -z "$zone_id" ]]; then
error "Zone not found for domain: $DOMAIN"
exit 1
fi
info "Zone ID: $zone_id"
echo "$zone_id"
}
# Function to get account ID (needed for Zero Trust API)
get_account_id() {
info "Getting account ID..."
# Try to get from token verification
local response=$(cf_api_request "GET" "/user/tokens/verify")
local account_id=$(echo "$response" | jq -r '.result.id // empty')
if [[ -z "$account_id" ]]; then
# Try alternative: get from accounts list
response=$(cf_api_request "GET" "/accounts")
account_id=$(echo "$response" | jq -r '.result[0].id // empty')
fi
if [[ -z "$account_id" ]]; then
# Last resort: try to get from zone
local zone_id=$(get_zone_id)
response=$(cf_api_request "GET" "/zones/${zone_id}")
account_id=$(echo "$response" | jq -r '.result.account.id // empty')
fi
if [[ -z "$account_id" ]]; then
error "Could not determine account ID"
error "You may need to specify CLOUDFLARE_ACCOUNT_ID in .env file"
exit 1
fi
info "Account ID: $account_id"
echo "$account_id"
}
# Function to extract tunnel ID from token
get_tunnel_id_from_token() {
local token="$1"
# Check if it's a JWT token (has dots)
if [[ "$token" == *.*.* ]]; then
# Decode JWT token (basic base64 decode of payload)
local payload=$(echo "$token" | cut -d'.' -f2)
# Add padding if needed
local padding=$((4 - ${#payload} % 4))
if [[ $padding -ne 4 ]]; then
payload="${payload}$(printf '%*s' $padding | tr ' ' '=')"
fi
# Decode and extract tunnel ID (field 't' contains tunnel ID)
if command -v python3 >/dev/null 2>&1; then
echo "$payload" | python3 -c "import sys, base64, json; payload=sys.stdin.read().strip(); padding=4-len(payload)%4; payload+=('='*padding if padding<4 else ''); data=json.loads(base64.b64decode(payload)); print(data.get('t', ''))" 2>/dev/null || echo ""
else
echo "$payload" | base64 -d 2>/dev/null | jq -r '.t // empty' 2>/dev/null || echo ""
fi
else
# Not a JWT token, return empty
echo ""
fi
}
# Function to get tunnel ID
get_tunnel_id() {
local account_id="$1"
local token="$2"
# Try to extract from JWT token first
local tunnel_id=$(get_tunnel_id_from_token "$token")
if [[ -n "$tunnel_id" ]]; then
info "Tunnel ID from token: $tunnel_id"
echo "$tunnel_id"
return 0
fi
# Fallback: list tunnels and find the one
warn "Could not extract tunnel ID from token, listing tunnels..."
local response=$(cf_api_request "GET" "/accounts/${account_id}/cfd_tunnel" 2>/dev/null)
if [[ -z "$response" ]]; then
error "Failed to list tunnels. Check API credentials."
exit 1
fi
local tunnel_id=$(echo "$response" | jq -r '.result[0].id // empty' 2>/dev/null)
if [[ -z "$tunnel_id" ]]; then
error "Could not find tunnel ID"
debug "Response: $response"
exit 1
fi
info "Tunnel ID: $tunnel_id"
echo "$tunnel_id"
}
# Function to get tunnel name
get_tunnel_name() {
local account_id="$1"
local tunnel_id="$2"
local response=$(cf_api_request "GET" "/accounts/${account_id}/cfd_tunnel/${tunnel_id}")
local tunnel_name=$(echo "$response" | jq -r '.result.name // empty')
echo "$tunnel_name"
}
# Function to configure tunnel routes
configure_tunnel_routes() {
local account_id="$1"
local tunnel_id="$2"
local tunnel_name="$3"
info "Configuring tunnel routes for: $tunnel_name"
# Build ingress rules array
local ingress_array="["
local first=true
for subdomain in "${!RPC_ENDPOINTS[@]}"; do
local service="${RPC_ENDPOINTS[$subdomain]}"
local hostname="${subdomain}.${DOMAIN}"
if [[ "$first" == "true" ]]; then
first=false
else
ingress_array+=","
fi
# Determine if WebSocket
local is_ws=false
if [[ "$subdomain" == *"ws"* ]]; then
is_ws=true
fi
# Build ingress rule
# Add noTLSVerify to skip certificate validation (certificates don't have IP SANs)
if [[ "$is_ws" == "true" ]]; then
ingress_array+="{\"hostname\":\"${hostname}\",\"service\":\"${service}\",\"originRequest\":{\"httpHostHeader\":\"${hostname}\",\"noTLSVerify\":true}}"
else
ingress_array+="{\"hostname\":\"${hostname}\",\"service\":\"${service}\",\"originRequest\":{\"noTLSVerify\":true}}"
fi
info " Adding route: ${hostname}${service}"
done
# Add catch-all (must be last)
ingress_array+=",{\"service\":\"http_status:404\"}]"
# Create config JSON
local config_data=$(echo "$ingress_array" | jq -c '{
config: {
ingress: .
}
}')
info "Updating tunnel configuration..."
local response=$(cf_api_request "PUT" "/accounts/${account_id}/cfd_tunnel/${tunnel_id}/configurations" "$config_data")
if echo "$response" | jq -e '.success' >/dev/null 2>&1; then
info "✓ Tunnel routes configured successfully"
else
local errors=$(echo "$response" | jq -r '.errors[]?.message // "Unknown error"' | head -3)
error "Failed to configure tunnel routes: $errors"
debug "Response: $response"
return 1
fi
}
# Function to create or update DNS record
create_or_update_dns_record() {
local zone_id="$1"
local name="$2"
local target="$3"
local proxied="${4:-true}"
# Check if record exists
local response=$(cf_api_request "GET" "/zones/${zone_id}/dns_records?name=${name}.${DOMAIN}&type=CNAME")
local record_id=$(echo "$response" | jq -r '.result[0].id // empty')
local data=$(jq -n \
--arg name "${name}.${DOMAIN}" \
--arg target "$target" \
--argjson proxied "$proxied" \
'{
type: "CNAME",
name: $name,
content: $target,
proxied: $proxied,
ttl: 1
}')
if [[ -n "$record_id" ]]; then
info " Updating existing DNS record: ${name}.${DOMAIN}"
response=$(cf_api_request "PUT" "/zones/${zone_id}/dns_records/${record_id}" "$data")
else
info " Creating DNS record: ${name}.${DOMAIN}"
response=$(cf_api_request "POST" "/zones/${zone_id}/dns_records" "$data")
fi
if echo "$response" | jq -e '.success' >/dev/null 2>&1; then
info " ✓ DNS record configured"
else
error " ✗ Failed to configure DNS record"
return 1
fi
}
# Function to configure DNS records
configure_dns_records() {
local zone_id="$1"
local tunnel_id="$2"
local tunnel_target="${tunnel_id}.cfargotunnel.com"
info "Configuring DNS records..."
info "Tunnel target: $tunnel_target"
for subdomain in "${!RPC_ENDPOINTS[@]}"; do
create_or_update_dns_record "$zone_id" "$subdomain" "$tunnel_target" "true"
done
}
# Main execution
main() {
info "Cloudflare API Configuration Script"
info "===================================="
echo ""
# Validate credentials
if [[ -z "$CLOUDFLARE_API_TOKEN" ]] && [[ -z "$CLOUDFLARE_EMAIL" ]] && [[ -z "$CLOUDFLARE_API_KEY" ]]; then
error "Cloudflare API credentials required!"
echo ""
echo "Set one of:"
echo " export CLOUDFLARE_API_TOKEN='your-api-token'"
echo " OR"
echo " export CLOUDFLARE_EMAIL='your-email@example.com'"
echo " export CLOUDFLARE_API_KEY='your-api-key'"
echo ""
echo "You can also create a .env file in the project root with these variables."
exit 1
fi
# If API_KEY is provided but no email, we need email for Global API Key
if [[ -n "$CLOUDFLARE_API_KEY" ]] && [[ -z "$CLOUDFLARE_EMAIL" ]] && [[ -z "$CLOUDFLARE_API_TOKEN" ]]; then
error "CLOUDFLARE_API_KEY requires CLOUDFLARE_EMAIL"
error "Please add CLOUDFLARE_EMAIL to your .env file"
error ""
error "OR create an API Token instead:"
error " 1. Go to: https://dash.cloudflare.com/profile/api-tokens"
error " 2. Create token with: Zone:DNS:Edit, Account:Cloudflare Tunnel:Edit"
error " 3. Set CLOUDFLARE_API_TOKEN in .env"
exit 1
fi
# Get zone ID
local zone_id=$(get_zone_id)
# Get account ID
local account_id="${CLOUDFLARE_ACCOUNT_ID:-}"
if [[ -z "$account_id" ]]; then
account_id=$(get_account_id)
else
info "Using provided Account ID: $account_id"
fi
# Get tunnel ID - try from .env first, then extraction, then API
local tunnel_id="${CLOUDFLARE_TUNNEL_ID:-}"
# If not in .env, try to extract from JWT token
if [[ -z "$tunnel_id" ]] && [[ "$TUNNEL_TOKEN" == *.*.* ]]; then
local payload=$(echo "$TUNNEL_TOKEN" | cut -d'.' -f2)
local padding=$((4 - ${#payload} % 4))
if [[ $padding -ne 4 ]]; then
payload="${payload}$(printf '%*s' $padding | tr ' ' '=')"
fi
if command -v python3 >/dev/null 2>&1; then
tunnel_id=$(echo "$payload" | python3 -c "import sys, base64, json; payload=sys.stdin.read().strip(); padding=4-len(payload)%4; payload+=('='*padding if padding<4 else ''); data=json.loads(base64.b64decode(payload)); print(data.get('t', ''))" 2>/dev/null || echo "")
fi
fi
# If extraction failed, try API (but don't fail if API doesn't work)
if [[ -z "$tunnel_id" ]]; then
tunnel_id=$(get_tunnel_id "$account_id" "$TUNNEL_TOKEN" 2>/dev/null || echo "")
fi
if [[ -z "$tunnel_id" ]]; then
error "Could not determine tunnel ID"
error "Please set CLOUDFLARE_TUNNEL_ID in .env file"
error "Or ensure API credentials are valid to fetch it automatically"
exit 1
fi
info "Using Tunnel ID: $tunnel_id"
local tunnel_name=$(get_tunnel_name "$account_id" "$tunnel_id" 2>/dev/null || echo "tunnel-${tunnel_id:0:8}")
echo ""
info "Configuration Summary:"
echo " Domain: $DOMAIN"
echo " Zone ID: $zone_id"
echo " Account ID: $account_id"
echo " Tunnel: $tunnel_name (ID: $tunnel_id)"
echo ""
# Configure tunnel routes
echo "=========================================="
info "Step 1: Configuring Tunnel Routes"
echo "=========================================="
configure_tunnel_routes "$account_id" "$tunnel_id" "$tunnel_name"
echo ""
echo "=========================================="
info "Step 2: Configuring DNS Records"
echo "=========================================="
configure_dns_records "$zone_id" "$tunnel_id"
echo ""
echo "=========================================="
info "Configuration Complete!"
echo "=========================================="
echo ""
info "Next steps:"
echo " 1. Wait 1-2 minutes for DNS propagation"
echo " 2. Test endpoints:"
echo " curl https://rpc-http-pub.d-bis.org/health"
echo " 3. Verify in Cloudflare Dashboard:"
echo " - Zero Trust → Networks → Tunnels → Check routes"
echo " - DNS → Records → Verify CNAME records"
}
# Run main function
main