238 lines
10 KiB
Bash
Executable File
238 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
||
# cloudflareMigrationDns.sh
|
||
# Setzt DNS Records für Amazon SES Migration + Cloudflare
|
||
# Unterstützt: DKIM, SPF (Merge), DMARC, MX, Autodiscover
|
||
# NEU: Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang
|
||
|
||
set -e
|
||
|
||
# --- KONFIGURATION ---
|
||
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
||
DRY_RUN=${DRY_RUN:-"false"}
|
||
|
||
# IP des Mailservers - PFLICHT wenn keine CNAME-Kette gewünscht
|
||
# export MAIL_SERVER_IP="1.2.3.4"
|
||
MAIL_SERVER_IP=${MAIL_SERVER_IP:-""}
|
||
|
||
# Ziel-Server für Mailclients. Standard: mail.<kundendomain>
|
||
# Wenn MAIL_SERVER_IP gesetzt ist, bekommt mail.<domain> einen A-Record
|
||
# und imap/smtp/pop/webmail zeigen per CNAME auf mail.<domain>
|
||
TARGET_MAIL_SERVER=${TARGET_MAIL_SERVER:-"mail.${DOMAIN_NAME}"}
|
||
|
||
# --- CHECKS ---
|
||
if [ -z "$DOMAIN_NAME" ]; then echo "❌ Fehler: DOMAIN_NAME fehlt."; exit 1; fi
|
||
if [ -z "$CF_API_TOKEN" ]; then echo "❌ Fehler: CF_API_TOKEN fehlt."; exit 1; fi
|
||
if ! command -v jq &> /dev/null; then echo "❌ Fehler: 'jq' fehlt."; exit 1; fi
|
||
if ! command -v aws &> /dev/null; then echo "❌ Fehler: 'aws' CLI fehlt."; exit 1; fi
|
||
|
||
if [ -z "$MAIL_SERVER_IP" ] && [ "$TARGET_MAIL_SERVER" == "mail.$DOMAIN_NAME" ]; then
|
||
echo "⚠️ WARNUNG: MAIL_SERVER_IP ist nicht gesetzt!"
|
||
echo " mail.$DOMAIN_NAME braucht einen A-Record."
|
||
echo " Bitte setzen: export MAIL_SERVER_IP=<IP deines Servers>"
|
||
exit 1
|
||
fi
|
||
|
||
echo "============================================================"
|
||
echo " 🛡️ DNS Migration Setup für: $DOMAIN_NAME"
|
||
echo " 🌍 Region: $AWS_REGION"
|
||
echo " 📬 Mail-Server Target: $TARGET_MAIL_SERVER"
|
||
[ -n "$MAIL_SERVER_IP" ] && echo " 🖥️ Server IP: $MAIL_SERVER_IP"
|
||
[ "$DRY_RUN" = "true" ] && echo " ⚠️ DRY RUN MODE - Keine Änderungen!"
|
||
echo "============================================================"
|
||
|
||
# 1. ZONE ID HOLEN
|
||
echo "🔍 Suche Cloudflare Zone ID..."
|
||
ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN_NAME" \
|
||
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].id')
|
||
|
||
if [ "$ZONE_ID" == "null" ] || [ -z "$ZONE_ID" ]; then
|
||
echo "❌ Zone nicht gefunden."
|
||
exit 1
|
||
fi
|
||
echo " ✅ Zone ID: $ZONE_ID"
|
||
|
||
# ------------------------------------------------------------------
|
||
# FUNKTION: ensure_record
|
||
# ------------------------------------------------------------------
|
||
ensure_record() {
|
||
local type=$1
|
||
local name=$2
|
||
local content=$3
|
||
local proxied=${4:-false}
|
||
local priority=$5
|
||
|
||
echo " ⚙️ Prüfe $type $name..."
|
||
|
||
local search_res=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=$type&name=$name" \
|
||
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json")
|
||
|
||
local rec_id=$(echo "$search_res" | jq -r '.result[0].id')
|
||
local rec_content=$(echo "$search_res" | jq -r '.result[0].content')
|
||
|
||
if [ "$type" == "MX" ]; then
|
||
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" --argjson prio "$priority" \
|
||
'{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p, priority: $prio}')
|
||
else
|
||
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" \
|
||
'{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p}')
|
||
fi
|
||
|
||
if [ "$rec_id" == "null" ]; then
|
||
if [ "$DRY_RUN" = "true" ]; then
|
||
echo " [DRY] Würde ERSTELLEN: $content"
|
||
else
|
||
res=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
|
||
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$json_data")
|
||
if [ "$(echo $res | jq -r .success)" == "true" ]; then
|
||
echo " ✅ Erstellt."
|
||
else
|
||
echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)"
|
||
fi
|
||
fi
|
||
else
|
||
if [ "$rec_content" == "$content" ]; then
|
||
echo " 🆗 Identisch. Überspringe."
|
||
else
|
||
if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then
|
||
echo " ⛔ MX existiert aber anders! Gefunden: $rec_content / Erwartet: $content"
|
||
echo " Bitte Record ID $rec_id manuell löschen."
|
||
return
|
||
fi
|
||
if [ "$DRY_RUN" = "true" ]; then
|
||
echo " [DRY] Würde UPDATEN: '$rec_content' → '$content'"
|
||
else
|
||
res=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$rec_id" \
|
||
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$json_data")
|
||
if [ "$(echo $res | jq -r .success)" == "true" ]; then
|
||
echo " 🔄 Aktualisiert."
|
||
else
|
||
echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 1: MAIL FROM ermitteln
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 1. MAIL FROM Domain ---"
|
||
if [ -z "$MAIL_FROM_DOMAIN" ]; then
|
||
SES_JSON=$(aws sesv2 get-email-identity --email-identity $DOMAIN_NAME --region $AWS_REGION 2>/dev/null)
|
||
MAIL_FROM_DOMAIN=$(echo "$SES_JSON" | jq -r '.MailFromAttributes.MailFromDomain')
|
||
if [ "$MAIL_FROM_DOMAIN" == "null" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then
|
||
MAIL_FROM_DOMAIN="mail.$DOMAIN_NAME"
|
||
echo " ⚠️ Kein MAIL FROM in SES. Fallback: $MAIL_FROM_DOMAIN"
|
||
fi
|
||
else
|
||
echo " Nutze: $MAIL_FROM_DOMAIN"
|
||
fi
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 2: DKIM Records
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 2. DKIM Records ---"
|
||
TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION \
|
||
--query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text)
|
||
for token in $TOKENS; do
|
||
ensure_record "CNAME" "${token}._domainkey.$DOMAIN_NAME" "${token}.dkim.amazonses.com" false
|
||
done
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 3: SES Verification
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 3. SES Verification TXT ---"
|
||
VERIF_TOKEN=$(aws ses get-identity-verification-attributes --identities $DOMAIN_NAME \
|
||
--region $AWS_REGION --query "VerificationAttributes.\"$DOMAIN_NAME\".VerificationToken" --output text)
|
||
if [ "$VERIF_TOKEN" != "None" ] && [ -n "$VERIF_TOKEN" ]; then
|
||
ensure_record "TXT" "_amazonses.$DOMAIN_NAME" "$VERIF_TOKEN" false
|
||
fi
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 4: MAIL FROM Subdomain (MX + SPF)
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 4. MAIL FROM Subdomain ($MAIL_FROM_DOMAIN) ---"
|
||
ensure_record "MX" "$MAIL_FROM_DOMAIN" "feedback-smtp.$AWS_REGION.amazonses.com" false 10
|
||
ensure_record "TXT" "$MAIL_FROM_DOMAIN" "v=spf1 include:amazonses.com ~all" false
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 5: Root Domain SPF
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 5. Root Domain SPF ---"
|
||
if [ -n "$OLD_PROVIDER_SPF" ]; then
|
||
FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all"
|
||
else
|
||
FINAL_SPF="v=spf1 include:amazonses.com ~all"
|
||
fi
|
||
ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 6: Root Domain MX
|
||
# ------------------------------------------------------------------
|
||
# WICHTIG: Der MX Record zeigt auf Amazon SES (inbound-smtp.*.amazonaws.com),
|
||
# da eingehende Mails über SES → S3 → SQS → Worker → DMS laufen.
|
||
# Der DMS ist NICHT direkt aus dem Internet erreichbar.
|
||
# Dieser Record wird daher NICHT angefasst.
|
||
echo ""
|
||
echo "--- 6. Root Domain MX (nur Info, wird nicht geändert) ---"
|
||
EXISTING_MX=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=MX&name=$DOMAIN_NAME" \
|
||
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].content')
|
||
if [ "$EXISTING_MX" == "null" ] || [ -z "$EXISTING_MX" ]; then
|
||
echo " ⚠️ Kein MX Record gefunden! Bitte manuell in SES/Cloudflare setzen:"
|
||
echo " inbound-smtp.$AWS_REGION.amazonaws.com (Prio 10)"
|
||
else
|
||
echo " ℹ️ MX vorhanden: $EXISTING_MX (wird nicht geändert)"
|
||
fi
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 7: DMARC
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 7. DMARC ---"
|
||
ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 8 (NEU): Mailclient Subdomains
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 8. Mailclient Subdomains (A + CNAME) ---"
|
||
|
||
if [ -n "$MAIL_SERVER_IP" ]; then
|
||
# A-Record für mail.<domain> direkt auf Server-IP
|
||
ensure_record "A" "mail.$DOMAIN_NAME" "$MAIL_SERVER_IP" false
|
||
else
|
||
# CNAME auf externen Ziel-Host (nur wenn verschieden)
|
||
if [ "$TARGET_MAIL_SERVER" != "mail.$DOMAIN_NAME" ]; then
|
||
ensure_record "CNAME" "mail.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false
|
||
fi
|
||
fi
|
||
|
||
# imap, smtp, pop, webmail → CNAME auf mail.<domain>
|
||
ensure_record "CNAME" "imap.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||
ensure_record "CNAME" "smtp.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||
ensure_record "CNAME" "pop.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||
ensure_record "CNAME" "webmail.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||
|
||
# ------------------------------------------------------------------
|
||
# SCHRITT 9: Autodiscover / Autoconfig
|
||
# ------------------------------------------------------------------
|
||
echo ""
|
||
echo "--- 9. Autodiscover / Autoconfig ---"
|
||
ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||
ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||
|
||
echo ""
|
||
echo "============================================================"
|
||
echo "✅ Fertig für Domain: $DOMAIN_NAME"
|
||
echo ""
|
||
echo " Mailclient-Konfiguration für Kunden:"
|
||
echo " IMAP: imap.$DOMAIN_NAME Port 993 (SSL)"
|
||
echo " SMTP: smtp.$DOMAIN_NAME Port 587 (STARTTLS) oder 465 (SSL)"
|
||
echo " POP3: pop.$DOMAIN_NAME Port 995 (SSL)"
|
||
echo " Webmail: webmail.$DOMAIN_NAME"
|
||
echo "============================================================" |