From ee19b5b659e527bda6683f75c20ae77d2ca46e69 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 12:58:24 -0600 Subject: [PATCH] changes --- DMS/setup-dms-tls.sh | 202 ++++++++++++++++++ basic_setup/cloudflareMigrationDns.sh | 154 +++++++------- caddy/Caddyfile | 282 -------------------------- caddy/docker-compose.yml | 7 - caddy/update-caddy-certs.sh | 137 +++++++++++++ 5 files changed, 413 insertions(+), 369 deletions(-) create mode 100755 DMS/setup-dms-tls.sh create mode 100755 caddy/update-caddy-certs.sh diff --git a/DMS/setup-dms-tls.sh b/DMS/setup-dms-tls.sh new file mode 100755 index 0000000..a3c166e --- /dev/null +++ b/DMS/setup-dms-tls.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# setup-dms-tls.sh +# Generiert Dovecot und Postfix SNI-Konfigurationen für Multi-Domain TLS. +# Liest die vorhandenen Domains aus den DMS Accounts und erstellt: +# - docker-data/dms/config/dovecot-sni.cf (Dovecot SNI pro Domain) +# - docker-data/dms/config/postfix-main.cf (Postfix SNI Map + TLS Chain) +# +# Voraussetzung: +# - Caddy hat Wildcard-Certs gezogen (z.B. *.andreasknuth.de) +# - Cert-Verzeichnis ist gemountet unter /etc/mail/certs im Container +# - Konvention Cert-Pfad: /etc/mail/certs/DOMAIN_NAME/*.DOMAIN_NAME.crt|.key +# +# Usage: +# DMS_CONTAINER=mailserver ./setup-dms-tls.sh +# DMS_CONTAINER=mailserver DEFAULT_DOMAIN=email-srvr.com ./setup-dms-tls.sh + +set -e + +DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} +CONFIG_DIR=${CONFIG_DIR:-"./docker-data/dms/config"} +CERTS_BASE_PATH=${CERTS_BASE_PATH:-"/etc/mail/certs"} + +# Die Default-Domain für DMS hostname/domainname (bleibt email-srvr.com) +DEFAULT_DOMAIN=${DEFAULT_DOMAIN:-"email-srvr.com"} + +echo "============================================================" +echo " 🔐 DMS TLS SNI Setup (Multi-Domain)" +echo " Container: $DMS_CONTAINER" +echo " Config Dir: $CONFIG_DIR" +echo " Certs Base: $CERTS_BASE_PATH" +echo " Default Domain: $DEFAULT_DOMAIN" +echo "============================================================" + +# --- Alle Domains aus DMS Accounts lesen --- +echo "" +echo "📋 Lese Domains aus DMS..." +DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \ + | grep -oP '(?<=@)[^\s]+' \ + | sort -u) + +if [ -z "$DOMAINS" ]; then + echo "❌ Keine Accounts im DMS gefunden!" + echo " Bitte zuerst Accounts anlegen: ./manage_mail_user.sh add user@domain.com PW" + exit 1 +fi + +echo " Gefundene Domains:" +for d in $DOMAINS; do echo " - $d"; done + +# --- Cert-Verfügbarkeit prüfen --- +echo "" +echo "🔍 Prüfe Zertifikat-Verfügbarkeit (im Container)..." +DOMAINS_WITH_CERTS="" +DOMAINS_WITHOUT_CERTS="" + +for domain in $DOMAINS; do + # Caddy speichert Wildcard-Certs als: *.domain.tld/ + # Pfad im Container (über den Volume-Mount): /etc/mail/certs/*.domain.tld/ + CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + + # Prüfe ob die Datei im Container existiert + if docker exec "$DMS_CONTAINER" test -f "$CERT_PATH" 2>/dev/null; then + echo " ✅ $domain → Cert gefunden" + DOMAINS_WITH_CERTS="$DOMAINS_WITH_CERTS $domain" + else + echo " ⚠️ $domain → KEIN Cert unter $CERT_PATH" + echo " Caddy-Block '*.${domain}' eintragen und Caddy neu starten!" + DOMAINS_WITHOUT_CERTS="$DOMAINS_WITHOUT_CERTS $domain" + fi +done + +if [ -n "$DOMAINS_WITHOUT_CERTS" ]; then + echo "" + echo "⚠️ WARNUNG: Fehlende Certs für:$DOMAINS_WITHOUT_CERTS" + echo " Diese Domains werden NICHT in die SNI-Configs eingetragen." + echo " Bitte Certs erzeugen und Script erneut ausführen." + echo "" +fi + +if [ -z "$DOMAINS_WITH_CERTS" ]; then + echo "❌ Kein einziges Zertifikat gefunden! Abbruch." + exit 1 +fi + +# ================================================================ +# DOVECOT SNI Konfiguration generieren +# ================================================================ +DOVECOT_CFG="$CONFIG_DIR/dovecot-sni.cf" +echo "" +echo "📝 Generiere Dovecot SNI Konfiguration: $DOVECOT_CFG" + +cat > "$DOVECOT_CFG" << 'HEADER' +# dovecot-sni.cf - Automatisch generiert von setup-dms-tls.sh +# SNI-basierte TLS-Konfiguration für mehrere Domains. +# Dovecot wählt das Zertifikat anhand des SNI-Hostnamens des Clients. +# Dieses File wird via Volume-Mount in den Container eingebunden. +# +# Gemounteter Pfad: /tmp/docker-mailserver/dovecot-sni.cf +# In DMS docker-compose.yml volumes Sektion: +# - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro + +HEADER + +for domain in $DOMAINS_WITH_CERTS; do + CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + + cat >> "$DOVECOT_CFG" << EOF +# Domain: $domain +local_name mail.$domain { + ssl_cert = <$CERT_PATH + ssl_key = <$KEY_PATH +} +local_name imap.$domain { + ssl_cert = <$CERT_PATH + ssl_key = <$KEY_PATH +} +local_name smtp.$domain { + ssl_cert = <$CERT_PATH + ssl_key = <$KEY_PATH +} +local_name pop.$domain { + ssl_cert = <$CERT_PATH + ssl_key = <$KEY_PATH +} + +EOF +done + +echo " ✅ $DOVECOT_CFG erstellt ($(echo $DOMAINS_WITH_CERTS | wc -w) Domains)" + +# ================================================================ +# POSTFIX SNI Konfiguration generieren +# ================================================================ +POSTFIX_CFG="$CONFIG_DIR/postfix-main.cf" +echo "" +echo "📝 Generiere Postfix SNI Konfiguration: $POSTFIX_CFG" + +# Prüfe ob postfix-main.cf schon existiert und sichere sie +if [ -f "$POSTFIX_CFG" ]; then + cp "$POSTFIX_CFG" "${POSTFIX_CFG}.bak.$(date +%Y%m%d%H%M%S)" + echo " ℹ️ Backup erstellt: ${POSTFIX_CFG}.bak.*" +fi + +# TLS Chain Files für Postfix aufbauen +# Postfix unterstützt smtpd_tls_chain_files mit mehreren Key/Cert Paaren +CHAIN_FILES="" +for domain in $DOMAINS_WITH_CERTS; do + KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + if [ -z "$CHAIN_FILES" ]; then + CHAIN_FILES=" $KEY_PATH, $CERT_PATH" + else + CHAIN_FILES="$CHAIN_FILES,\n $KEY_PATH, $CERT_PATH" + fi +done + +cat > "$POSTFIX_CFG" << POSTFIX_CONF +# postfix-main.cf - Automatisch generiert von setup-dms-tls.sh +# Postfix SNI-Konfiguration für mehrere Domains. +# DMS lädt dieses File automatisch beim Start via /tmp/docker-mailserver/ + +# ------------------------------------------------------------------ +# TLS Chain Files (Key + Cert pro Domain) +# Postfix wählt das passende Paar automatisch per SNI +# ------------------------------------------------------------------ +smtpd_tls_chain_files = +$(printf '%b' "$CHAIN_FILES") + +POSTFIX_CONF + +echo " ✅ $POSTFIX_CFG erstellt" + +# ================================================================ +# Hinweise für docker-compose.yml +# ================================================================ +echo "" +echo "============================================================" +echo "📋 Nächste Schritte:" +echo "" +echo "1. Volume-Mounts in DMS docker-compose.yml hinzufügen:" +echo "" +echo " volumes:" +echo " # Bestehend (Caddy Certs - gesamtes Verzeichnis):" +echo " - /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/" +echo " acme-v02.api.letsencrypt.org-directory:/etc/mail/certs:ro" +echo "" +echo " # NEU - Dovecot SNI:" +echo " - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro" +echo "" +echo " # Postfix-main.cf wird von DMS automatisch geladen wenn sie liegt unter:" +echo " - ./docker-data/dms/config/postfix-main.cf:/tmp/docker-mailserver/postfix-main.cf:ro" +echo "" +echo "2. DMS neu starten:" +echo " docker compose restart mailserver" +echo "" +echo "3. TLS testen:" +for domain in $DOMAINS_WITH_CERTS; do + echo " openssl s_client -connect mail.$domain:993 -servername mail.$domain" +done +echo "============================================================" \ No newline at end of file diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 9d3b0a9..754e1a0 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -1,7 +1,8 @@ #!/bin/bash # cloudflareMigrationDns.sh # Setzt DNS Records für Amazon SES Migration + Cloudflare -# Unterstützt: DKIM, SPF (Merge), DMARC, MX (Safety Check), Autodiscover +# Unterstützt: DKIM, SPF (Merge), DMARC, MX, Autodiscover +# NEU: Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang set -e @@ -9,8 +10,13 @@ set -e 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. +# 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 --- @@ -19,9 +25,18 @@ if [ -z "$CF_API_TOKEN" ]; then echo "❌ Fehler: CF_API_TOKEN fehlt."; exit 1; 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 "============================================================" @@ -38,41 +53,31 @@ 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 + local priority=$5 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 @@ -81,34 +86,27 @@ ensure_record() { if [ "$(echo $res | jq -r .success)" == "true" ]; then echo " ✅ Erstellt." else - echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" fi fi else - # --- EXISTS --- if [ "$rec_content" == "$content" ]; then - echo " 🆗 Identisch vorhanden. Überspringe." + echo " 🆗 Identisch. Ü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. + echo " ⛔ MX existiert aber anders! Gefunden: $rec_content / Erwartet: $content" + echo " Bitte Record ID $rec_id manuell löschen." 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'" + 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 Update: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" fi fi fi @@ -120,37 +118,36 @@ ensure_record() { # ------------------------------------------------------------------ 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" + echo " ⚠️ Kein MAIL FROM in SES. Fallback: $MAIL_FROM_DOMAIN" fi else - echo " Nutze vorgegebene MAIL FROM: $MAIL_FROM_DOMAIN" + echo " Nutze: $MAIL_FROM_DOMAIN" fi # ------------------------------------------------------------------ -# SCHRITT 2: DKIM Records (CNAME) +# 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) +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) +# 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" ]; then +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 @@ -159,47 +156,27 @@ fi # ------------------------------------------------------------------ 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) +# SCHRITT 5: Root Domain SPF # ------------------------------------------------------------------ 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) +# SCHRITT 6: Root Domain MX # ------------------------------------------------------------------ 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 +ensure_record "MX" "$DOMAIN_NAME" "mail.$DOMAIN_NAME" false 10 # ------------------------------------------------------------------ # SCHRITT 7: DMARC @@ -209,34 +186,51 @@ echo "--- 7. DMARC ---" ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false # ------------------------------------------------------------------ -# SCHRITT 8: Autodiscover / Autoconfig +# SCHRITT 8 (NEU): Mailclient Subdomains # ------------------------------------------------------------------ echo "" -echo "--- 8. Autodiscover / Autoconfig ---" -# Ziel ist meist der IMAP/SMTP Server -echo " ℹ️ Ziel für Clients: $TARGET_MAIL_SERVER" +echo "--- 8. Mailclient Subdomains (A + CNAME) ---" -ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false -ensure_record "CNAME" "autoconfig.$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 +# 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 -# Füge das zu deinem Skript hinzu (Schritt 9 optional): # ------------------------------------------------------------------ -# SCHRITT 9: SRV Records (Service Discovery) +# SCHRITT 9: Autodiscover / Autoconfig # ------------------------------------------------------------------ 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 +echo "--- 9. Autodiscover / Autoconfig ---" +ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false +ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false -# 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)" +# ------------------------------------------------------------------ +# SCHRITT 10: SRV Records +# ------------------------------------------------------------------ +echo "" +echo "--- 10. SRV Records ---" +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 echo "" -echo "✅ Fertig." \ No newline at end of file +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 "============================================================" \ No newline at end of file diff --git a/caddy/Caddyfile b/caddy/Caddyfile index eb167b5..5f16483 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -19,285 +19,3 @@ autoconfig.bayarea-cc.com, autoconfig.bizmatch.net { respond "Autodiscover Service Online" 200 } -# Prod: Neue Domains -www.bizmatch.net { - handle /pictures/* { - root * /home/aknuth/git/bizmatch-project/bizmatch-server # Prod-Ordner - file_server - } - # Statische Dateien (CSS, JS, Bilder) – lange cachen, da sich der Name bei Änderungen ändert - header /assets/* Cache-Control "public, max-age=31536000, immutable" - header /*.css Cache-Control "public, max-age=31536000, immutable" - header /*.js Cache-Control "public, max-age=31536000, immutable" - - # Die index.html und API-Antworten – NIEMALS cachen - header /index.html Cache-Control "no-cache, no-store, must-revalidate" - - #handle { - # root * /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser # Neuer Prod-Dist-Ordner - # try_files {path} {path}/ /index.html - # file_server - #} - handle { - reverse_proxy host.docker.internal:4200 - } - log { - output file /var/log/caddy/access.prod.log # Separate Logs - } - encode gzip zstd -} -bizmatch.net { - redir https://www.bizmatch.net{uri} permanent - import email_settings -} -www.qrmaster.net { - handle { - reverse_proxy host.docker.internal:3050 - } - log { - output file /var/log/caddy/qrmaster.log - format console - } - encode gzip -} -qrmaster.net { - redir https://www.qrmaster.net{uri} permanent -} -bayarea-cc.com { - # TLS-Direktive entfernen, falls Cloudflare die Verbindung terminiert - # tls { - # dns cloudflare {env.CLOUDFLARE_API_TOKEN} - # } - - handle /api { - reverse_proxy host.docker.internal:3001 - } - handle { - root * /app - try_files {path} /index.html - file_server - } - log { - output stderr - format console - } - encode gzip - import email_settings -} -www.bayarea-cc.com { - redir https://bayarea-cc.com{uri} permanent -} -setup.bayarea-cc.com { - # Wir setzen das Root-Verzeichnis auf den neuen Pfad im Container - root * /var/www/email-setup - - # Webserver-Standardverhalten - file_server - - # Wenn jemand nur die Domain aufruft, zeige setup.html - try_files {path} /setup.html -} -cielectrical.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3000 - } - log { - output file /var/log/caddy/cielectrical.log - format console - } - encode gzip -} -hamptonbrown.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3010 - } - log { - output file /var/log/caddy/hamptonbrown.log - format console - } - encode gzip -} -nqsltd.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3020 - } - log { - output file /var/log/caddy/nqsltd.log - format console - } - encode gzip -} -gregknoppcpa.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3030 - } - log { - output file /var/log/caddy/gregknoppcpa.log - format console - } - encode gzip -} -buddelectric.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3040 - } - log { - output file /var/log/caddy/buddelectric.log - format console - } - encode gzip zstd -} -iitwelders.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:8080 - } - log { - output file /var/log/caddy/iitwelders.log - format console - } - encode gzip -} -fancytextstuff.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3010 - } - log { - output file /var/log/caddy/fancytext.log - format console - } - encode gzip -} -www.fancytextstuff.com { - redir https://fancytextstuff.com{uri} permanent -} -auth.bizmatch.net { - reverse_proxy https://bizmatch-net.firebaseapp.com { - header_up Host bizmatch-net.firebaseapp.com - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - header_up X-Real-IP {remote_host} - } -} -gitea.bizmatch.net { - reverse_proxy gitea:3500 -} - -dev.bizmatch.net { - handle /pictures/* { - root * /home/aknuth/git/bizmatch-project/bizmatch-server - file_server - } - - handle { - root * /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser - try_files {path} {path}/ /index.html - file_server - } - - log { - output file /var/log/caddy/access.log { - roll_size 10MB - roll_keep 5 - roll_keep_for 48h - } - } - - encode gzip - -} - - -api.bizmatch.net { - reverse_proxy host.docker.internal:3001 { # Neu: Proxy auf Prod-Port 3001 - header_up X-Real-IP {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} - header_up CF-IPCountry {http.request.header.CF-IPCountry} - } -} -mailsync.bizmatch.net { - reverse_proxy host.docker.internal:5000 { - header_up X-Real-IP {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} - header_up CF-IPCountry {http.request.header.CF-IPCountry} - } -} - -# Roundcube für docker-mailserver -app.email-bayarea.com { - reverse_proxy roundcube:80 - - log { - output stderr - format console - } - - encode gzip -} -# Roundcube für docker-mailserver -config.email-bayarea.com { - - root * /home/aknuth/git/config-email/frontend/dist - try_files {path} {path}/ /index.html - file_server - - log { - output file /var/log/caddy/config-email.log - } - - encode gzip -} -# Roundcube für docker-mailserver -api.email-bayarea.com { - reverse_proxy host.docker.internal:3002 - - log { - output stderr - format console - } - - encode gzip -} -annavillesda.org { - # API requests to backend - handle /api/* { - reverse_proxy host.docker.internal:3070 - } - - # Frontend static files - handle { - root * /home/aknuth/git/annaville-sda-site/dist - try_files {path} {path}/ /index.html - file_server - } - - log { - output file /var/log/caddy/access.prod.log - } - - encode gzip -} -www.annavillesda.org { - redir https://annavillesda.org{uri} permanent -} -# ----------------- -# just for certificate generation -# ----------------- -mail.andreasknuth.de { - reverse_proxy nginx-mailcow:8080 -} -web.email-bayarea.com { - reverse_proxy nginx-mailcow:8080 -} -# Dieser Block dient nur dazu, das Zertifikat für den Mailserver zu beschaffen/erneuern. -mail.email-srvr.com { - respond "Mailserver Certificate Authority is running." 200 -} diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index bbb49e9..41c6677 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -23,14 +23,7 @@ services: - $PWD/email-setup:/var/www/email-setup - caddy_data:/data - caddy_config:/config - - /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser - - /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser - - /home/aknuth/git/bizmatch-project/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project/bizmatch-server/pictures - - /home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures - - /home/aknuth/git/annaville-sda-site/dist:/home/aknuth/git/annaville-sda-site/dist:ro # ← DAS FEHLT! - - /home/aknuth/git/bay-area-affiliates/dist/bay-area-affiliates/browser:/app - /home/aknuth/log/caddy:/var/log/caddy - - /home/aknuth/git/config-email/frontend/dist:/home/aknuth/git/config-email/frontend/dist:ro environment: - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL} diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh new file mode 100755 index 0000000..70c3952 --- /dev/null +++ b/caddy/update-caddy-certs.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# update-caddy-certs.sh +# Liest alle Domains aus dem DMS und generiert die notwendigen +# Caddyfile-Blöcke für Wildcard-Zertifikate. +# +# Die generierten Blöcke werden NICHT automatisch in das Caddyfile geschrieben, +# sondern in eine separate Datei (caddy_mail_certs.conf) ausgegeben, +# die per "import" in das Hauptcaddyfile eingebunden werden kann. +# +# Usage: +# DMS_CONTAINER=mailserver ./update-caddy-certs.sh +# DMS_CONTAINER=mailserver CADDY_DIR=/pfad/zu/caddy ./update-caddy-certs.sh +# DMS_CONTAINER=mailserver DRY_RUN=true ./update-caddy-certs.sh + +set -e + +DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} +CADDY_DIR=${CADDY_DIR:-"."} # Verzeichnis wo das Caddyfile liegt +OUTPUT_FILE=${OUTPUT_FILE:-"$CADDY_DIR/mail_certs"} # Ohne Extension - Caddy importiert ohne .conf +DRY_RUN=${DRY_RUN:-"false"} + +echo "============================================================" +echo " 📜 Caddy Wildcard-Cert Konfig Generator" +echo " DMS Container: $DMS_CONTAINER" +echo " Output: $OUTPUT_FILE" +[ "$DRY_RUN" = "true" ] && echo " ⚠️ DRY RUN - Keine Dateien werden geschrieben" +echo "============================================================" + +# --- Domains aus DMS lesen --- +echo "" +echo "📋 Lese Domains aus DMS..." +DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \ + | grep -oP '(?<=@)[^\s]+' \ + | sort -u) + +if [ -z "$DOMAINS" ]; then + echo "❌ Keine Accounts gefunden!" + exit 1 +fi + +echo " Gefundene Domains:" +for d in $DOMAINS; do echo " - $d"; done + +# --- email-srvr.com immer einschließen (Default-Domain des DMS) --- +EXTRA_DOMAINS="email-srvr.com" +for extra in $EXTRA_DOMAINS; do + if ! echo "$DOMAINS" | grep -q "^${extra}$"; then + DOMAINS="$DOMAINS $extra" + echo " + $extra (Default DMS Domain - immer dabei)" + fi +done + +# --- Konfig generieren --- +echo "" +echo "📝 Generiere Caddy-Konfiguration..." + +CONTENT="" +CONTENT="${CONTENT}# mail_certs - Automatisch generiert von update-caddy-certs.sh\n" +CONTENT="${CONTENT}# Wildcard-Zertifikate für alle DMS-Domains.\n" +CONTENT="${CONTENT}# Einbinden im Hauptcaddyfile: import mail_certs\n" +CONTENT="${CONTENT}# Generiert: $(date)\n" +CONTENT="${CONTENT}\n" + +for domain in $DOMAINS; do + echo " → Block für: $domain" + CONTENT="${CONTENT}# Wildcard-Cert für $domain\n" + CONTENT="${CONTENT}*.${domain}, ${domain} {\n" + CONTENT="${CONTENT} tls {\n" + CONTENT="${CONTENT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n" + CONTENT="${CONTENT} }\n" + CONTENT="${CONTENT} respond \"OK\" 200\n" + CONTENT="${CONTENT}}\n" + CONTENT="${CONTENT}\n" +done + +# --- Ausgabe --- +if [ "$DRY_RUN" = "true" ]; then + echo "" + echo "--- VORSCHAU (DRY RUN) ---" + printf '%b' "$CONTENT" + echo "--- ENDE VORSCHAU ---" +else + printf '%b' "$CONTENT" > "$OUTPUT_FILE" + echo "" + echo " ✅ Geschrieben: $OUTPUT_FILE" +fi + +# --- Prüfen ob Import im Caddyfile vorhanden --- +CADDYFILE="$CADDY_DIR/Caddyfile" +if [ -f "$CADDYFILE" ]; then + if grep -q "import mail_certs" "$CADDYFILE"; then + echo " ✅ 'import mail_certs' bereits im Caddyfile vorhanden." + else + echo "" + echo "⚠️ AKTION ERFORDERLICH:" + echo " 'import mail_certs' fehlt noch im Caddyfile!" + echo " Bitte folgende Zeile am Anfang (nach dem globalen Block) eintragen:" + echo "" + echo " import mail_certs" + echo "" + echo " Oder automatisch einfügen? (y/N)" + read -r answer + if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then + # Import nach der ersten Zeile mit "import " einfügen (falls schon welche da sind) + # oder nach dem globalen {} Block + if grep -q "^import " "$CADDYFILE"; then + # Schreibe nach der letzten import-Zeile + sed -i "/^import /a import mail_certs" "$CADDYFILE" + else + # Schreibe nach dem schließenden } des globalen Blocks + sed -i "/^}/a \\\nimport mail_certs" "$CADDYFILE" + fi + echo " ✅ Import eingefügt." + fi + fi +fi + +# --- Caddy reload --- +echo "" +echo "============================================================" +echo "🔄 Nächste Schritte:" +echo "" +echo "1. Caddyfile prüfen - 'import mail_certs' muss vorhanden sein" +echo "" +echo "2. Caddy Konfiguration validieren:" +echo " docker exec caddy caddy validate --config /etc/caddy/Caddyfile" +echo "" +echo "3. Caddy neu laden (kein Downtime):" +echo " docker exec caddy caddy reload --config /etc/caddy/Caddyfile" +echo "" +echo "4. Cert-Generierung verfolgen (dauert ~30s pro Domain):" +echo " docker logs -f caddy 2>&1 | grep -i 'certificate\|acme\|tls'" +echo "" +echo "5. Cert-Pfade prüfen:" +echo " ls /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/" +echo " acme-v02.api.letsencrypt.org-directory/" +echo "============================================================" \ No newline at end of file