Files
email-amazon/basic_setup/cloudflareMigrationDns.sh
2026-05-20 17:52:35 -05:00

361 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# cloudflareMigrationDns.sh
# Setzt DNS Records für Amazon SES Migration + Cloudflare
# Unterstützt: DKIM, SPF (Merge), DMARC, MX, Autodiscover
# Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang
#
# MIGRATIONS-FLAGS:
# SKIP_CLIENT_DNS=true → Abschnitt 8 (mail/imap/smtp/pop/webmail) überspringen
# Nutzen: Client-Subdomains bleiben komplett beim alten Provider
# SKIP_PROTOCOL_DNS=true → nur imap/smtp/pop überspringen, aber mail + webmail setzen
# Nutzen: Webmail/Autodiscover vorbereiten, Mailclients bleiben beim alten Provider
# SKIP_DMARC=true → Abschnitt 7 (DMARC) überspringen
# Nutzen: Bestehenden DMARC-Record nicht anfassen
#
# Typischer Migrations-Ablauf:
# Phase 0a (Vorbereitung, Client-Records bleiben alt):
# SKIP_CLIENT_DNS=true SKIP_DMARC=true → nur SES + SPF/DKIM/MailFrom
# Phase 0b (Webmail vorbereiten, imap/smtp/pop bleiben alt):
# SKIP_CLIENT_DNS=false SKIP_PROTOCOL_DNS=true SKIP_DMARC=false
# Phase 1 (MX Cutover):
# MX manuell umstellen
# Phase 2 (Client Switch):
# SKIP_CLIENT_DNS=false SKIP_PROTOCOL_DNS=false → alle Client-Records setzen
set -e
# --- KONFIGURATION ---
AWS_REGION=${AWS_REGION:-"us-east-2"}
DRY_RUN=${DRY_RUN:-"false"}
# Migrations-Flags (NEU)
SKIP_CLIENT_DNS=${SKIP_CLIENT_DNS:-"false"}
SKIP_PROTOCOL_DNS=${SKIP_PROTOCOL_DNS:-"false"}
SKIP_DMARC=${SKIP_DMARC:-"false"}
# IP des Mailservers - PFLICHT wenn keine CNAME-Kette gewünscht
MAIL_SERVER_IP=${MAIL_SERVER_IP:-""}
# Ziel-Server für Mailclients. Standard: mail.<kundendomain>
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 " Setze: export MAIL_SERVER_IP=<deine-server-ip>"
# Kein exit - Abschnitt 8 wird ggf. übersprungen
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!"
[ "$SKIP_CLIENT_DNS" = "true" ] && echo " ⏭️ SKIP: alle Client-Subdomains (mail/imap/smtp/pop/webmail)"
[ "$SKIP_PROTOCOL_DNS" = "true" ] && echo " ⏭️ SKIP: Protokoll-Subdomains imap/smtp/pop bleiben unverändert"
[ "$SKIP_DMARC" = "true" ] && echo " ⏭️ SKIP: DMARC Record"
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
# Prüft Existenz -> Create oder Update (je nach Typ)
# ------------------------------------------------------------------
ensure_record() {
local type=$1
local name=$2
local content=$3
local proxied=${4:-false}
local priority=$5 # Optional für MX
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
local rec_content
if [ "$type" == "TXT" ] && [ "$name" == "$DOMAIN_NAME" ] && [[ "$content" == v=spf1* ]]; then
# Spezialfall Root-Domain SPF: Filtere gezielt den SPF-Eintrag heraus,
# damit z.B. Google Site Verification nicht überschrieben wird.
rec_id=$(echo "$search_res" | jq -r '.result[] | select(.content | contains("v=spf1")) | .id' | head -n 1)
rec_content=$(echo "$search_res" | jq -r '.result[] | select(.content | contains("v=spf1")) | .content' | head -n 1)
else
# Standardverhalten für alle anderen (A, CNAME, MX, etc.)
rec_id=$(echo "$search_res" | jq -r '.result[0].id')
rec_content=$(echo "$search_res" | jq -r '.result[0].content')
fi
# Fallback für jq, damit das restliche Skript funktioniert
[ -z "$rec_id" ] && rec_id="null"
[ -z "$rec_content" ] && rec_content="null"
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 beim Erstellen: $(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 " ⛔ Root-MX existiert aber ist anders: $rec_content"
echo " → Wird NICHT automatisch geändert (Migrations-Schutz)"
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 beim Updaten: $(echo $res | jq -r '.errors[0].message')"
fi
fi
fi
fi
}
# ------------------------------------------------------------------
# SCHRITT 1: MAIL FROM Domain (aus SES lesen)
# ------------------------------------------------------------------
echo ""
echo "--- 1. MAIL FROM Domain ---"
MAIL_FROM_DOMAIN=$(aws sesv2 get-email-identity \
--email-identity "$DOMAIN_NAME" \
--region "$AWS_REGION" \
--query 'MailFromAttributes.MailFromDomain' \
--output text 2>/dev/null || echo "NONE")
if [ "$MAIL_FROM_DOMAIN" == "NONE" ] || [ "$MAIL_FROM_DOMAIN" == "None" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then
echo " Keine MAIL FROM Domain in SES konfiguriert."
echo " → Überspringe MAIL FROM DNS Setup."
MAIL_FROM_DOMAIN=""
fi
# ------------------------------------------------------------------
# SCHRITT 2: DKIM Records
# ------------------------------------------------------------------
echo ""
echo "--- 2. DKIM Records ---"
DKIM_TOKENS=$(aws sesv2 get-email-identity \
--email-identity "$DOMAIN_NAME" \
--region "$AWS_REGION" \
--query 'DkimAttributes.Tokens' \
--output text 2>/dev/null || echo "")
if [ -n "$DKIM_TOKENS" ] && [ "$DKIM_TOKENS" != "None" ]; then
for TOKEN in $DKIM_TOKENS; do
ensure_record "CNAME" "${TOKEN}._domainkey.${DOMAIN_NAME}" "${TOKEN}.dkim.amazonses.com" false
done
else
echo " ⚠️ Keine DKIM Tokens gefunden. SES Identity angelegt?"
fi
# ------------------------------------------------------------------
# SCHRITT 3: SES Verification TXT
# ------------------------------------------------------------------
echo ""
echo "--- 3. SES Verification TXT ---"
VERIFICATION_TOKEN=$(aws ses get-identity-verification-attributes \
--identities "$DOMAIN_NAME" \
--region "$AWS_REGION" \
--query "VerificationAttributes.\"${DOMAIN_NAME}\".VerificationToken" \
--output text 2>/dev/null || echo "")
if [ -n "$VERIFICATION_TOKEN" ] && [ "$VERIFICATION_TOKEN" != "None" ]; then
ensure_record "TXT" "_amazonses.${DOMAIN_NAME}" "$VERIFICATION_TOKEN" false
else
echo " ⚠️ Kein Verification Token. SES Identity angelegt?"
fi
# ------------------------------------------------------------------
# SCHRITT 4: MAIL FROM Subdomain (MX + SPF)
# ------------------------------------------------------------------
echo ""
echo "--- 4. MAIL FROM Subdomain (${MAIL_FROM_DOMAIN:-'nicht konfiguriert'}) ---"
if [ -n "$MAIL_FROM_DOMAIN" ]; then
# Prüfe ob CNAME-Konflikt auf der MAIL FROM Subdomain existiert
CNAME_CHECK=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=CNAME&name=$MAIL_FROM_DOMAIN" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].content')
if [ "$CNAME_CHECK" != "null" ] && [ -n "$CNAME_CHECK" ]; then
echo " ⛔ CNAME-Konflikt! $MAIL_FROM_DOMAIN hat CNAME → $CNAME_CHECK"
echo " MX + TXT können nicht neben CNAME existieren."
echo " → awsses.sh mit anderem MAIL_FROM_SUBDOMAIN erneut ausführen"
exit 1
fi
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
else
echo " Übersprungen (keine MAIL FROM Domain konfiguriert)."
fi
# ------------------------------------------------------------------
# SCHRITT 5: Root Domain SPF (Merge mit altem Provider)
# ------------------------------------------------------------------
echo ""
echo "--- 5. Root Domain SPF ---"
# Aktuellen SPF-Record lesen
# Cloudflare liefert TXT-Content manchmal mit Anführungszeichen,
# daher erst alle TXT-Records holen und dann filtern
CURRENT_SPF=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=TXT&name=$DOMAIN_NAME" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
| jq -r '[.result[] | select(.content | gsub("^\"|\"$";"") | startswith("v=spf1"))][0].content // ""')
# Anführungszeichen sofort entfernen
CURRENT_SPF=$(echo "$CURRENT_SPF" | tr -d '"')
if [ -n "$CURRENT_SPF" ]; then
echo " 📋 Aktueller SPF: $CURRENT_SPF"
# Prüfe ob amazonses.com schon drin ist
if echo "$CURRENT_SPF" | grep -q "include:amazonses.com"; then
echo " 🆗 SPF enthält bereits include:amazonses.com"
else
# amazonses.com einfügen direkt nach v=spf1
NEW_SPF=$(echo "$CURRENT_SPF" | sed 's/v=spf1 /v=spf1 include:amazonses.com /')
# ?all → ~all upgraden
NEW_SPF=$(echo "$NEW_SPF" | sed 's/?all/~all/')
echo " 📝 Neuer SPF: $NEW_SPF"
ensure_record "TXT" "$DOMAIN_NAME" "$NEW_SPF" false
fi
else
echo " Kein SPF Record vorhanden. Erstelle neuen."
ensure_record "TXT" "$DOMAIN_NAME" "v=spf1 include:amazonses.com ~all" false
fi
# ------------------------------------------------------------------
# SCHRITT 6: Root Domain MX
# ------------------------------------------------------------------
echo ""
echo "--- 6. Root Domain MX ---"
# Prüfen, ob bereits MX-Records für die Root-Domain existieren
MX_COUNT=$(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 | length')
if [ "$MX_COUNT" -eq 0 ]; then
echo " Kein MX-Record vorhanden. Setze initialen SES Inbound MX..."
ensure_record "MX" "$DOMAIN_NAME" "inbound-smtp.${AWS_REGION}.amazonaws.com" false 10
else
echo " ⛔ MX-Record(s) für @ bereits vorhanden ($MX_COUNT Eintrag/Einträge)."
echo " → Wird zum Schutz der alten Postfächer AUF KEINEN FALL automatisch geändert!"
echo " → Muss nach der Migration manuell umgestellt werden."
fi
# ------------------------------------------------------------------
# SCHRITT 7: DMARC
# ------------------------------------------------------------------
echo ""
echo "--- 7. DMARC ---"
if [ "$SKIP_DMARC" = "true" ]; then
echo " ⏭️ Übersprungen (SKIP_DMARC=true)"
echo " Bestehender DMARC-Record bleibt unverändert."
else
ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false
fi
# ------------------------------------------------------------------
# SCHRITT 8: Mailclient Subdomains (A + CNAME)
# ------------------------------------------------------------------
echo ""
echo "--- 8. Mailclient Subdomains (A + CNAME) ---"
if [ "$SKIP_CLIENT_DNS" = "true" ]; then
echo " ⏭️ Übersprungen (SKIP_CLIENT_DNS=true)"
echo " mail/imap/smtp/pop/webmail bleiben beim alten Provider."
echo " Setze SKIP_CLIENT_DNS=false nach MX-Cutover + Client-Umstellung."
else
if [ -n "$MAIL_SERVER_IP" ]; then
# A-Record für mail.<domain> direkt auf Server-IP
# Wichtig: mail muss DNS-only bleiben; Cloudflare Proxy funktioniert nicht für SMTP/IMAP/POP.
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
# Webmail kann bereits vorbereitet werden, auch wenn imap/smtp/pop noch beim alten Provider bleiben.
ensure_record "CNAME" "webmail.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
if [ "$SKIP_PROTOCOL_DNS" = "true" ]; then
echo " ⏭️ Überspringe imap/smtp/pop (SKIP_PROTOCOL_DNS=true)"
echo " imap/smtp/pop bleiben unverändert beim alten Provider."
else
# imap/smtp/pop → CNAME auf mail.<domain>
# Wichtig: diese Records müssen DNS-only bleiben; Cloudflare Proxy funktioniert nicht für Mail-Protokolle.
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
fi
fi
# ------------------------------------------------------------------
# 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"
if [ "$SKIP_CLIENT_DNS" = "true" ]; then
echo ""
echo " ⚠️ Client-Subdomains wurden NICHT geändert."
echo " Nach MX-Cutover + Worker-Validierung erneut ausführen mit:"
echo " SKIP_CLIENT_DNS=false SKIP_PROTOCOL_DNS=false SKIP_DMARC=false ./cloudflareMigrationDns.sh"
elif [ "$SKIP_PROTOCOL_DNS" = "true" ]; then
echo ""
echo " ⚠️ imap/smtp/pop wurden NICHT geändert."
echo " Für den finalen Client-Cutover erneut ausführen mit:"
echo " SKIP_CLIENT_DNS=false SKIP_PROTOCOL_DNS=false SKIP_DMARC=false ./cloudflareMigrationDns.sh"
fi
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 "============================================================"