Validare DKIM nella tua CI/CD: bloccare i record rotti prima della produzione
Di CaptainDNS
Pubblicato il 19 maggio 2026

- Un record DKIM rotto raramente fallisce i test funzionali ma rompe la deliverability dal primo email post-deploy
- I linter locali rilevano la sintassi; l'API CaptainDNS
/v1/dkim/validateverifica lo stato reale pubblicato nel DNS - GitHub Actions, GitLab CI e gli hook Terraform possono bloccare un merge o un
applysu uno score DKIM insufficiente - La validazione pre-commit risparmia minuti CI ma non sostituisce un check sullo stato DNS pubblicato
- Combinare le validazioni SPF, DKIM e DMARC in uno stesso stage evita regressioni a cascata
Tieni il tuo DNS versionato in Git, le tue rotazioni di chiavi DKIM passano da una pull request e tutto va bene finché un giorno Gmail inizia a classificare le tue mail come spam. La causa: un tag p= troncato da un provider DNS, oppure una firma che non corrisponde più al selettore pubblicato. Il bug non appare in terraform plan, né nei test di integrazione applicativi.
La risposta sta in una parola: shift-left. Invece di scoprire il problema in produzione, valida ogni modifica DKIM prima del merge, poi prima dell'apply, poi dopo il deploy. Questo articolo mostra come integrare questi controlli in GitHub Actions, GitLab CI, Terraform e un hook pre-commit, con esempi copy-paste.
Per verificare rapidamente la sintassi di un record, il DKIM Syntax Check fornisce un'API pubblica riutilizzabile in qualsiasi pipeline.
Perché un record DKIM rotto arriva in produzione
Quattro cause principali spiegano perché un DKIM invalido supera le difese standard di un deployment DNS.
Modifica manuale dei record DNS. Un operatore copia una chiave pubblica da un provider transazionale, dimentica un carattere, oppure scambia due blocchi base64. Senza validazione sintattica, il record viene pubblicato così com'è.
Rotazione di chiave senza validazione. La nuova coppia viene generata, la chiave pubblica codificata, ma la pipeline di rotazione non ricontrolla che la stringa base64 resti coerente dopo l'export. Un troncamento a 254 caratteri in alcuni pannelli di amministrazione basta a rompere la firma.
Provider DNS che fa split in due chunk. Una stringa TXT superiore a 255 caratteri deve essere frammentata dal server DNS, che la concatena con virgolette. Alcuni provider (o alcune UI) tagliano la stringa senza virgolette: il resolver riceve allora due stringhe distinte, e la chiave ricostruita è invalida.
Nessun canary post-deploy. Il bug appare solo sulle prime email inviate dopo la modifica. Quando l'informazione risale (report DMARC, ticket di supporto), migliaia di messaggi sono già finiti in spam.
Shift-left: validare prima del merge
Il principio è semplice: qualsiasi modifica di un record DNS legato alla mail passa per una validazione automatica in pull request. Hai due opzioni per validare.
Linter locale open source. Strumenti come dkim-checker (CLI Go basata su github.com/emersion/go-msgauth/dkim) o script fatti in casa verificano la sintassi di un record, la coerenza dei tag (v=DKIM1, k=rsa, p=...) e la lunghezza della chiave. Vantaggio: zero dipendenza di rete, veloce. Svantaggio: non sa se la chiave è effettivamente pubblicata e accessibile.
Linter cloud via API. L'endpoint CaptainDNS /v1/dkim/validate accetta un dominio e un selettore, interroga il DNS pubblicato, parsa il record e restituisce uno score normalizzato, uno stato (valid, invalid, warning) e raccomandazioni. Vantaggio: valida lo stato reale osservato dall'esterno, come un MX che riceve la mail. Svantaggio: dipende dalla rete, dal quota API e dal DNS.
| Criterio | Linter locale | API CaptainDNS |
|---|---|---|
Sintassi (v=, k=, p=) | Sì | Sì |
| Chiave pubblicata nel DNS | No | Sì |
| Dimensione effettiva della chiave | Parziale | Sì |
| Score normalizzato | No | Sì (calcolato lato backend) |
| Dipendenza di rete | No | Sì |
| Caso d'uso | pre-commit, lint sintassi | validazione PR, post-deploy |
Nota importante: lo score, le soglie e le raccomandazioni sono calcolati dall'API CaptainDNS lato backend. Il client CI consuma un valore già normalizzato, non rifà alcun calcolo.
Combina le due cose: linter locale in pre-commit per iterazioni rapide, API in CI per la validazione finale prima del merge.
Pipeline GitHub Actions per validare una pull request DNS
Ecco un workflow che si attiva a ogni pull request che modifica file DNS. Estrae i record DKIM, chiama l'API CaptainDNS e blocca il merge se lo stato è invalid o se lo score è inferiore a 50.
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
La chiave API si genera dalla tua console CaptainDNS, poi si memorizza come secret GitHub (Settings > Secrets and variables > Actions). Usa una chiave con scope dkim:read per limitare il raggio d'azione in caso di leak.

