#!/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. # Wenn MAIL_SERVER_IP gesetzt ist, bekommt mail. einen A-Record # und imap/smtp/pop/webmail zeigen per CNAME auf 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 " Bitte setzen: export MAIL_SERVER_IP=" 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. 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 # ------------------------------------------------------------------ # 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 "============================================================"