#!/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. 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=" # 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=$(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 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 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 | startswith("v=spf1"))][0].content // ""') if [ -n "$CURRENT_SPF" ]; then # 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/') # Anführungszeichen entfernen falls vorhanden NEW_SPF=$(echo "$NEW_SPF" | tr -d '"') 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. 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. 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 # ------------------------------------------------------------------ # SCHRITT 10: SRV Records # ------------------------------------------------------------------ echo "" echo "--- 10. SRV Records ---" if [ "$SKIP_CLIENT_DNS" = "true" ]; then echo " ⏭️ Übersprungen (SKIP_CLIENT_DNS=true)" else ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 mail.$DOMAIN_NAME" false ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 mail.$DOMAIN_NAME" false ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 mail.$DOMAIN_NAME" false fi 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 "============================================================"