GitLab CI: pipeline con stage validate
Su GitLab la logica resta identica. Il file .gitlab-ci.yml qui sotto aggiunge una cache per evitare di rivalidare record invariati e invia una notifica Slack in caso di fallimento.
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/**/*
La cache GitLab conserva le risposte tra due run sullo stesso branch. Se un selettore non è cambiato, l'API viene comunque chiamata (lo stato DNS può evolvere indipendentemente dal codice), ma lo storico permette di tracciare le regressioni.
Terraform pre-apply: validazione prima della pubblicazione DNS
Quando il tuo DNS è gestito da Terraform, Pulumi o OpenTofu, la validazione logica avviene prima dell'apply. L'hook qui sotto estrae i TXT modificati dal piano JSON, li valida via l'API CaptainDNS e blocca l'apply in caso di fallimento.
#!/usr/bin/env bash
# scripts/dkim-preapply.sh
set -euo pipefail
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# Estrarre i record TXT DKIM modificati
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
Aggancia questo script a un wrapper make plan && make validate && make apply, o allo stage Atlantis se usi questo pattern GitOps. La stessa logica vale per Pulumi (via pulumi preview --json) e OpenTofu, dato che il formato JSON del piano è compatibile.
Lint locale in pre-commit
Per risparmiare minuti CI, aggiungi un hook pre-commit che valida la sintassi dei record DKIM ancora prima del push. Il framework pre-commit.com permette di orchestrare più hook Python o bash.
# .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
Lo script dkim-lint.sh può appoggiarsi su un parser locale Go o Python che verifica la presenza dei tag obbligatori (v=DKIM1, p=...) e la validità base64 della chiave pubblica. Basta per intercettare l'80 % degli errori di copia-incolla.
Limite da conoscere: un lint pre-commit non può verificare lo stato reale pubblicato nel DNS, né i conflitti con un selettore già attivo. La validazione API in CI resta indispensabile per questi casi.
Test di integrazione post-deploy
Una volta applicato il DNS, verifica che la catena end-to-end funzioni. Uno smoke test invia una mail reale e controlla l'header Authentication-Results.
#!/usr/bin/env bash
# scripts/dkim-smoke-test.sh
set -euo pipefail
# Inviare una mail di test via swaks
swaks --to test-canary@captaindns.com \
--from noreply@captaindns.com \
--server smtp.captaindns.com \
--header "Subject: DKIM canary $(date +%s)" \
--body "Canary message"
# Attendere 60 secondi che la mail arrivi
sleep 60
# Verificare l'header Authentication-Results via IMAP o un endpoint dedicato
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"
# Rollback automatico
terraform apply -auto-approve -var "dkim_selector=previous"
exit 1
fi
Puoi anche usare servizi sintetici come Mail-Tester o GlockApps, che espongono un'API per recuperare i risultati di autenticazione di una mail inviata a un indirizzo usa-e-getta. Il ciclo di feedback diventa: invio della mail, lettura del risultato, poi rollback DNS se DKIM fallisce.
Per una verifica puntuale senza pipeline, il DKIM Record Check interroga il DNS e mostra lo stato completo del selettore in pochi secondi.
Combinare con SPF / DMARC
Validare DKIM da solo non basta. Un DKIM valid ma uno SPF rotto produce un dmarc=fail sulle mail non firmate da DKIM. Un DMARC mal allineato peggiora la deliverability anche con un DKIM perfetto.
La pipeline completa deve quindi validare i tre protocolli in un unico stage:
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 # opzionale
Ogni script chiama l'endpoint corrispondente: /v1/spf/validate, /v1/dkim/validate, /v1/dmarc/validate. Condividere il client HTTP, il parsing della risposta e le soglie in un modulo comune evita la duplicazione.

