#!/usr/bin/env bash # ============================================================================= # BUILD-C3 — DNS Apply Script # Wavult Group — Partner-/Geo-modell DNS-implementation # ============================================================================= # Förberett av: OpenClaw BUILD-C2 subagent | 2026-06-06 # Status: FÖRBERETT — EJ KÖRT # # SÄKERHETSPOLICY: # Detta skript KÖR INGA DNS-ändringar automatiskt. # Varje sektion är en funktion som måste anropas explicit. # Kör: bash c3-apply-dns.sh --help för instruktioner. # # KÖRMILJÖ: # - Kräver AWS CLI med profil 'platform-ai-agent' (eller default-profil) # - Kräver IAM-behörighet: route53:ChangeResourceRecordSets, route53:ListResourceRecordSets # - Kräver: jq (för JSON-parsning) # - Region: eu-north-1 (Route53 är globalt, men default-profil används) # # GODKÄNNANDESTRUKTUR: # ✅ SÄKER — Kan köras utan Eriks godkännande (inga MX-ändringar, inga deletions) # ⚠️ ERIK-OK — Kräver explicit godkännande av Erik innan körning # 🔴 DESTRUKTIV — Tar bort data, kräver verifiering + Erik-godkännande # ============================================================================= set -euo pipefail # --------------------------------------------------------------------------- # Konfiguration # --------------------------------------------------------------------------- AWS_REGION="eu-north-1" DMARC_RUA="dmarc@aamos.systems" MAILU_MX="mail.wavult.com" MAILU_MX_PRIORITY=10 # DKIM-selektor och pubkey måste hämtas från Mailu-admin och fyllas i nedan DKIM_SELECTOR="mail" # OBS: Ersätt DKIM_PUBKEY_PLACEHOLDER med faktisk pubkey från Mailu-admin # Hämta via: Mailu Admin → Domain → → DKIM → kopiera pubkey-strängen DKIM_PUBKEY="HÄMTA-FRÅN-MAILU-ADMIN-FÖRE-KÖRNING" # --------------------------------------------------------------------------- # Hjälpfunktioner # --------------------------------------------------------------------------- log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"; } warn() { echo "[WARN] $*" >&2; } die() { echo "[ERROR] $*" >&2; exit 1; } check_deps() { command -v aws >/dev/null || die "aws CLI saknas" command -v jq >/dev/null || die "jq saknas" [[ "$DKIM_PUBKEY" == "HÄMTA-FRÅN-MAILU-ADMIN-FÖRE-KÖRNING" ]] && \ die "DKIM_PUBKEY är inte ifyllt — hämta från Mailu-admin och sätt variabeln" } # Skicka en Route53 change batch (JSON-sträng) och vänta tills den är INSYNC apply_change_batch() { local zone_id="$1" local batch_json="$2" local description="$3" log "Applicerar ändring på zone $zone_id: $description" local change_id change_id=$(aws route53 change-resource-record-sets \ --hosted-zone-id "$zone_id" \ --change-batch "$batch_json" \ --query 'ChangeInfo.Id' --output text) log "Change ID: $change_id — väntar på INSYNC..." aws route53 wait resource-record-sets-changed --id "$change_id" log "✅ $description — INSYNC" } # Bygg ett UPSERT-batch för SPF + DMARC (ingen MX = partnerdomän/avveckling) build_no_mx_spf_dmarc_batch() { local domain="$1" jq -n \ --arg domain "$domain" \ --arg rua "$DMARC_RUA" \ '{ "Comment": ("SPF -all + DMARC reject for " + $domain + " (no MX)"), "Changes": [ { "Action": "UPSERT", "ResourceRecordSet": { "Name": ($domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": "\"v=spf1 -all\""}] } }, { "Action": "UPSERT", "ResourceRecordSet": { "Name": ("_dmarc." + $domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": ("\"v=DMARC1; p=reject; rua=mailto:" + $rua + "\"")}] } } ] }' } # Bygg ett UPSERT-batch för MX + SPF + DKIM + DMARC p=none (Nivå 2 EU-landsdomän) build_eu_level2_batch() { local domain="$1" local dkim_selector="$2" local dkim_pubkey="$3" jq -n \ --arg domain "$domain" \ --arg mx "$MAILU_MX" \ --argjson prio "$MAILU_MX_PRIORITY" \ --arg sel "$dkim_selector" \ --arg key "$dkim_pubkey" \ --arg rua "$DMARC_RUA" \ '{ "Comment": ("EU Level-2 mail setup for " + $domain), "Changes": [ { "Action": "UPSERT", "ResourceRecordSet": { "Name": ($domain + "."), "Type": "MX", "TTL": 300, "ResourceRecords": [{"Value": (($prio | tostring) + " " + $mx + ".")}] } }, { "Action": "UPSERT", "ResourceRecordSet": { "Name": ($domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": ("\"v=spf1 mx a:" + $mx + " ~all\"")}] } }, { "Action": "UPSERT", "ResourceRecordSet": { "Name": ($sel + "._domainkey." + $domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": ("\"v=DKIM1; k=rsa; p=" + $key + "\"")}] } }, { "Action": "UPSERT", "ResourceRecordSet": { "Name": ("_dmarc." + $domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": ("\"v=DMARC1; p=none; rua=mailto:" + $rua + "; fo=1\"")}] } } ] }' } # Bygg DELETE-batch för MX-post (kräver att befintlig MX-post anges exakt) build_delete_mx_batch() { local domain="$1" local existing_mx_value="$2" # t.ex. "10 mail.wavult.com." jq -n \ --arg domain "$domain" \ --arg mxval "$existing_mx_value" \ --arg rua "$DMARC_RUA" \ '{ "Comment": ("REMOVE MX + add SPF -all + DMARC reject for " + $domain), "Changes": [ { "Action": "DELETE", "ResourceRecordSet": { "Name": ($domain + "."), "Type": "MX", "TTL": 300, "ResourceRecords": [{"Value": $mxval}] } }, { "Action": "UPSERT", "ResourceRecordSet": { "Name": ($domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": "\"v=spf1 -all\""}] } }, { "Action": "UPSERT", "ResourceRecordSet": { "Name": ("_dmarc." + $domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": ("\"v=DMARC1; p=reject; rua=mailto:" + $rua + "\"")}] } } ] }' } # --------------------------------------------------------------------------- # FAS 1 — SÄKER: Partnerdomäner (Nivå 3) — ingen MX + DMARC reject # ✅ SÄKER — Inga MX-ändringar, inga deletions. Lägger till SPF -all och DMARC reject. # Dessa domäner har ALDRIG haft aktiv MX (bekräftat i C1). # --------------------------------------------------------------------------- fas1_partner_domains() { log "=== FAS 1: Partnerdomäner — SPF -all + DMARC reject ===" log "✅ SÄKER: Inga destruktiva ändringar, inga MX-poster påverkas" # Landvex partner-domäner declare -A PARTNER_ZONES=( ["landvex.mx"]="Z035940510YOVC15ORL0P" ["landvex.lat"]="Z0452682DQUO68GWV1LF" ["landvex.asia"]="Z07961892X5K5ZAGU9P7A" ["landvex.in"]="Z0114999EEC7DABEJF1R" ["landvex.jp"]="Z06417423FRM1NZ24HQIG" # landvex.co och .tech — se FLAGGA nedan # ["landvex.co"]="Z06390531NTPHQPHH556O" # FLAGGA: Klassificering oklar — Erik-beslut # ["landvex.tech"]="Z03595122SFEDANKC6HLC" # FLAGGA: Klassificering oklar — Erik-beslut # Quixzoom partner-domäner ["quixzoom.mx"]="Z03592031KP18830ISSFO" ["quixzoom.lat"]="Z07960653PSQVVJH6MWKQ" ["quixzoom.in"]="Z0116012C853QJX10S7F" ["quixzoom.jp"]="Z0691646HX1TESS78DHG" ["quixzoom.asia"]="Z03303363QVPGTSXJ4CBP" # quixzoom.co och .tech — se FLAGGA nedan # ["quixzoom.co"]="Z0640526G3V30S6YP4KW" # FLAGGA: Klassificering oklar — Erik-beslut # ["quixzoom.tech"]="Z06392373COSIWKDCKU02" # FLAGGA: Klassificering oklar — Erik-beslut ) for domain in "${!PARTNER_ZONES[@]}"; do local zone_id="${PARTNER_ZONES[$domain]}" local batch batch=$(build_no_mx_spf_dmarc_batch "$domain") apply_change_batch "$zone_id" "$batch" "Partnerdomän: $domain" done log "=== FAS 1 klar ===" } # --------------------------------------------------------------------------- # FAS 2 — SÄKER: EU Nivå 2 (landsdomäner) — MX + SPF + DKIM + DMARC p=none # ✅ SÄKER: Aktiverar mailmottagning (redirect → primär via Mailu catch-all). # DMARC p=none = neutral, samlar rapporter utan att blockera. # FÖRUTSÄTTNING: DKIM_PUBKEY måste vara ifyllt med rätt pubkey från Mailu. # --------------------------------------------------------------------------- fas2_eu_level2_domains() { log "=== FAS 2: EU Nivå 2 (landsdomäner) — MX + SPF + DKIM + DMARC p=none ===" log "✅ SÄKER: Aktiverar mailmottagning, DMARC p=none (neutral)" log "⚠️ KRAV: DKIM_PUBKEY måste vara ifyllt — kontrollera variabeln" # Landvex EU Nivå 2 (har idag ingen MX) declare -A EU2_ZONES=( ["landvex.se"]="Z094509730UHHMSPHTT2N" ["landvex.eu"]="Z03535482N3R7HVG35RLC" ["landvex.de"]="Z09315373152U0OFU6S2J" ["landvex.fr"]="Z05975142FLYRUIUNWORN" ["landvex.nl"]="Z05939403BEIHDPX4CFG1" ["landvex.es"]="Z03544502KQL4NRCHY4AU" ["landvex.it"]="Z06922101ZAJMZJR3ZLJI" ["landvex.fi"]="Z0646854PR1U78P1AJS0" ["landvex.co.uk"]="Z06400622V84QB2SX67DT" ["landvex.be"]="Z0642291ACB2WGN8KIPV" ["landvex.dk"]="Z03432136USE09VHQGF9" ["landvex.net"]="Z06400772MZF13N0GK4HC" ["landvex.org"]="Z03600372562O0AYE74AX" ["landvex-gov.eu"]="Z03523293HYR2LY9FB8V3" ["landvex-intelligence.eu"]="Z04528812DA06BOTM71QN" ["landvex-infrastructure.eu"]="Z06915882XI394D5IHZZ0" ["landvex-systems.com"]="Z03530401ABRF61KQ4UGD" ["landvex-secure.com"]="Z06922211OLB8FWUK47U5" # Quixzoom EU Nivå 2 ["quixzoom.eu"]="Z0352300H6B69TZB3FB" ["quixzoom.nl"]="Z08393705I60UK5C6T3D" ["quixzoom.de"]="Z02042236EVWD34RIL8" ["quixzoom.fr"]="Z0590787VZDW5D5XHQAV" ["quixzoom.es"]="Z03537332N1170YZA6Y96" ["quixzoom.it"]="Z01178703OEP5JBWVMCVN" ["quixzoom.fi"]="Z01156752B3NC8OLB0VQ3" ["quixzoom.ch"]="Z05948653CJP2I13KROV9" ["quixzoom.be"]="Z02403741O3VID61R4CWK" ["quixzoom.dk"]="Z03418436Y57RW7ET3I2" ["quixzoom.net"]="Z064013718W8XEVMMH8I3" ["quixzoom.org"]="Z0646024GDLOX0L6C9L0" # Wavult ["wavult.se"]="Z09416523AFDKP6WRCHRL" # landvex.ch — använd en av de två zones (se duplikat-hantering fas5) # FLAGGA: Verifiera vilken zone som är aktiv (NS-delegation) INNAN körning # ["landvex.ch"]="Z060292760R1ZY0PQH3V" # Duplikat — verifiera FÖR körning # quixzoom.co.uk — samma # ["quixzoom.co.uk"]="Z035263432T28MD24XUIG" # Duplikat — verifiera FÖR körning ) for domain in "${!EU2_ZONES[@]}"; do local zone_id="${EU2_ZONES[$domain]}" local batch batch=$(build_eu_level2_batch "$domain" "$DKIM_SELECTOR" "$DKIM_PUBKEY") apply_change_batch "$zone_id" "$batch" "EU Nivå 2: $domain" done log "=== FAS 2 klar ===" } # --------------------------------------------------------------------------- # FAS 3 — SÄKER: pixdrift.com — SPF -all + DMARC reject (har ingen MX) # ✅ SÄKER: Pixdrift är dödd, har aldrig haft MX (bekräftat C1). Inga deletions. # --------------------------------------------------------------------------- fas3_pixdrift() { log "=== FAS 3: pixdrift.com — SPF -all + DMARC reject ===" log "✅ SÄKER: Pixdrift har ingen MX — lägger bara till SPF och DMARC" local zone_id="Z06885272CR0QX3ZHVMTK" local batch batch=$(build_no_mx_spf_dmarc_batch "pixdrift.com") apply_change_batch "$zone_id" "$batch" "Avveckling: pixdrift.com" log "=== FAS 3 klar ===" } # --------------------------------------------------------------------------- # FAS 4A — ⚠️ ERIK-OK: Verifiera Mailu-loggar FÖR körning # Kontrollera att ingen aktiv korrespondens finns på corpfitt/hypbit # --------------------------------------------------------------------------- fas4a_verify_dead_domains_mail() { log "=== FAS 4A: Verifiering av aktiv korrespondens (READ-ONLY) ===" log "⚠️ KRÄVER MANUELL KONTROLL: Logga in i Mailu-admin (mail.wavult.com)" log "" log "Kontrollera följande INNAN fas4b körs:" log " 1. Gå till Mailu-admin → Domains → corpfitt.com" log " - Finns det konton/alias? → Ta bort dem i Mailu FÖR att ta bort MX i DNS" log " - Kolla mailloggar (Mailu → Logs) efter inkommande mail senaste 90 dagar" log " 2. Upprepa för hypbit.com" log " 3. Om inga konton/alias + ingen trafik → godkänn fas4b med Erik" log "" log "Alternativt via AWS SSM (root-access):" log " sudo grep -E '(corpfitt|hypbit)' /var/log/mail.log | tail -50" log " (Anpassa loggväg efter Mailu-installationens konfiguration)" log "" log "=== FAS 4A klar — KÖR INTE FAS 4B UTAN ERIKS GODKÄNNANDE ===" } # --------------------------------------------------------------------------- # FAS 4B — ⚠️ ERIK-OK: corpfitt.com + hypbit.com — Ta bort MX + DMARC reject # KRÄVER: Fas 4A genomförd + Eriks godkännande + verifiering att inga konton finns # --------------------------------------------------------------------------- fas4b_decommission_dead_mx() { log "=== FAS 4B: Avveckling MX för döda domäner ===" log "⚠️ KRÄVER ERIKS GODKÄNNANDE" log "⚠️ KRÄVER: Fas 4A genomförd (Mailu-loggar kontrollerade)" log "" # Bekräftelseprompt — kräver explicit input read -r -p "Har Erik godkänt denna åtgärd? Har fas4a genomförts? (skriv 'JA ERIK HAR GODKÄNT' för att fortsätta): " confirm if [[ "$confirm" != "JA ERIK HAR GODKÄNT" ]]; then log "Avbröt — inget ändrat." exit 0 fi # corpfitt.com log "Tar bort MX på corpfitt.com (Zone: Z03586003P55GRO87T6M3)" local corpfitt_batch corpfitt_batch=$(build_delete_mx_batch "corpfitt.com" "10 mail.wavult.com.") apply_change_batch "Z03586003P55GRO87T6M3" "$corpfitt_batch" "Avveckling MX: corpfitt.com" # hypbit.com log "Tar bort MX på hypbit.com (Zone: Z06944491GZGBRKHYNYSK)" local hypbit_batch hypbit_batch=$(build_delete_mx_batch "hypbit.com" "10 mail.wavult.com.") apply_change_batch "Z06944491GZGBRKHYNYSK" "$hypbit_batch" "Avveckling MX: hypbit.com" log "=== FAS 4B klar ===" } # --------------------------------------------------------------------------- # FAS 5 — ⚠️ ERIK-OK: Duplikatzon-konsolidering # KRÄVER: NS-verifiering mot registraren, Eriks godkännande, manuell record-jämförelse # --------------------------------------------------------------------------- fas5_inspect_duplicate_zones() { log "=== FAS 5: Duplikatzoner — Inspektion (READ-ONLY) ===" log "✅ SÄKER: Denna funktion listar records och NS — inga ändringar" declare -A DUP_ZONES=( ["landvex.ch"]="Z060292760R1ZY0PQH3V Z00716462L6XS9QZX1HHS" ["quixzoom.co.uk"]="Z035263432T28MD24XUIG Z06391212CQRAVQ1FMNHV" ["aamos.ai"]="Z0992155TZTAVTB9DKU Z10021043NUR85M9WUOQM" ["wavult.com"]="Z03332778KCOYPM9OBKP Z02946821NE63OOAKDTKA" ) for domain in "${!DUP_ZONES[@]}"; do IFS=' ' read -r zone_a zone_b <<< "${DUP_ZONES[$domain]}" log "" log "--- $domain ---" log "Zone A ($zone_a) records:" aws route53 list-resource-record-sets --hosted-zone-id "$zone_a" \ --query 'ResourceRecordSets[*].{Type:Type,Name:Name,Records:ResourceRecords}' \ --output table 2>/dev/null || warn "Kunde inte lista records för $zone_a" log "Zone B ($zone_b) records:" aws route53 list-resource-record-sets --hosted-zone-id "$zone_b" \ --query 'ResourceRecordSets[*].{Type:Type,Name:Name,Records:ResourceRecords}' \ --output table 2>/dev/null || warn "Kunde inte lista records för $zone_b" log "Extern NS-kontroll för $domain:" dig NS "$domain" +short || warn "dig-fel för $domain" done log "" log "=== FAS 5 INSPEKTION KLAR ===" log "Jämför NS-poster ovan mot registrarens delegering." log "Zone vars NS matchar registraren = aktiv. Den andra = phantom (kan raderas)." log "RADERA INTE zones via detta skript — gör det via AWS-konsolen efter manuell verifiering." } # --------------------------------------------------------------------------- # FAS 6 — ⚠️ DMARC-skärpning (aktiva maildomäner) # KRÄVER: 2 veckor DMARC-rapportanalys + DKIM-verifiering # --------------------------------------------------------------------------- fas6_dmarc_tighten_active_domains() { log "=== FAS 6: DMARC-skärpning på aktiva maildomäner ===" log "⚠️ KRÄVER: DKIM verifierat i praktiken (mail-tester.com)" log "⚠️ KRÄVER: 2 veckors DMARC-rapportanalys utan avvikelser" log "⚠️ KRÄVER: Eriks godkännande" log "" # Dessa domäner har AKTIV MX och roll-routing — skärpning kräver extra försiktighet local ACTIVE_MAIL_DOMAINS=( "aamos.systems:Z03908683A2AIMRZR85BQ" "landvex.com:Z05966261S4KJRAGIJ6FQ" "quixzoom.com:Z04915922EJ53UAUIOPFG" # wavult.com och aamos.ai har duplikatzoner — lös fas5 först # "wavult.com:Z03332778KCOYPM9OBKP" # "aamos.ai:Z0992155TZTAVTB9DKU" ) read -r -p "Har DKIM verifierats? Har DMARC-rapporter analyserats 2 veckor? Eriks godkännande? (skriv 'JA VERIFIERAT' för att fortsätta): " confirm if [[ "$confirm" != "JA VERIFIERAT" ]]; then log "Avbröt — inget ändrat." exit 0 fi read -r -p "Välj DMARC-policy [quarantine/reject]: " policy if [[ "$policy" != "quarantine" && "$policy" != "reject" ]]; then die "Ogiltig policy: $policy. Välj 'quarantine' eller 'reject'." fi read -r -p "Välj pct (1-100, t.ex. 10 för gradvis utrullning): " pct if ! [[ "$pct" =~ ^[0-9]+$ ]] || (( pct < 1 || pct > 100 )); then die "Ogiltigt pct-värde: $pct" fi for entry in "${ACTIVE_MAIL_DOMAINS[@]}"; do IFS=':' read -r domain zone_id <<< "$entry" log "Skärper DMARC på $domain → p=$policy; pct=$pct" local batch batch=$(jq -n \ --arg domain "$domain" \ --arg policy "$policy" \ --argjson pct "$pct" \ --arg rua "$DMARC_RUA" \ '{ "Comment": ("DMARC tighten to " + $policy + " pct=" + ($pct|tostring) + " for " + $domain), "Changes": [{ "Action": "UPSERT", "ResourceRecordSet": { "Name": ("_dmarc." + $domain + "."), "Type": "TXT", "TTL": 300, "ResourceRecords": [{"Value": ("\"v=DMARC1; p=" + $policy + "; pct=" + ($pct|tostring) + "; rua=mailto:" + $rua + "; fo=1\"")}] } }] }') apply_change_batch "$zone_id" "$batch" "DMARC skärpning: $domain → p=$policy pct=$pct" done log "=== FAS 6 klar ===" } # --------------------------------------------------------------------------- # Hjälptext och dispatcher # --------------------------------------------------------------------------- usage() { cat < [--dry-run] FASER: fas1 ✅ Partnerdomäner: SPF -all + DMARC reject (10 domäner) fas2 ✅ EU Nivå 2: MX + SPF + DKIM + DMARC p=none (30 domäner) OBS: Fyll i DKIM_PUBKEY-variabeln före körning! fas3 ✅ pixdrift.com: SPF -all + DMARC reject fas4a ✅ Inspektion/verifiering av döda domäner (read-only) fas4b ⚠️ corpfitt+hypbit MX-borttagning (kräver Erik-OK + prompt) fas5 ✅ Duplikatzon-inspektion (read-only) fas6 ⚠️ DMARC-skärpning aktiva maildomäner (kräver Erik-OK + prompt) FÖRE KÖRNING: 1. Sätt DKIM_PUBKEY i skript-toppen (hämta från Mailu Admin → Domains → DKIM) 2. Bekräfta att AWS-profil är korrekt: aws sts get-caller-identity 3. Fas2 och fas4b: Verifiera att catch-all konfigurerats i Mailu FÖR DNS-ändring EXEMPEL: # Kontrollera AWS-åtkomst aws sts get-caller-identity # Kör fas1 (partnerdomäner, säker) bash c3-apply-dns.sh fas1 # Inspektera duplikatzoner (read-only) bash c3-apply-dns.sh fas5 # Kör fas4a (verifiera döda domäner, read-only) bash c3-apply-dns.sh fas4a EOF } main() { local cmd="${1:-help}" shift || true case "$cmd" in fas1) check_deps; fas1_partner_domains ;; fas2) check_deps; fas2_eu_level2_domains ;; fas3) check_deps; fas3_pixdrift ;; fas4a) fas4a_verify_dead_domains_mail ;; fas4b) check_deps; fas4b_decommission_dead_mx ;; fas5) fas5_inspect_duplicate_zones ;; fas6) check_deps; fas6_dmarc_tighten_active_domains ;; help|--help|-h) usage ;; *) echo "Okänt kommando: $cmd" usage exit 1 ;; esac } main "$@"