#!/bin/bash # cloudflareMigrationDns.sh # Setzt DNS Records für Amazon SES Migration + Cloudflare # Unterstützt: DKIM, SPF (Merge), DMARC, MX (Safety Check), Autodiscover set -e # --- KONFIGURATION --- AWS_REGION=${AWS_REGION:-"us-east-2"} DRY_RUN=${DRY_RUN:-"false"} # Ziel für Autodiscover/IMAP (wohin sollen Mail-Clients verbinden?) # Standard: mail.deinedomain.tld. Kann überschrieben werden. 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 echo "============================================================" echo " 🛡️ DNS Migration Setup für: $DOMAIN_NAME" echo " 🌍 Region: $AWS_REGION" [ "$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 # 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..." # Bestehenden Record suchen # Hinweis: Wir suchen exakt nach Name und Typ 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') # JSON Body bauen 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}') elif [ "$type" == "TXT" ]; then # Bei TXT Quotes escapen falls nötig, aber jq macht das meist gut 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}') 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 # LOGIK if [ "$rec_id" == "null" ]; then # --- CREATE --- 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 # --- EXISTS --- if [ "$rec_content" == "$content" ]; then echo " 🆗 Identisch vorhanden. Überspringe." else # Inhalt anders -> Update oder Error? if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then echo " ⛔ MX Record existiert aber ist anders!" echo " Gefunden: $rec_content" echo " Erwartet: $content" echo " ABBRUCH: Bitte alten MX Record ID $rec_id manuell löschen." # Wir brechen hier nicht das ganze Script ab, aber setzen den neuen nicht. return fi # Für TXT (SPF/DMARC) oder CNAME machen wir ein UPDATE (Overwrite) if [ "$DRY_RUN" = "true" ]; then echo " [DRY] Würde UPDATEN von '$rec_content' auf '$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 Update: $(echo $res | jq -r .errors[0].message)" fi fi fi fi } # ------------------------------------------------------------------ # SCHRITT 1: MAIL FROM ermitteln # ------------------------------------------------------------------ echo "" echo "--- 1. MAIL FROM Domain ---" # Wenn von außen nicht gesetzt, versuche via AWS if [ -z "$MAIL_FROM_DOMAIN" ]; then echo " Variable MAIL_FROM_DOMAIN leer, frage AWS SES..." 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 " ⚠️ Keine MAIL FROM in SES gefunden. Fallback auf: $MAIL_FROM_DOMAIN" fi else echo " Nutze vorgegebene MAIL FROM: $MAIL_FROM_DOMAIN" fi # ------------------------------------------------------------------ # SCHRITT 2: DKIM Records (CNAME) # ------------------------------------------------------------------ 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 (_amazonses) # ------------------------------------------------------------------ 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" ]; 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) ---" # MX für die Subdomain (feedback loop) ensure_record "MX" "$MAIL_FROM_DOMAIN" "feedback-smtp.$AWS_REGION.amazonses.com" false 10 # SPF für die Subdomain (strikte SES Regel) ensure_record "TXT" "$MAIL_FROM_DOMAIN" "v=spf1 include:amazonses.com ~all" false # ------------------------------------------------------------------ # SCHRITT 5: Root Domain SPF (Merge Logic) # ------------------------------------------------------------------ echo "" echo "--- 5. Root Domain SPF ---" if [ -n "$OLD_PROVIDER_SPF" ]; then # Merge: SES + Alter Provider FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all" echo " ℹ️ Modus: Migration (SES + Alt)" else # Nur SES FINAL_SPF="v=spf1 include:amazonses.com ~all" echo " ℹ️ Modus: SES only" fi ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false # ------------------------------------------------------------------ # SCHRITT 6: Root Domain MX (Safety First) # ------------------------------------------------------------------ echo "" echo "--- 6. Root Domain MX ---" # Hier wollen wir den Inbound SMTP von AWS (falls man AWS WorkMail nutzt oder DMS via AWS ingress) # WARTE: Du nutzt DMS. Dein DMS hat vermutlich eine eigene IP/Hostname (z.B. mail.buddelectric.net). # Wenn du SES NUR ZUM SENDEN nutzt, darfst du den Root MX NICHT auf Amazon ändern! # # Annahme: Du willst den MX für den Empfang setzen. # Da du oben "feedback-smtp" erwähnt hast, geht es wohl um den SES Return-Path. # Aber der "echte MX" für die Domain ($DOMAIN_NAME) zeigt auf DEINEN Mailserver (DMS). # # Falls du den MX auf deinen DMS Server zeigen lassen willst: TARGET_MX=${TARGET_MX:-"mail.$DOMAIN_NAME"} echo " ℹ️ Ziel-MX ist: $TARGET_MX" # HINWEIS: MX Records brauchen oft einen Hostnamen, keine IP. # Wir prüfen, ob ein MX existiert. ensure_record "MX" "$DOMAIN_NAME" "$TARGET_MX" false 10 # ------------------------------------------------------------------ # 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: Autodiscover / Autoconfig # ------------------------------------------------------------------ echo "" echo "--- 8. Autodiscover / Autoconfig ---" # Ziel ist meist der IMAP/SMTP Server echo " ℹ️ Ziel für Clients: $TARGET_MAIL_SERVER" ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false # Füge das zu deinem Skript hinzu (Schritt 9 optional): # ------------------------------------------------------------------ # SCHRITT 9: SRV Records (Service Discovery) # ------------------------------------------------------------------ echo "" echo "--- 9. SRV Records (Service Discovery) ---" # Das hilft Outlook, direkt "email-srvr.com" zu nutzen statt "mail.domain.tld" # Format: _service._proto.name TTL class SRV priority weight port target # IMAP SRV ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 $TARGET_MAIL_SERVER" false # IMAPS SRV (Port 993) ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 $TARGET_MAIL_SERVER" false # SUBMISSION SRV (Port 587) ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 $TARGET_MAIL_SERVER" false echo " ✅ SRV Records gesetzt (Server: $TARGET_MAIL_SERVER)" echo "" echo "✅ Fertig."