Piano d'azione raccomandato
- Inventario: elenca i tuoi selettori DKIM attivi per dominio e sottodominio
- Provisionare una chiave API CaptainDNS con scope
dkim:read+spf:read+dmarc:read - Aggiungere l'hook pre-commit per il lint sintassi locale
- Mettere in piedi un workflow CI che valida i file DNS modificati in PR
- Collegare Terraform a un hook pre-apply che blocca i record invalidi
- Attivare un canary post-deploy: una mail di test inviata dopo ogni
apply - Estendere agli altri protocolli: SPF, DMARC, BIMI nello stesso stage di validazione
FAQ
Perché validare DKIM nella CI/CD?
Un record DKIM rotto non provoca un errore visibile lato DNS. Rompe solo la deliverability delle mail, cosa che si constata con qualche ora di ritardo via i report DMARC o le segnalazioni degli utenti. Validare in CI permette di bloccare il merge prima che migliaia di mail partano con una firma invalida.
Qual è la differenza tra validazione locale e validazione via API?
Un linter locale verifica solo la sintassi di un record (tag, formato base64). L'API CaptainDNS interroga il DNS pubblicato, parsa il record come lo vedrebbe un ricevente e restituisce uno score calcolato lato backend. Il locale è veloce ma cieco allo stato reale pubblicato.
Come integrare la validazione DKIM in GitHub Actions?
Crea un workflow attivato sulle pull request che modificano la cartella DNS. Recupera le coppie dominio/selettore, chiama l'endpoint /v1/dkim/validate via curl, parsa la risposta con jq e fai fallire il job se lo stato è invalid o se lo score è inferiore alla tua soglia (50 è un valore di partenza ragionevole).
Si può bloccare un terraform apply su un DKIM invalido?
Sì. Esegui terraform plan -out=tfplan poi terraform show -json per estrarre i record TXT modificati. Valida ogni record via l'API CaptainDNS prima di lanciare terraform apply. Se la validazione fallisce, esci dallo script con un codice non zero per bloccare l'apply.
Bisogna validare anche SPF e DMARC nella stessa pipeline?
Sì. DKIM, SPF e DMARC sono interdipendenti. Un DKIM valido isolato non garantisce la deliverability se SPF fallisce o se DMARC non è allineato. Valida i tre nello stesso stage CI, con fail-fast sul primo fallimento o un report consolidato.
Quali secret configurare per l'API CaptainDNS in CI?
Basta un solo secret: CAPTAINDNS_API_KEY. Provisionalo dalla console, dagli lo scope sugli endpoint necessari (lettura DKIM/SPF/DMARC) e memorizzalo nel secret manager della tua CI. Evita le chiavi admin in una pipeline CI, che ha solo bisogno di lettura.
Cosa fare se il record passa localmente ma fallisce dopo il deploy?
Tre cause frequenti: propagazione DNS incompleta (aspetta 30 minuti), provider DNS che ha fatto split del TXT senza virgolette (verifica con dig +short TXT selettore._domainkey.dominio), o cache negativa su un resolver intermedio. Lo smoke test post-deploy deve riprovare con backoff esponenziale prima di scatenare un rollback.
Glossario
- Shift-left: pratica che consiste nello spostare i controlli di qualità verso le fasi a monte del ciclo di sviluppo (PR, pre-commit), piuttosto che lasciarli in produzione.
- Pre-commit hook: script eseguito localmente prima di ogni commit Git, che può bloccare il commit in caso di errore.
- Pre-apply hook: script eseguito prima di
terraform apply(o equivalente Pulumi/OpenTofu) che valida le modifiche pianificate. - Canary: test post-deployment con un volume ridotto che rileva le regressioni prima che colpiscano tutto il traffico.
- Score DKIM: valore normalizzato (0 a 100) calcolato dall'API CaptainDNS a partire dalla sintassi, dalla dimensione della chiave, dall'algoritmo e dalle raccomandazioni RFC.
- ARC (Authenticated Received Chain): protocollo RFC 8617 che preserva i risultati di autenticazione DKIM/SPF durante l'inoltro delle email.
Testa la tua integrazione con il DKIM Syntax Check: lo strumento espone la stessa API utilizzata in CI/CD, ideale per validare un record prima di aggiungerlo alla tua pipeline.
Guide DKIM correlate da consultare
- Guida completa DKIM: funzionamento, configurazione DNS, RSA vs Ed25519 e integrazione con SPF/DMARC.
- DKIM fail: cause e correzioni: diagnosi delle 7 cause principali di un
dkim=faile le loro correzioni passo passo.


