- 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.
472 lines
16 KiB
Bash
Executable File
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
|
|
|