DKIM in Ihrer CI/CD validieren: defekte Records vor der Produktion abfangen
Von CaptainDNS
Veröffentlicht am 19. Mai 2026

- Ein defekter DKIM-Record fällt selten in funktionalen Tests auf, bricht aber die Zustellbarkeit ab der ersten E-Mail nach dem Deploy
- Lokale Linter prüfen die Syntax; die CaptainDNS-API
/v1/dkim/validateprüft den realen, im DNS veröffentlichten Zustand - GitHub Actions, GitLab CI und Terraform-Hooks können einen Merge oder ein
applybei unzureichendem DKIM-Score blockieren - Die pre-commit-Validierung spart CI-Zeit, ersetzt aber keine Prüfung des im DNS veröffentlichten Zustands
- Die Bündelung der SPF-, DKIM- und DMARC-Validierungen in einem einzigen Stage verhindert kaskadierende Regressionen
Sie versionieren Ihr DNS in Git, Ihre DKIM-Schlüsselrotationen laufen über einen Pull Request, und alles geht gut, bis Gmail Ihre Mails plötzlich in den Spam einsortiert. Die Ursache: ein vom DNS-Provider abgeschnittenes p=-Tag oder eine Signatur, die nicht mehr zum veröffentlichten Selektor passt. Der Bug zeigt sich weder in terraform plan noch in den Anwendungs-Integrationstests.
Die Antwort steckt in einem Wort: shift-left. Statt das Problem in der Produktion zu entdecken, validieren Sie jede DKIM-Änderung vor dem Merge, dann vor dem apply, dann nach dem Deploy. Dieser Artikel zeigt, wie Sie diese Prüfungen in GitHub Actions, GitLab CI, Terraform und einen pre-commit-Hook einbinden, mit copy-paste-fertigen Beispielen.
Für eine schnelle Syntaxprüfung eines Eintrags bietet der DKIM Syntax Check eine öffentliche API, die in jeder Pipeline wiederverwendbar ist.
Warum ein defekter DKIM-Record in der Produktion landet
Vier Hauptursachen erklären, wie ein ungültiger DKIM-Record die Standardabwehrmechanismen eines DNS-Deployments umgeht.
Manuelle Bearbeitung der DNS-Records. Ein Operator kopiert einen öffentlichen Schlüssel von einem Transaktions-Provider, vergisst ein Zeichen oder vertauscht zwei base64-Blöcke. Ohne Syntaxvalidierung wird der Eintrag genau so veröffentlicht.
Schlüsselrotation ohne Validierung. Das neue Schlüsselpaar wird erzeugt, der öffentliche Schlüssel kodiert, aber die Rotations-Pipeline prüft nicht erneut, ob die base64-Zeichenkette nach dem Export konsistent bleibt. Eine Abschneidung bei 254 Zeichen in manchen Admin-Oberflächen genügt, um die Signatur zu zerstören.
Ein DNS-Provider, der in zwei Chunks splittet. Eine TXT-Zeichenkette über 255 Zeichen muss vom DNS-Server fragmentiert und mit Anführungszeichen wieder zusammengesetzt werden. Manche Provider (oder UIs) splitten die Zeichenkette ohne Anführungszeichen: der Resolver erhält zwei getrennte Zeichenketten, und der rekonstruierte Schlüssel ist ungültig.
Kein Post-Deploy-Canary. Der Bug zeigt sich erst bei den ersten Mails nach der Änderung. Bis die Information ankommt (DMARC-Report, Support-Ticket), wurden bereits tausende Nachrichten als Spam eingestuft.
Shift-left: vor dem Merge validieren
Das Prinzip ist einfach: jede Änderung an einem mailbezogenen DNS-Record durchläuft eine automatische Validierung im Pull Request. Sie haben zwei Optionen für die Validierung.
Lokaler Open-Source-Linter. Werkzeuge wie dkim-checker (eine Go-CLI auf Basis von github.com/emersion/go-msgauth/dkim) oder eigene Skripte prüfen die Syntax eines Eintrags, die Konsistenz der Tags (v=DKIM1, k=rsa, p=...) und die Schlüssellänge. Vorteil: keine Netzwerkabhängigkeit, schnell. Nachteil: keine Aussage darüber, ob der Schlüssel tatsächlich veröffentlicht und erreichbar ist.
Cloud-Linter über API. Der CaptainDNS-Endpoint /v1/dkim/validate akzeptiert eine Domain und einen Selektor, fragt das veröffentlichte DNS ab, parst den Eintrag und liefert einen normalisierten Score, einen Zustand (valid, invalid, warning) und Empfehlungen. Vorteil: validiert den realen, von außen beobachteten Zustand, genau wie ein empfangender MX. Nachteil: hängt vom Netzwerk, vom API-Kontingent und vom DNS ab.
| Kriterium | Lokaler Linter | CaptainDNS-API |
|---|---|---|
Syntax (v=, k=, p=) | Ja | Ja |
| Schlüssel im DNS veröffentlicht | Nein | Ja |
| Effektive Schlüsselgröße | Teilweise | Ja |
| Normalisierter Score | Nein | Ja (im Backend berechnet) |
| Netzwerkabhängigkeit | Nein | Ja |
| Anwendungsfall | pre-commit, Syntax-Lint | PR-Validierung, post-deploy |
Wichtiger Hinweis: Score, Schwellen und Empfehlungen werden von der CaptainDNS-API im Backend berechnet. Der CI-Client konsumiert einen bereits normalisierten Wert und führt keine eigene Berechnung durch.
Kombinieren Sie beides: lokaler Linter im pre-commit für schnelle Iterationen, API in der CI für die finale Validierung vor dem Merge.
GitHub-Actions-Pipeline zur Validierung eines DNS-Pull-Requests
Hier ein Workflow, der bei jedem Pull Request mit Änderungen an DNS-Dateien ausgelöst wird. Er extrahiert die DKIM-Records, ruft die CaptainDNS-API auf und blockiert den Merge, wenn der Zustand invalid oder der Score kleiner als 50 ist.
name: Validate DKIM records
on:
pull_request:
paths:
- 'dns/**.tf'
- 'dns/**.yaml'
jobs:
validate-dkim:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Detect changed DNS files
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
dns:
- 'dns/**'
- name: Validate DKIM via CaptainDNS API
if: steps.changes.outputs.dns == 'true'
env:
CDNS_API_KEY: ${{ secrets.CAPTAINDNS_API_KEY }}
run: |
set -euo pipefail
for entry in $(yq '.dkim_selectors[]' dns/email.yaml); do
domain=$(echo "$entry" | yq '.domain')
selector=$(echo "$entry" | yq '.selector')
response=$(curl -sS -X POST \
-H "Authorization: Bearer ${CDNS_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"domain\":\"${domain}\",\"selector\":\"${selector}\"}" \
https://api.captaindns.com/public/v1/dkim/validate)
state=$(echo "$response" | jq -r '.state')
score=$(echo "$response" | jq -r '.score')
echo "Selector ${selector} for ${domain}: state=${state}, score=${score}"
if [ "$state" = "invalid" ] || [ "$score" -lt 50 ]; then
echo "::error::DKIM validation failed for ${selector}._domainkey.${domain}"
exit 1
fi
done
Der API-Schlüssel wird über Ihre CaptainDNS-Konsole bereitgestellt und anschließend als GitHub-Secret hinterlegt (Settings > Secrets and variables > Actions). Verwenden Sie einen Schlüssel mit dem Scope dkim:read, um den Schaden im Falle eines Leaks zu begrenzen.

GitLab CI: Pipeline mit validate-Stage
In GitLab bleibt die Logik identisch. Die folgende .gitlab-ci.yml ergänzt einen Cache, um unveränderte Records nicht erneut zu validieren, und schickt im Fehlerfall eine Slack-Benachrichtigung.
stages:
- validate
- apply
validate-dkim:
stage: validate
image: alpine:3.20
before_script:
- apk add --no-cache curl jq yq bash
cache:
key: dkim-validation-$CI_COMMIT_REF_SLUG
paths:
- .dkim-cache/
script:
- mkdir -p .dkim-cache
- |
bash -c '
set -euo pipefail
for entry in $(yq ".dkim_selectors[]" dns/email.yaml); do
domain=$(echo "$entry" | yq ".domain")
selector=$(echo "$entry" | yq ".selector")
cache_key=".dkim-cache/${domain}_${selector}.json"
response=$(curl -sS -X POST \
-H "Authorization: Bearer ${CAPTAINDNS_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"domain\":\"${domain}\",\"selector\":\"${selector}\"}" \
https://api.captaindns.com/public/v1/dkim/validate)
echo "$response" > "$cache_key"
state=$(echo "$response" | jq -r ".state")
score=$(echo "$response" | jq -r ".score")
if [ "$state" = "invalid" ] || [ "$score" -lt 50 ]; then
curl -X POST -H "Content-Type: application/json" \
-d "{\"text\":\"DKIM validation failed: ${selector}._domainkey.${domain} (score=${score})\"}" \
"$SLACK_WEBHOOK_URL"
exit 1
fi
done
'
only:
changes:
- dns/**/*
Der GitLab-Cache hält die Antworten zwischen zwei Runs auf demselben Branch vor. Wenn sich ein Selektor nicht geändert hat, wird die API trotzdem aufgerufen (der DNS-Zustand kann sich unabhängig vom Code ändern), aber die Historie erlaubt das Nachvollziehen von Regressionen.
Terraform pre-apply: Validierung vor der DNS-Veröffentlichung
Wenn Ihr DNS von Terraform, Pulumi oder OpenTofu verwaltet wird, findet die logische Validierung vor apply statt. Der folgende Hook extrahiert die geänderten TXT-Records aus dem JSON-Plan, validiert sie über die CaptainDNS-API und blockiert das Apply bei Fehler.
#!/usr/bin/env bash
# scripts/dkim-preapply.sh
set -euo pipefail
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# Geänderte DKIM-TXT-Records extrahieren
jq -r '
.resource_changes[]
| select(.type == "cloudflare_record" or .type == "aws_route53_record")
| select(.change.after.type == "TXT")
| select(.change.after.name | test("_domainkey"))
| "\(.change.after.name)|\(.change.after.value // (.change.after.records | join("")))"
' tfplan.json > dkim-changes.txt
while IFS='|' read -r fqdn value; do
# fqdn: selector._domainkey.captaindns.com
selector="${fqdn%%._domainkey.*}"
domain="${fqdn#*._domainkey.}"
response=$(curl -sS -X POST \
-H "Authorization: Bearer ${CDNS_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"domain\":\"${domain}\",\"selector\":\"${selector}\",\"record\":\"${value}\"}" \
https://api.captaindns.com/public/v1/dkim/validate)
state=$(echo "$response" | jq -r '.state')
if [ "$state" = "invalid" ]; then
echo "Pre-apply DKIM check failed: ${fqdn}"
echo "$response" | jq '.recommendations'
exit 1
fi
done < dkim-changes.txt
terraform apply tfplan.binary
Hängen Sie dieses Skript an ein make plan && make validate && make apply oder an die Atlantis-Stage, wenn Sie dieses GitOps-Muster nutzen. Die gleiche Logik gilt für Pulumi (über pulumi preview --json) und OpenTofu, da das JSON-Plan-Format kompatibel ist.
Lokales Lint im pre-commit
Um CI-Minuten zu sparen, fügen Sie einen pre-commit-Hook hinzu, der die Syntax der DKIM-Records noch vor dem Push validiert. Das Framework pre-commit.com ermöglicht die Orchestrierung mehrerer Python- oder Bash-Hooks.
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: dkim-syntax-lint
name: DKIM syntax lint
entry: scripts/dkim-lint.sh
language: script
files: ^dns/.*\.(tf|yaml)$
pass_filenames: true
Das Skript dkim-lint.sh kann auf einen lokalen Go- oder Python-Parser zurückgreifen, der das Vorhandensein der Pflichttags (v=DKIM1, p=...) und die base64-Gültigkeit des öffentlichen Schlüssels prüft. Das reicht, um 80 % der Copy-Paste-Fehler abzufangen.
Wichtige Einschränkung: ein pre-commit-Lint kann weder den im DNS veröffentlichten Zustand prüfen noch Konflikte mit einem bereits aktiven Selektor. Die API-Validierung in der CI bleibt für diese Fälle unverzichtbar.
Integrationstests nach dem Deploy
Sobald das DNS angewendet wurde, prüfen Sie, dass die End-to-End-Kette funktioniert. Ein Smoke-Test schickt eine echte Mail und prüft den Header Authentication-Results.
#!/usr/bin/env bash
# scripts/dkim-smoke-test.sh
set -euo pipefail
# Eine Test-Mail über swaks senden
swaks --to test-canary@captaindns.com \
--from noreply@captaindns.com \
--server smtp.captaindns.com \
--header "Subject: DKIM canary $(date +%s)" \
--body "Canary message"
# 60 Sekunden auf Zustellung warten
sleep 60
# Den Authentication-Results-Header über IMAP oder einen eigenen Endpoint prüfen
result=$(curl -sS "https://canary.captaindns.com/last-mail/auth-results")
if ! echo "$result" | grep -q "dkim=pass"; then
echo "Canary DKIM check FAILED: $result"
# Automatischer Rollback
terraform apply -auto-approve -var "dkim_selector=previous"
exit 1
fi
Sie können auch synthetische Dienste wie Mail-Tester oder GlockApps verwenden, die eine API zum Abruf der Authentifizierungsergebnisse einer an eine Wegwerf-Adresse gesendeten Mail bereitstellen. Die Feedback-Schleife wird: Mail senden, Ergebnis lesen, dann DNS zurückrollen, wenn DKIM scheitert.
Für eine punktuelle Prüfung ohne Pipeline fragt der DKIM Record Check das DNS ab und zeigt den vollständigen Zustand des Selektors in wenigen Sekunden.
Mit SPF / DMARC kombinieren
DKIM allein zu validieren reicht nicht. Ein valid DKIM bei kaputtem SPF führt zu dmarc=fail für Mails, die nicht durch DKIM signiert sind. Ein schlecht ausgerichtetes DMARC verschlechtert die Zustellbarkeit selbst bei einem perfekten DKIM.
Eine vollständige Pipeline muss daher die drei Protokolle in derselben Stage validieren:
validate-email-auth:
stage: validate
script:
- bash scripts/validate-spf.sh dns/email.yaml
- bash scripts/validate-dkim.sh dns/email.yaml
- bash scripts/validate-dmarc.sh dns/email.yaml
- bash scripts/validate-bimi.sh dns/email.yaml # optional
Jedes Skript ruft den entsprechenden Endpoint auf: /v1/spf/validate, /v1/dkim/validate, /v1/dmarc/validate. Die Bündelung von HTTP-Client, Response-Parsing und Schwellen in einem gemeinsamen Modul vermeidet Duplikation.

Empfohlener Ausrollungsplan
- Inventar: Listen Sie Ihre aktiven DKIM-Selektoren pro Domain und Subdomain auf
- CaptainDNS-API-Schlüssel mit Scope
dkim:read+spf:read+dmarc:readbereitstellen - pre-commit-Hook hinzufügen für das lokale Syntax-Lint
- CI-Workflow einrichten, der geänderte DNS-Dateien im PR validiert
- Terraform an einen pre-apply-Hook anschließen, der ungültige Records blockiert
- Post-Deploy-Canary aktivieren: eine Test-Mail nach jedem
apply - Auf weitere Protokolle ausweiten: SPF, DMARC, BIMI in derselben Validierungs-Stage
FAQ
Warum DKIM in der CI/CD validieren?
Ein defekter DKIM-Record erzeugt keinen sichtbaren Fehler auf DNS-Seite. Er bricht nur die Zustellbarkeit der Mails, was sich erst nach einigen Stunden über DMARC-Reports oder Nutzerrückmeldungen zeigt. Eine Validierung in der CI ermöglicht es, den Merge zu blockieren, bevor tausende Mails mit einer ungültigen Signatur versendet werden.
Was ist der Unterschied zwischen lokaler Validierung und API-Validierung?
Ein lokaler Linter prüft nur die Syntax eines Eintrags (Tags, base64-Format). Die CaptainDNS-API fragt das veröffentlichte DNS ab, parst den Eintrag genau so, wie ein Empfänger ihn sieht, und liefert einen im Backend berechneten Score zurück. Lokal ist schnell, aber blind für den real veröffentlichten Zustand.
Wie integriere ich die DKIM-Validierung in GitHub Actions?
Erstellen Sie einen Workflow, der bei Pull Requests auf den DNS-Ordner ausgelöst wird. Holen Sie die Paare aus Domain und Selektor, rufen Sie den Endpoint /v1/dkim/validate per curl auf, parsen Sie die Antwort mit jq und lassen Sie den Job scheitern, wenn der Zustand invalid oder der Score unter Ihrem Schwellenwert liegt (50 ist ein sinnvoller Ausgangspunkt).
Kann man ein terraform apply bei ungültigem DKIM blockieren?
Ja. Führen Sie terraform plan -out=tfplan aus, dann terraform show -json, um die geänderten TXT-Records zu extrahieren. Validieren Sie jeden Eintrag über die CaptainDNS-API, bevor Sie terraform apply starten. Wenn die Validierung fehlschlägt, verlassen Sie das Skript mit einem Exit-Code ungleich null, um das Apply zu blockieren.
Sollte man auch SPF und DMARC in derselben Pipeline validieren?
Ja. DKIM, SPF und DMARC sind voneinander abhängig. Ein isoliertes gültiges DKIM garantiert keine Zustellbarkeit, wenn SPF scheitert oder DMARC nicht ausgerichtet ist. Validieren Sie die drei in derselben CI-Stage, mit fail-fast beim ersten Fehler oder einem konsolidierten Report.
Welche Secrets sind für die CaptainDNS-API in der CI nötig?
Ein einziges Secret reicht: CAPTAINDNS_API_KEY. Provisionieren Sie ihn aus der Konsole, scopen Sie ihn auf die nötigen Endpoints (lesen DKIM/SPF/DMARC) und speichern Sie ihn im Secret-Manager Ihrer CI. Vermeiden Sie Admin-Keys in einer CI-Pipeline, die nur Lesezugriff benötigt.
Was tun, wenn der Record lokal passt, aber nach dem Deploy scheitert?
Drei häufige Ursachen: unvollständige DNS-Propagation (30 Minuten warten), DNS-Provider, der den TXT ohne Anführungszeichen gesplittet hat (mit dig +short TXT selektor._domainkey.domain prüfen), oder negativer Cache auf einem zwischengeschalteten Resolver. Der Post-Deploy-Smoke-Test sollte mit exponentiellem Backoff erneut versuchen, bevor ein Rollback ausgelöst wird.
Glossar
- Shift-left: Praxis, Qualitätskontrollen in frühere Phasen des Entwicklungszyklus zu verlagern (PR, pre-commit), statt sie in der Produktion zu belassen.
- Pre-commit-Hook: lokal vor jedem Git-Commit ausgeführtes Skript, das den Commit bei Fehler blockieren kann.
- Pre-apply-Hook: vor
terraform apply(oder dem Pulumi/OpenTofu-Äquivalent) ausgeführtes Skript, das die geplanten Änderungen validiert. - Canary: Test nach dem Deployment mit reduziertem Volumen, der Regressionen erkennt, bevor sie den gesamten Traffic betreffen.
- DKIM-Score: normalisierter Wert (0 bis 100), den die CaptainDNS-API aus Syntax, Schlüsselgröße, Algorithmus und RFC-Empfehlungen berechnet.
- ARC (Authenticated Received Chain): Protokoll nach RFC 8617, das die DKIM/SPF-Authentifizierungsergebnisse beim Weiterleiten von E-Mails bewahrt.
Testen Sie Ihre Integration mit dem DKIM Syntax Check: das Werkzeug stellt dieselbe API bereit, die in der CI/CD verwendet wird, ideal um einen Record vor dem Hinzufügen zu Ihrer Pipeline zu validieren.
Verwandte DKIM-Leitfäden zum Weiterlesen
- Vollständiger DKIM-Leitfaden: Funktionsweise, DNS-Konfiguration, RSA vs Ed25519 und Integration mit SPF/DMARC.
- DKIM fail: Ursachen und Korrekturen: Diagnose der 7 Hauptursachen für
dkim=failund ihre schrittweisen Korrekturen.


