38587699f9
- docker-compose.yml: tunnel de Cloudflare para exponer servicios sin abrir puertos en el router - add-domain.sh: script para añadir subdominios al tunnel y crear registros DNS en Cloudflare vía API - stack.env: plantilla con tokens y configuración del tunnel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
6.2 KiB
Bash
Executable File
158 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Registra subdominios en Cloudflare DNS y opcionalmente en el tunnel.
|
|
#
|
|
# Modos:
|
|
# ./add-domain.sh <sub> [sub2 ...] → A record directo (sin tunnel)
|
|
# ./add-domain.sh --tunnel <sub> [sub2 ...] → CNAME al tunnel + ingress
|
|
#
|
|
# Ejemplos:
|
|
# ./add-domain.sh immich → A 193.70.84.224, solo DNS
|
|
# ./add-domain.sh --tunnel immich obsidian → CNAME tunnel + ingress
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
|
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
echo "ERROR: No se encontró .env en $SCRIPT_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
set -a; source "$ENV_FILE"; set +a
|
|
|
|
# ── Modo ────────────────────────────────────────────────────────────────────
|
|
MODE="direct"
|
|
if [ "${1:-}" = "--tunnel" ]; then
|
|
MODE="tunnel"
|
|
shift
|
|
fi
|
|
|
|
if [ $# -eq 0 ]; then
|
|
echo "Uso: $0 [--tunnel] <subdominio> [subdominio2 ...]"
|
|
exit 1
|
|
fi
|
|
|
|
# ── Validar variables obligatorias ──────────────────────────────────────────
|
|
REQUIRED_VARS="CLOUDFLARE_API_TOKEN CLOUDFLARE_ZONE_ID CLOUDFLARE_DOMAIN"
|
|
[ "$MODE" = "tunnel" ] && REQUIRED_VARS="$REQUIRED_VARS CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_TUNNEL_ID"
|
|
|
|
for VAR in $REQUIRED_VARS; do
|
|
if [ -z "${!VAR:-}" ] || [[ "${!VAR}" == change-me* ]]; then
|
|
echo "ERROR: $VAR no está configurado en .env"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
SERVER_IP="${CLOUDFLARE_SERVER_IP:-}"
|
|
if [ "$MODE" = "direct" ] && [ -z "$SERVER_IP" ]; then
|
|
# Autodetectar IP pública del servidor
|
|
SERVER_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || true)
|
|
if [ -z "$SERVER_IP" ]; then
|
|
echo "ERROR: No se pudo detectar la IP del servidor. Define CLOUDFLARE_SERVER_IP en .env"
|
|
exit 1
|
|
fi
|
|
echo "→ IP del servidor autodetectada: $SERVER_IP"
|
|
fi
|
|
|
|
# ── Modo tunnel: obtener ingress actual ─────────────────────────────────────
|
|
TUNNEL_CNAME="${CLOUDFLARE_TUNNEL_ID:-}.cfargotunnel.com"
|
|
CURRENT=""
|
|
if [ "$MODE" = "tunnel" ]; then
|
|
echo "→ Obteniendo configuración actual del tunnel..."
|
|
CURRENT=$(curl -s \
|
|
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/cfd_tunnel/${CLOUDFLARE_TUNNEL_ID}/configurations" \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
|
|
if ! echo "$CURRENT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('success') else 1)" 2>/dev/null; then
|
|
echo "ERROR: No se pudo obtener la config del tunnel"
|
|
echo "$CURRENT"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ── Por cada subdominio ──────────────────────────────────────────────────────
|
|
for SUB in "$@"; do
|
|
FQDN="${SUB}.${CLOUDFLARE_DOMAIN}"
|
|
echo ""
|
|
echo "=== $FQDN ==="
|
|
|
|
# Tunnel: añadir al ingress
|
|
if [ "$MODE" = "tunnel" ]; then
|
|
CURRENT=$(echo "$CURRENT" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
ingress = data['result']['config']['ingress']
|
|
hostname = '${FQDN}'
|
|
if any(r.get('hostname') == hostname for r in ingress):
|
|
print(json.dumps(data), end='')
|
|
print(' tunnel: ya existe, sin cambios', file=sys.stderr)
|
|
sys.exit(0)
|
|
catch_all = ingress.pop()
|
|
ingress.append({'hostname': hostname, 'service': 'https://traefik:443', 'originRequest': {'noTLSVerify': True}})
|
|
ingress.append(catch_all)
|
|
data['result']['config']['ingress'] = ingress
|
|
print(json.dumps(data), end='')
|
|
print(' tunnel: añadido', file=sys.stderr)
|
|
")
|
|
DNS_PAYLOAD="{\"type\":\"CNAME\",\"name\":\"${SUB}\",\"content\":\"${TUNNEL_CNAME}\",\"ttl\":1,\"proxied\":true,\"comment\":\"Cloudflare Tunnel\"}"
|
|
DNS_TARGET="$TUNNEL_CNAME (CNAME proxied)"
|
|
else
|
|
DNS_PAYLOAD="{\"type\":\"A\",\"name\":\"${SUB}\",\"content\":\"${SERVER_IP}\",\"ttl\":1,\"proxied\":false,\"comment\":\"Direct\"}"
|
|
DNS_TARGET="$SERVER_IP (A record)"
|
|
fi
|
|
|
|
# DNS: crear o actualizar
|
|
EXISTING=$(curl -s \
|
|
"https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records?name=${FQDN}" \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
|
|
RECORD_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result'][0]['id'])" 2>/dev/null || true)
|
|
|
|
if [ -n "$RECORD_ID" ]; then
|
|
RESULT=$(curl -s -X PUT \
|
|
"https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records/${RECORD_ID}" \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$DNS_PAYLOAD")
|
|
MSG="actualizado"
|
|
else
|
|
RESULT=$(curl -s -X POST \
|
|
"https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records" \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$DNS_PAYLOAD")
|
|
MSG="creado"
|
|
fi
|
|
|
|
if echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('success') else 1)" 2>/dev/null; then
|
|
echo " DNS: $MSG → $DNS_TARGET"
|
|
else
|
|
echo " DNS ERROR:"
|
|
echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('errors'))"
|
|
fi
|
|
done
|
|
|
|
# ── Guardar ingress del tunnel ───────────────────────────────────────────────
|
|
if [ "$MODE" = "tunnel" ]; then
|
|
echo ""
|
|
echo "→ Guardando configuración del tunnel..."
|
|
NEW_CONFIG=$(echo "$CURRENT" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
print(json.dumps({'config': data['result']['config']}))
|
|
")
|
|
RESULT=$(curl -s -X PUT \
|
|
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/cfd_tunnel/${CLOUDFLARE_TUNNEL_ID}/configurations" \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$NEW_CONFIG")
|
|
if echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('success') else 1)" 2>/dev/null; then
|
|
echo " Tunnel ingress actualizado ✓"
|
|
else
|
|
echo " ERROR al actualizar tunnel:"
|
|
echo "$RESULT"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "Listo. DNS activo en ~30s."
|