Files
proxmox/scripts/cloudflare-tunnels/scripts/automate-cloudflare-setup.sh
defiQUG fbda1b4beb
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands
- CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround
- CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check
- NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere
- MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates
- LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 15:46:57 -08:00

694 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
# Complete automation of Cloudflare setup via API
# Creates tunnels, DNS records, and Cloudflare Access applications
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)"
TUNNELS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# 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"; }
# Load .env file
if [[ -f "$PROJECT_ROOT/.env" ]]; then
source "$PROJECT_ROOT/.env"
log_info "Loaded credentials from .env"
else
log_error ".env file not found at $PROJECT_ROOT/.env"
exit 1
fi
# Cloudflare API configuration
CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-}"
CLOUDFLARE_EMAIL="${CLOUDFLARE_EMAIL:-}"
CLOUDFLARE_API_KEY="${CLOUDFLARE_API_KEY:-}"
CLOUDFLARE_ZONE_ID="${CLOUDFLARE_ZONE_ID:-}"
CLOUDFLARE_ACCOUNT_ID="${CLOUDFLARE_ACCOUNT_ID:-}"
DOMAIN="${DOMAIN:-d-bis.org}"
# Check for required tools
if ! command -v curl >/dev/null 2>&1; then
log_error "curl is required"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
log_error "jq is required. Install with: apt-get install jq"
exit 1
fi
# 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 auth_header=""
if [[ -n "$CLOUDFLARE_API_TOKEN" ]]; then
auth_header="Authorization: Bearer ${CLOUDFLARE_API_TOKEN}"
elif [[ -n "$CLOUDFLARE_API_KEY" ]] && [[ -n "$CLOUDFLARE_EMAIL" ]]; then
auth_header="X-Auth-Email: ${CLOUDFLARE_EMAIL}"
# Note: We'll need to pass both headers, so we'll use a different approach
else
log_error "Cloudflare API credentials not found!" >&2
exit 1
fi
local response
local http_code
local temp_file=$(mktemp)
# Build curl command with timeout
if [[ -n "$CLOUDFLARE_API_TOKEN" ]]; then
if [[ -n "$data" ]]; then
http_code=$(curl -s --max-time 30 -o "$temp_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$data" 2>/dev/null)
else
http_code=$(curl -s --max-time 30 -o "$temp_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" 2>/dev/null)
fi
else
if [[ -n "$data" ]]; then
http_code=$(curl -s --max-time 30 -o "$temp_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
-H "X-Auth-Key: ${CLOUDFLARE_API_KEY}" \
-H "Content-Type: application/json" \
-d "$data" 2>/dev/null)
else
http_code=$(curl -s --max-time 30 -o "$temp_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \
-H "X-Auth-Key: ${CLOUDFLARE_API_KEY}" \
-H "Content-Type: application/json" 2>/dev/null)
fi
fi
# Read response from temp file
if [[ -f "$temp_file" ]] && [[ -s "$temp_file" ]]; then
response=$(cat "$temp_file")
else
response=""
fi
rm -f "$temp_file"
# Check if response is valid JSON
if [[ -z "$response" ]] || ! echo "$response" | jq -e . >/dev/null 2>&1; then
log_error "Invalid JSON response from API (HTTP ${http_code:-unknown})" >&2
if [[ -n "$response" ]]; then
log_error "Response: $(echo "$response" | head -3)" >&2
fi
return 1
fi
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 [[ "$http_code" != "200" ]] && [[ "$http_code" != "201" ]]; then
log_error "API request failed (HTTP $http_code): $errors" >&2
fi
# Don't return error for GET requests that might return empty results
if [[ "$method" == "GET" ]] && [[ "$http_code" == "200" ]]; then
echo "$response"
return 0
fi
return 1
fi
# Only output JSON to stdout on success
echo "$response"
}
# Get zone ID
get_zone_id() {
if [[ -n "$CLOUDFLARE_ZONE_ID" ]]; then
echo "$CLOUDFLARE_ZONE_ID"
return 0
fi
log_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
log_error "Zone not found for domain: $DOMAIN"
exit 1
fi
log_success "Zone ID: $zone_id"
echo "$zone_id"
}
# Get account ID
get_account_id() {
if [[ -n "$CLOUDFLARE_ACCOUNT_ID" ]]; then
echo "$CLOUDFLARE_ACCOUNT_ID"
return 0
fi
log_info "Getting account ID..."
local response=$(cf_api_request "GET" "/user/tokens/verify")
local account_id=$(echo "$response" | jq -r '.result.id // empty')
if [[ -z "$account_id" ]]; then
response=$(cf_api_request "GET" "/accounts")
account_id=$(echo "$response" | jq -r '.result[0].id // empty')
fi
if [[ -z "$account_id" ]]; then
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
log_error "Could not determine account ID"
exit 1
fi
log_success "Account ID: $account_id"
echo "$account_id"
}
# Create tunnel
create_tunnel() {
local account_id="$1"
local tunnel_name="$2"
log_info "Creating tunnel: $tunnel_name"
# Check if tunnel already exists (skip check if API fails)
local response
response=$(cf_api_request "GET" "/accounts/${account_id}/cfd_tunnel" 2>/dev/null) || response=""
if [[ -n "$response" ]] && echo "$response" | jq -e '.result' >/dev/null 2>&1; then
local existing_id
existing_id=$(echo "$response" | jq -r ".result[]? | select(.name == \"${tunnel_name}\") | .id" 2>/dev/null || echo "")
if [[ -n "$existing_id" ]] && [[ "$existing_id" != "null" ]] && [[ "$existing_id" != "" ]]; then
log_warn "Tunnel $tunnel_name already exists (ID: $existing_id)"
echo "$existing_id"
return 0
fi
else
# If API call failed, log but continue (might be permission issue)
log_warn "Could not check for existing tunnels, attempting to create..."
fi
# Create tunnel
local data
data=$(jq -n \
--arg name "$tunnel_name" \
'{name: $name}' 2>/dev/null)
if [[ -z "$data" ]]; then
log_error "Failed to create tunnel data JSON"
return 1
fi
response=$(cf_api_request "POST" "/accounts/${account_id}/cfd_tunnel" "$data" 2>/dev/null)
local api_result=$?
# If creation failed, check if tunnel already exists (might have been created between check and create)
if [[ $api_result -ne 0 ]] || [[ -z "$response" ]] || ! echo "$response" | jq -e . >/dev/null 2>&1; then
# Try to get existing tunnel one more time
local check_response
check_response=$(cf_api_request "GET" "/accounts/${account_id}/cfd_tunnel" 2>/dev/null) || check_response=""
if [[ -n "$check_response" ]] && echo "$check_response" | jq -e '.result' >/dev/null 2>&1; then
local existing_id
existing_id=$(echo "$check_response" | jq -r ".result[]? | select(.name == \"${tunnel_name}\") | .id" 2>/dev/null || echo "")
if [[ -n "$existing_id" ]] && [[ "$existing_id" != "null" ]] && [[ "$existing_id" != "" ]]; then
log_warn "Tunnel $tunnel_name already exists (ID: $existing_id)"
echo "$existing_id"
return 0
fi
fi
log_error "Failed to create tunnel"
if [[ -n "$response" ]]; then
log_error "Response: $(echo "$response" | head -3)"
fi
return 1
fi
local tunnel_id
tunnel_id=$(echo "$response" | jq -r '.result.id // empty' 2>/dev/null || echo "")
if [[ -z "$tunnel_id" ]] || [[ "$tunnel_id" == "null" ]] || [[ "$tunnel_id" == "" ]]; then
log_error "Failed to create tunnel"
local errors
errors=$(echo "$response" | jq -r '.errors[]?.message // .error // "Unknown error"' 2>/dev/null | head -3)
if [[ -n "$errors" ]]; then
log_error "Errors: $errors"
fi
return 1
fi
log_success "Tunnel created: $tunnel_id"
echo "$tunnel_id"
}
# Get tunnel token (generate new token)
get_tunnel_token() {
local account_id="$1"
local tunnel_id="$2"
log_info "Generating tunnel token..."
# Note: Cloudflare API doesn't allow "getting" existing tokens, only generating new ones
# The endpoint is: POST /accounts/{account_id}/cfd_tunnel/{tunnel_id}/token
local response
response=$(cf_api_request "POST" "/accounts/${account_id}/cfd_tunnel/${tunnel_id}/token" 2>/dev/null) || response=""
if [[ -z "$response" ]]; then
log_warn "Could not generate token via API (may need manual generation)"
return 1
fi
local token
token=$(echo "$response" | jq -r '.result.token // empty' 2>/dev/null || echo "")
if [[ -z "$token" ]] || [[ "$token" == "null" ]]; then
log_warn "Token generation returned empty result"
return 1
fi
log_success "Token generated"
echo "$token"
}
# Configure tunnel routes
configure_tunnel_routes() {
local account_id="$1"
local tunnel_id="$2"
local hostname="$3"
local service="$4"
log_info "Configuring tunnel route: $hostname$service"
# Get existing config (may not exist for new tunnels)
local response
response=$(cf_api_request "GET" "/accounts/${account_id}/cfd_tunnel/${tunnel_id}/configurations" 2>/dev/null) || response=""
local existing_config="{}"
local ingress="[]"
# Check if response is valid and has config
if [[ -n "$response" ]] && echo "$response" | jq -e '.result.config' >/dev/null 2>&1; then
existing_config=$(echo "$response" | jq -r '.result.config // {}' 2>/dev/null || echo "{}")
ingress=$(echo "$existing_config" | jq -r '.ingress // []' 2>/dev/null || echo "[]")
# Check if route already exists
local route_exists
route_exists=$(echo "$ingress" | jq -r "any(.hostname == \"${hostname}\")" 2>/dev/null || echo "false")
if [[ "$route_exists" == "true" ]]; then
log_success "Route already configured for $hostname, skipping..."
return 0
fi
log_info "Found existing config, adding new route..."
else
log_info "No existing config found, creating new configuration..."
ingress="[]"
fi
# Build new ingress array - simple approach: replace entire config with just this route + catch-all
# This is simpler and more reliable than trying to merge with existing config
local config_data
config_data=$(jq -n \
--arg hostname "$hostname" \
--arg service "$service" \
'{
config: {
ingress: [
{
hostname: $hostname,
service: $service,
originRequest: {
noHappyEyeballs: true,
connectTimeout: "30s",
tcpKeepAlive: "30s",
keepAliveConnections: 100,
keepAliveTimeout: "90s",
disableChunkedEncoding: true,
noTLSVerify: true
}
},
{
service: "http_status:404"
}
]
}
}' 2>/dev/null)
if [[ -z "$config_data" ]]; then
log_error "Failed to create config JSON"
return 1
fi
response=$(cf_api_request "PUT" "/accounts/${account_id}/cfd_tunnel/${tunnel_id}/configurations" "$config_data" 2>/dev/null)
local api_result=$?
if [[ $api_result -eq 0 ]] && echo "$response" | jq -e '.success' >/dev/null 2>&1; then
log_success "Tunnel route configured"
return 0
else
log_error "Failed to configure tunnel route"
if [[ -n "$response" ]]; then
local errors
errors=$(echo "$response" | jq -r '.errors[]?.message // .error // "Unknown error"' 2>/dev/null | head -3)
log_error "Errors: $errors"
fi
return 1
fi
}
# Create DNS record
create_dns_record() {
local zone_id="$1"
local name="$2"
local target="$3"
log_info "Creating DNS record: $name$target"
# Check if record exists
local response=$(cf_api_request "GET" "/zones/${zone_id}/dns_records?name=${name}&type=CNAME" 2>/dev/null || echo "")
local record_id=$(echo "$response" | jq -r '.result[0].id // empty' 2>/dev/null || echo "")
local data=$(jq -n \
--arg name "$name" \
--arg target "$target" \
'{
type: "CNAME",
name: $name,
content: $target,
proxied: true,
ttl: 1
}')
if [[ -n "$record_id" ]]; then
log_warn "DNS record exists, updating..."
response=$(cf_api_request "PUT" "/zones/${zone_id}/dns_records/${record_id}" "$data")
else
response=$(cf_api_request "POST" "/zones/${zone_id}/dns_records" "$data")
fi
if echo "$response" | jq -e '.success' >/dev/null 2>&1; then
log_success "DNS record configured"
return 0
else
log_error "Failed to configure DNS record"
return 1
fi
}
# Create Cloudflare Access application
create_access_application() {
local account_id="$1"
local app_name="$2"
local domain="$3"
log_info "Creating Access application: $app_name"
# Check if application exists
local response=$(cf_api_request "GET" "/accounts/${account_id}/access/apps" 2>/dev/null || echo "")
local existing_id=$(echo "$response" | jq -r ".result[] | select(.domain == \"${domain}\") | .id" 2>/dev/null || echo "")
if [[ -n "$existing_id" ]]; then
log_warn "Access application already exists (ID: $existing_id)"
echo "$existing_id"
return 0
fi
# Create application
local data=$(jq -n \
--arg name "$app_name" \
--arg domain "$domain" \
'{
name: $name,
domain: $domain,
type: "self_hosted",
session_duration: "8h"
}')
response=$(cf_api_request "POST" "/accounts/${account_id}/access/apps" "$data")
local app_id=$(echo "$response" | jq -r '.result.id // empty')
if [[ -z "$app_id" ]]; then
log_error "Failed to create Access application"
return 1
fi
log_success "Access application created: $app_id"
echo "$app_id"
}
# Create Access policy
create_access_policy() {
local account_id="$1"
local app_id="$2"
log_info "Creating Access policy for application..."
# Check if policy exists
local response=$(cf_api_request "GET" "/accounts/${account_id}/access/apps/${app_id}/policies" 2>/dev/null || echo "")
local existing_id=$(echo "$response" | jq -r '.result[0].id // empty' 2>/dev/null || echo "")
if [[ -n "$existing_id" ]]; then
log_warn "Access policy already exists"
return 0
fi
# Create policy with MFA requirement
local data=$(jq -n \
'{
name: "Allow Team Access",
decision: "allow",
include: [
{
email: {
email: "@'${DOMAIN}'"
}
}
],
require: [
{
email: {}
}
]
}')
response=$(cf_api_request "POST" "/accounts/${account_id}/access/apps/${app_id}/policies" "$data")
if echo "$response" | jq -e '.success' >/dev/null 2>&1; then
log_success "Access policy created"
return 0
else
log_warn "Failed to create Access policy (may need manual configuration)"
return 1
fi
}
# Main execution
main() {
log_info "=========================================="
log_info " Cloudflare Automated Setup"
log_info "=========================================="
echo ""
# Validate credentials
if [[ -z "$CLOUDFLARE_API_TOKEN" ]] && [[ -z "$CLOUDFLARE_API_KEY" ]]; then
log_error "Cloudflare API credentials not found in .env"
exit 1
fi
# Get IDs
local zone_id=$(get_zone_id)
local account_id=$(get_account_id)
echo ""
log_info "Configuration:"
echo " Domain: $DOMAIN"
echo " Zone ID: $zone_id"
echo " Account ID: $account_id"
echo ""
# Tunnel configuration
declare -A TUNNELS=(
["ml110"]="ml110-01.d-bis.org:https://${PROXMOX_HOST_ML110}:8006"
["r630-01"]="r630-01.d-bis.org:https://${PROXMOX_HOST_R630_01}:8006"
["r630-02"]="r630-02.d-bis.org:https://${PROXMOX_HOST_R630_02}:8006"
)
echo "=========================================="
log_info "Step 1: Creating Tunnels"
echo "=========================================="
declare -A TUNNEL_IDS=()
declare -A TUNNEL_TOKENS=()
for tunnel_name in "${!TUNNELS[@]}"; do
local full_name="tunnel-${tunnel_name}"
# First, try to get existing tunnel
local response
response=$(cf_api_request "GET" "/accounts/${account_id}/cfd_tunnel" 2>/dev/null) || response=""
local tunnel_id=""
if [[ -n "$response" ]] && echo "$response" | jq -e '.result' >/dev/null 2>&1; then
tunnel_id=$(echo "$response" | jq -r ".result[]? | select(.name == \"${full_name}\") | .id" 2>/dev/null || echo "")
fi
# If not found, try to create
if [[ -z "$tunnel_id" ]] || [[ "$tunnel_id" == "null" ]] || [[ "$tunnel_id" == "" ]]; then
log_info "Tunnel $full_name not found, creating..."
tunnel_id=$(create_tunnel "$account_id" "$full_name")
else
log_success "Using existing tunnel: $full_name (ID: $tunnel_id)"
fi
if [[ -z "$tunnel_id" ]] || [[ "$tunnel_id" == "null" ]] || [[ "$tunnel_id" == "" ]]; then
log_error "Failed to get or create tunnel $full_name"
continue
fi
TUNNEL_IDS["$tunnel_name"]="$tunnel_id"
# Get token (optional - can be generated later via cloudflared CLI)
log_info "Attempting to generate token for tunnel $full_name..."
local token
token=$(get_tunnel_token "$account_id" "$tunnel_id" 2>/dev/null) || token=""
if [[ -z "$token" ]] || [[ "$token" == "null" ]]; then
log_warn "Could not generate token for $full_name via API"
log_info "Token can be generated later via: cloudflared tunnel token <tunnel-id>"
log_info "Or use cloudflared CLI: cloudflared tunnel create $full_name"
token=""
else
log_success "Token generated for $full_name"
fi
TUNNEL_TOKENS["$tunnel_name"]="$token"
echo ""
done
echo "=========================================="
log_info "Step 2: Configuring Tunnel Routes"
echo "=========================================="
for tunnel_name in "${!TUNNELS[@]}"; do
local config="${TUNNELS[$tunnel_name]}"
local hostname="${config%%:*}"
local service="${config#*:}"
local tunnel_id="${TUNNEL_IDS[$tunnel_name]}"
configure_tunnel_routes "$account_id" "$tunnel_id" "$hostname" "$service"
echo ""
done
echo "=========================================="
log_info "Step 3: Creating DNS Records"
echo "=========================================="
for tunnel_name in "${!TUNNELS[@]}"; do
local config="${TUNNELS[$tunnel_name]}"
local hostname="${config%%:*}"
local tunnel_id="${TUNNEL_IDS[$tunnel_name]}"
local target="${tunnel_id}.cfargotunnel.com"
create_dns_record "$zone_id" "$hostname" "$target"
echo ""
done
echo "=========================================="
log_info "Step 4: Creating Cloudflare Access Applications"
echo "=========================================="
for tunnel_name in "${!TUNNELS[@]}"; do
local config="${TUNNELS[$tunnel_name]}"
local hostname="${config%%:*}"
local app_name="Proxmox ${tunnel_name}"
local app_id=$(create_access_application "$account_id" "$app_name" "$hostname")
# Create policy
create_access_policy "$account_id" "$app_id"
echo ""
done
echo "=========================================="
log_success "Setup Complete!"
echo "=========================================="
echo ""
log_info "Tunnel IDs:"
for tunnel_name in "${!TUNNEL_IDS[@]}"; do
echo " $tunnel_name:"
echo " ID: ${TUNNEL_IDS[$tunnel_name]}"
if [[ -n "${TUNNEL_TOKENS[$tunnel_name]}" ]] && [[ "${TUNNEL_TOKENS[$tunnel_name]}" != "" ]]; then
echo " Token: ${TUNNEL_TOKENS[$tunnel_name]:0:50}..."
else
echo " Token: (not generated - use cloudflared CLI to generate)"
fi
done
# Save credentials to file for easy access
local creds_file="$TUNNELS_DIR/tunnel-credentials.json"
log_info ""
log_info "Saving credentials to: $creds_file"
local json_output="{"
local first=true
for tunnel_name in "${!TUNNEL_IDS[@]}"; do
if [[ "$first" == "true" ]]; then
first=false
else
json_output+=","
fi
json_output+="\"$tunnel_name\":{"
json_output+="\"id\":\"${TUNNEL_IDS[$tunnel_name]}\","
json_output+="\"token\":\"${TUNNEL_TOKENS[$tunnel_name]}\""
json_output+="}"
done
json_output+="}"
echo "$json_output" | jq . > "$creds_file"
chmod 600 "$creds_file"
log_success "Credentials saved to $creds_file"
echo ""
log_info "Next steps:"
echo " 1. Credentials saved to: $creds_file"
echo " 2. Run: ./scripts/save-credentials-from-file.sh"
echo " 3. Or manually: ./scripts/save-tunnel-credentials.sh <name> <id> <token>"
echo " 4. Start services: systemctl start cloudflared-*"
echo " 5. Test access: curl -I https://ml110-01.d-bis.org"
}
main