Files
CoM-ghost_protocol/phantom/playbooks/common/templates/setup-tunnel.sh.j2
T
n0mad1k 1eff254a66 Fix tunnel script overwriting credentials for existing tunnels
When an existing Cloudflare tunnel was found, the script would overwrite
the credentials file with an empty TunnelSecret, breaking the service.
Now validates existing credentials and only recreates the tunnel if the
credentials file is missing or invalid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 13:37:20 -04:00

322 lines
12 KiB
Django/Jinja

#!/usr/bin/env bash
# Phantom — Cloudflare Tunnel Setup
# Deployed by phantom to {{ ansible_host | default('this server') }}
# Run this script to expose {{ _tls_domain }} via Cloudflare Tunnel.
# Requires: a Cloudflare account managing the domain's DNS zone.
#
# Usage:
# ./setup-tunnel.sh # Interactive — prompts for API token
# ./setup-tunnel.sh <CF_API_TOKEN> # Non-interactive — token as argument
set -euo pipefail
DOMAIN="{{ _tls_domain }}"
TUNNEL_NAME="phantom-${DOMAIN%%.*}"
echo "============================================"
echo " Phantom — Cloudflare Tunnel Setup"
echo "============================================"
echo " Domain: ${DOMAIN}"
echo " Tunnel Name: ${TUNNEL_NAME}"
echo ""
# ── Get Cloudflare API token ──────────────────────────────────────────
CF_TOKEN="${1:-}"
if [ -z "${CF_TOKEN}" ]; then
echo "[*] Cloudflare API token required."
echo " Create one at: https://dash.cloudflare.com/profile/api-tokens"
echo " Permissions needed: Zone:DNS:Edit + Account:Cloudflare Tunnel:Edit"
echo ""
read -rp " Cloudflare API token: " CF_TOKEN
echo ""
fi
if [ -z "${CF_TOKEN}" ]; then
echo "[-] No token provided."
exit 1
fi
# ── Install cloudflared ───────────────────────────────────────────────
if command -v cloudflared &>/dev/null; then
echo "[+] cloudflared already installed: $(cloudflared --version 2>&1 | head -1)"
else
echo "[*] Installing cloudflared..."
if [ -f /usr/share/keyrings/cloudflare-main.gpg ]; then
echo " Cloudflare apt repo already configured"
else
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" \
| tee /etc/apt/sources.list.d/cloudflared.list >/dev/null
apt-get update -qq
fi
apt-get install -y -qq cloudflared
echo "[+] cloudflared installed"
fi
# ── Resolve Cloudflare Account ID ─────────────────────────────────────
echo "[*] Verifying API token..."
ACCOUNT_INFO=$(curl -sf -X GET "https://api.cloudflare.com/client/v4/accounts?page=1&per_page=1" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" 2>/dev/null || true)
ACCOUNT_ID=$(echo "${ACCOUNT_INFO}" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if data.get('success') and data.get('result'):
print(data['result'][0]['id'])
except: pass
" 2>/dev/null || true)
if [ -z "${ACCOUNT_ID}" ]; then
echo "[-] Failed to verify API token. Check permissions:"
echo " - Account:Cloudflare Tunnel:Edit"
echo " - Zone:DNS:Edit"
exit 1
fi
echo "[+] Account verified: ${ACCOUNT_ID}"
# ── Create or find existing tunnel via API ────────────────────────────
echo "[*] Checking for existing tunnel '${TUNNEL_NAME}'..."
TUNNEL_LIST=$(curl -sf -X GET \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel?name=${TUNNEL_NAME}&is_deleted=false" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" 2>/dev/null || true)
TUNNEL_ID=$(echo "${TUNNEL_LIST}" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if data.get('success') and data.get('result'):
print(data['result'][0]['id'])
except: pass
" 2>/dev/null || true)
CREATED_NEW_TUNNEL=false
if [ -n "${TUNNEL_ID}" ]; then
echo "[+] Tunnel '${TUNNEL_NAME}' already exists: ${TUNNEL_ID}"
# Check if we have valid credentials on disk
CRED_FILE="/etc/cloudflared/${TUNNEL_ID}.json"
if [ -f "${CRED_FILE}" ]; then
EXISTING_SECRET=$(python3 -c "
import json, sys
try:
with open('${CRED_FILE}') as f:
d = json.load(f)
s = d.get('TunnelSecret', '')
if s: print(s)
except: pass
" 2>/dev/null || true)
if [ -n "${EXISTING_SECRET}" ]; then
echo "[+] Existing credentials file is valid"
else
echo "[-] Credentials file exists but has empty secret — tunnel must be recreated"
echo "[*] Deleting tunnel '${TUNNEL_NAME}' to recreate with new credentials..."
curl -sf -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" >/dev/null 2>&1 || true
TUNNEL_ID=""
fi
else
echo "[-] No credentials file found — tunnel must be recreated"
echo "[*] Deleting tunnel '${TUNNEL_NAME}' to recreate with new credentials..."
curl -sf -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" >/dev/null 2>&1 || true
TUNNEL_ID=""
fi
fi
if [ -z "${TUNNEL_ID}" ]; then
echo "[*] Creating tunnel '${TUNNEL_NAME}'..."
TUNNEL_SECRET=$(python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())")
CREATE_RESP=$(curl -sf -X POST \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"name\": \"${TUNNEL_NAME}\", \"tunnel_secret\": \"${TUNNEL_SECRET}\", \"config_src\": \"local\"}" \
2>/dev/null || true)
TUNNEL_ID=$(echo "${CREATE_RESP}" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if data.get('success') and data.get('result'):
print(data['result']['id'])
else:
errors = data.get('errors', [])
if errors:
print('ERROR:' + errors[0].get('message', 'unknown'), file=sys.stderr)
except: pass
" 2>/dev/null || true)
if [ -z "${TUNNEL_ID}" ] || [[ "${TUNNEL_ID}" == ERROR:* ]]; then
echo "[-] Failed to create tunnel."
echo "${CREATE_RESP}" | python3 -m json.tool 2>/dev/null || echo "${CREATE_RESP}"
exit 1
fi
echo "[+] Tunnel created: ${TUNNEL_ID}"
CREATED_NEW_TUNNEL=true
fi
# ── Write credentials + config ────────────────────────────────────────
echo "[*] Writing tunnel configuration..."
mkdir -p /etc/cloudflared
# Write credentials file only for newly created tunnels
if [ "${CREATED_NEW_TUNNEL}" = true ]; then
python3 -c "
import json
creds = {'AccountTag': '${ACCOUNT_ID}', 'TunnelID': '${TUNNEL_ID}', 'TunnelSecret': '${TUNNEL_SECRET}'}
with open('/etc/cloudflared/${TUNNEL_ID}.json', 'w') as f:
json.dump(creds, f)
"
chmod 600 "/etc/cloudflared/${TUNNEL_ID}.json"
echo "[+] Credentials file written"
fi
cat > /etc/cloudflared/config.yml << CFEOF
tunnel: ${TUNNEL_ID}
credentials-file: /etc/cloudflared/${TUNNEL_ID}.json
ingress:
- hostname: ${DOMAIN}
service: https://localhost:443
originRequest:
noTLSVerify: true
- service: http_status:404
CFEOF
chmod 600 /etc/cloudflared/config.yml
echo "[+] Config written"
# ── Route DNS ─────────────────────────────────────────────────────────
echo "[*] Setting up DNS route for ${DOMAIN}..."
# Get zone ID for the domain
BASE_DOMAIN=$(echo "${DOMAIN}" | rev | cut -d. -f1-2 | rev)
ZONE_INFO=$(curl -sf -X GET \
"https://api.cloudflare.com/client/v4/zones?name=${BASE_DOMAIN}" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" 2>/dev/null || true)
ZONE_ID=$(echo "${ZONE_INFO}" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if data.get('success') and data.get('result'):
print(data['result'][0]['id'])
except: pass
" 2>/dev/null || true)
if [ -z "${ZONE_ID}" ]; then
echo "[-] Could not find zone for ${BASE_DOMAIN}"
echo " Add the CNAME manually: ${DOMAIN} → ${TUNNEL_ID}.cfargotunnel.com"
else
# Check for existing record
EXISTING_RECORD=$(curl -sf -X GET \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${DOMAIN}" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" 2>/dev/null || true)
RECORD_ID=$(echo "${EXISTING_RECORD}" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if data.get('success') and data.get('result'):
print(data['result'][0]['id'])
except: pass
" 2>/dev/null || true)
TUNNEL_CNAME="${TUNNEL_ID}.cfargotunnel.com"
if [ -n "${RECORD_ID}" ]; then
# Update existing record to point to tunnel
echo " Updating existing DNS record..."
curl -sf -X PUT \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"${DOMAIN}\",\"content\":\"${TUNNEL_CNAME}\",\"proxied\":true}" \
>/dev/null 2>&1
echo "[+] DNS record updated: ${DOMAIN} → tunnel (proxied)"
else
# Create new CNAME
curl -sf -X POST \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"${DOMAIN}\",\"content\":\"${TUNNEL_CNAME}\",\"proxied\":true}" \
>/dev/null 2>&1
echo "[+] DNS CNAME created: ${DOMAIN} → tunnel (proxied)"
fi
fi
# ── Install systemd service ──────────────────────────────────────────
echo "[*] Installing cloudflared systemd service..."
# Stop existing service if running
systemctl stop cloudflared 2>/dev/null || true
# Try cloudflared's built-in installer first
if ! cloudflared service install 2>/dev/null; then
# Fallback: write unit file manually
if [ ! -f /etc/systemd/system/cloudflared.service ]; then
cat > /etc/systemd/system/cloudflared.service << SVCEOF
[Unit]
Description=cloudflared tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
TimeoutStartSec=0
ExecStart=/usr/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
SVCEOF
fi
fi
systemctl daemon-reload
systemctl enable cloudflared
systemctl start cloudflared
echo "[+] cloudflared service started"
# ── Verify ────────────────────────────────────────────────────────────
echo ""
echo "[*] Waiting for tunnel to connect..."
sleep 5
if systemctl is-active --quiet cloudflared; then
echo "[+] cloudflared service is running"
else
echo "[-] cloudflared service failed to start"
echo " Check: journalctl -u cloudflared --no-pager -n 20"
exit 1
fi
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://${DOMAIN}/" --max-time 10 2>/dev/null || echo "000")
if [ "${HTTP_CODE}" != "000" ] && [ "${HTTP_CODE}" != "502" ] && [ "${HTTP_CODE}" != "503" ]; then
echo "[+] Tunnel verified — https://${DOMAIN}/ returned HTTP ${HTTP_CODE}"
else
echo "[!] Tunnel may need a moment to propagate."
echo " DNS changes can take 1-5 minutes."
echo " Test: curl -I https://${DOMAIN}/"
fi
echo ""
echo "============================================"
echo " Cloudflare Tunnel setup complete!"
echo ""
echo " Domain: https://${DOMAIN}"
echo " Tunnel: ${TUNNEL_NAME} (${TUNNEL_ID})"
echo " Service: systemctl status cloudflared"
echo ""
echo " To remove: cloudflared tunnel delete ${TUNNEL_NAME}"
echo "============================================"