diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 680a3b8..4ca7949 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -2,7 +2,18 @@ # 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 +# 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 @@ -10,13 +21,14 @@ set -e 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 -# 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 --- @@ -28,8 +40,8 @@ if ! command -v aws &> /dev/null; then echo "❌ Fehler: 'aws' CLI fehlt."; exit 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 + echo " Setze: export MAIL_SERVER_IP=" + # Kein exit - Abschnitt 8 wird ggf. übersprungen fi echo "============================================================" @@ -38,6 +50,8 @@ 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 @@ -53,13 +67,14 @@ 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 + local priority=$5 # Optional für MX echo " ⚙️ Prüfe $type $name..." @@ -86,7 +101,7 @@ ensure_record() { if [ "$(echo $res | jq -r .success)" == "true" ]; then echo " ✅ Erstellt." else - echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r '.errors[0].message')" fi fi else @@ -94,8 +109,8 @@ ensure_record() { 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." + echo " ⛔ Root-MX existiert aber ist anders: $rec_content" + echo " → Wird NICHT automatisch geändert (Migrations-Schutz)" return fi if [ "$DRY_RUN" = "true" ]; then @@ -104,9 +119,9 @@ ensure_record() { 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." + echo " ✅ Aktualisiert." else - echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler beim Updaten: $(echo $res | jq -r '.errors[0].message')" fi fi fi @@ -114,19 +129,20 @@ ensure_record() { } # ------------------------------------------------------------------ -# SCHRITT 1: MAIL FROM ermitteln +# SCHRITT 1: MAIL FROM Domain (aus SES lesen) # ------------------------------------------------------------------ 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" +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 # ------------------------------------------------------------------ @@ -134,89 +150,138 @@ fi # ------------------------------------------------------------------ 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 +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 +# SCHRITT 3: SES Verification TXT # ------------------------------------------------------------------ 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 +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) ---" -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 +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 +# SCHRITT 5: Root Domain SPF (Merge mit altem Provider) # ------------------------------------------------------------------ echo "" echo "--- 5. Root Domain SPF ---" -if [ -n "$OLD_PROVIDER_SPF" ]; then - FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all" + +# 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 - FINAL_SPF="v=spf1 include:amazonses.com ~all" + echo " ℹ️ Kein SPF Record vorhanden. Erstelle neuen." + ensure_record "TXT" "$DOMAIN_NAME" "v=spf1 include:amazonses.com ~all" false fi -ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false # ------------------------------------------------------------------ -# SCHRITT 6: Root Domain MX +# SCHRITT 6: Root Domain MX (nur Info, wird nicht geändert) # ------------------------------------------------------------------ -# 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 +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 ---" -ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false +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 (NEU): Mailclient Subdomains +# SCHRITT 8: Mailclient Subdomains (A + CNAME) # ------------------------------------------------------------------ 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 +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 - # 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 + 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 -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 + # 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 @@ -226,9 +291,28 @@ 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)"