email-amazon/basic_setup/cloudflareMigrationDns.sh

329 lines
14 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 (imap/smtp/pop/webmail) + 10 (SRV) überspringen
# Nutzen: Client-Subdomains bleiben beim alten Provider
# SKIP_DMARC=true → Abschnitt 7 (DMARC) überspringen
# Nutzen: Bestehenden DMARC-Record nicht anfassen
#
# Typischer Migrations-Ablauf:
# Phase 0 (Vorbereitung): SKIP_CLIENT_DNS=true SKIP_DMARC=true → nur SES + SPF
# Phase 1 (MX Cutover): MX umstellen (manuell)
# Phase 2 (Client Switch): ohne SKIP Flags → alle 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_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: Client-Subdomains (imap/smtp/pop/webmail/SRV)"
[ "$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 (nur Info, wird nicht geändert)
# ------------------------------------------------------------------
echo ""
echo "--- 6. Root Domain MX (nur Info, wird nicht geändert) ---"
CURRENT_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 // "keiner"')
echo " MX vorhanden: $CURRENT_MX (wird nicht geändert)"
# ------------------------------------------------------------------
# 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 " 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
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
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_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 "============================================================"