Validar DKIM no seu CI/CD: detectar registros quebrados antes da produção
Por CaptainDNS
Publicado em 19 de maio de 2026

- Um registro DKIM quebrado raramente falha nos testes funcionais mas quebra a deliverability já no primeiro e-mail pós-deploy
- Os linters locais detectam a sintaxe; a API CaptainDNS
/v1/dkim/validateverifica o estado real publicado no DNS - GitHub Actions, GitLab CI e hooks do Terraform podem bloquear um merge ou um
applyem um score DKIM insuficiente - A validação pre-commit economiza minutos de CI mas não substitui uma verificação do estado DNS publicado
- Combinar as validações SPF, DKIM e DMARC em um único stage evita regressões em cascata
Você versiona o seu DNS no Git, as suas rotações de chaves DKIM passam por uma pull request, e tudo vai bem até o dia em que o Gmail começa a marcar os seus e-mails como spam. A causa: um tag p= truncado por um provider DNS, ou uma assinatura que não corresponde mais ao seletor publicado. O bug não aparece em terraform plan, nem nos testes de integração da aplicação.
A resposta cabe em uma palavra: shift-left. Em vez de descobrir o problema em produção, valide cada modificação DKIM antes do merge, depois antes do apply, depois após o deploy. Este artigo mostra como integrar esses controles no GitHub Actions, GitLab CI, Terraform e em um hook pre-commit, com exemplos copy-paste.
Para verificar rapidamente a sintaxe de um registro, o DKIM Syntax Check fornece uma API pública reutilizável em qualquer pipeline.
Por que um registro DKIM quebrado chega à produção
Quatro causas principais explicam por que um DKIM inválido atravessa as defesas padrão de um deployment DNS.
Edição manual dos registros DNS. Um operador cola uma chave pública de um provider transacional, esquece um caractere, ou troca dois blocos base64. Sem validação sintática, o registro é publicado tal como está.
Rotação de chave sem validação. O novo par é gerado, a chave pública é codificada, mas o pipeline de rotação não revalida que a string base64 permaneça coerente após a exportação. Um truncamento a 254 caracteres em alguns painéis de administração basta para quebrar a assinatura.
Provider DNS que faz split em dois chunks. Uma string TXT com mais de 255 caracteres deve ser fragmentada pelo servidor DNS, que a concatena com aspas. Alguns providers (ou algumas UIs) cortam a string sem aspas: o resolver recebe então duas strings distintas, e a chave reconstituída é inválida.
Sem canary pós-deploy. O bug só aparece nos primeiros e-mails enviados após a mudança. Quando a informação chega (relatório DMARC, ticket de suporte), milhares de mensagens já foram classificadas como spam.
Shift-left: validar antes do merge
O princípio é simples: qualquer modificação de um registro DNS ligado ao e-mail passa por uma validação automática em pull request. Você tem duas opções para validar.
Linter local open source. Ferramentas como dkim-checker (CLI Go baseada em github.com/emersion/go-msgauth/dkim) ou scripts caseiros verificam a sintaxe de um registro, a coerência das tags (v=DKIM1, k=rsa, p=...) e o tamanho da chave. Vantagem: zero dependência de rede, rápido. Desvantagem: não sabe se a chave está realmente publicada e acessível.
Linter cloud via API. O endpoint CaptainDNS /v1/dkim/validate aceita um domínio e um seletor, consulta o DNS publicado, parseia o registro e retorna um score normalizado, um estado (valid, invalid, warning) e recomendações. Vantagem: valida o estado real observado do exterior, como um MX que recebesse o e-mail. Desvantagem: depende da rede, do limite da API e do DNS.
| Critério | Linter local | API CaptainDNS |
|---|---|---|
Sintaxe (v=, k=, p=) | Sim | Sim |
| Chave publicada no DNS | Não | Sim |
| Tamanho efetivo da chave | Parcial | Sim |
| Score normalizado | Não | Sim (calculado no backend) |
| Dependência de rede | Não | Sim |
| Caso de uso | pre-commit, lint sintaxe | validação PR, pós-deploy |
Nota importante: o score, os limites e as recomendações são calculados pela API CaptainDNS no backend. O cliente CI consome um valor já normalizado, não refaz nenhum cálculo.
Combine os dois: linter local em pre-commit para iterações rápidas, API em CI para a validação final antes do merge.
Pipeline GitHub Actions para validar uma pull request DNS
Aqui está um workflow que é acionado a cada pull request que modifica arquivos DNS. Ele extrai os registros DKIM, chama a API CaptainDNS e bloqueia o merge se o estado for invalid ou se o score for inferior 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
A chave API é provisionada a partir do console CaptainDNS e depois armazenada como secret do GitHub (Settings > Secrets and variables > Actions). Use uma chave com escopo dkim:read para limitar o raio de ação em caso de vazamento.

GitLab CI: pipeline com stage validate
No GitLab a lógica permanece idêntica. O arquivo .gitlab-ci.yml abaixo adiciona um cache para evitar revalidar registros inalterados e envia uma notificação para o Slack em caso de falha.
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/**/*
O cache do GitLab guarda as respostas entre dois runs no mesmo branch. Se um seletor não mudou, a API ainda é chamada (o estado DNS pode evoluir independentemente do código), mas o histórico permite rastrear regressões.
Terraform pre-apply: validação antes da publicação DNS
Quando o seu DNS é gerenciado por Terraform, Pulumi ou OpenTofu, a validação lógica acontece antes do apply. O hook abaixo extrai os TXT modificados do plano JSON, valida-os via a API CaptainDNS e bloqueia o apply em caso de falha.
#!/usr/bin/env bash
# scripts/dkim-preapply.sh
set -euo pipefail
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# Extrair os registros TXT DKIM modificados
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
Conecte esse script em um wrapper make plan && make validate && make apply, ou no stage Atlantis se você usa esse padrão GitOps. A mesma lógica se aplica ao Pulumi (via pulumi preview --json) e ao OpenTofu, dado que o formato JSON do plano é compatível.
Lint local em pre-commit
Para economizar minutos de CI, adicione um hook pre-commit que valida a sintaxe dos registros DKIM antes mesmo do push. O framework pre-commit.com permite orquestrar vários hooks Python ou 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
O script dkim-lint.sh pode se apoiar em um parser local Go ou Python que verifica a presença das tags obrigatórias (v=DKIM1, p=...) e a validade base64 da chave pública. Basta para pegar 80 % dos erros de copiar e colar.
Limite a conhecer: um lint pre-commit não consegue verificar o estado real publicado no DNS, nem conflitos com um seletor já ativo. A validação API em CI continua indispensável para esses casos.
Testes de integração pós-deploy
Uma vez o DNS aplicado, verifique se a cadeia fim a fim funciona. Um smoke test envia um e-mail real e verifica o header Authentication-Results.
#!/usr/bin/env bash
# scripts/dkim-smoke-test.sh
set -euo pipefail
# Enviar um e-mail de teste 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"
# Aguardar 60 segundos para o e-mail chegar
sleep 60
# Verificar o header Authentication-Results via IMAP ou um endpoint próprio
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 automático
terraform apply -auto-approve -var "dkim_selector=previous"
exit 1
fi
Você também pode usar serviços sintéticos como Mail-Tester ou GlockApps, que expõem uma API para recuperar os resultados de autenticação de um e-mail enviado a um endereço descartável. O ciclo de feedback torna-se: envio do e-mail, leitura do resultado, depois rollback do DNS se o DKIM falhar.
Para uma verificação pontual sem pipeline, o DKIM Record Check consulta o DNS e mostra o estado completo do seletor em poucos segundos.
Combinar com SPF / DMARC
Validar DKIM sozinho não basta. Um DKIM valid mas um SPF quebrado gera um dmarc=fail nos e-mails não assinados pelo DKIM. Um DMARC mal alinhado degrada a deliverability mesmo com um DKIM perfeito.
A pipeline completa deve, portanto, validar os três protocolos em um mesmo 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 # opcional
Cada script chama o endpoint correspondente: /v1/spf/validate, /v1/dkim/validate, /v1/dmarc/validate. Compartilhar o cliente HTTP, o parsing da resposta e os limites em um módulo comum evita a duplicação.

Plano de ação recomendado
- Inventário: liste seus seletores DKIM ativos por domínio e subdomínio
- Provisionar uma chave API CaptainDNS com escopo
dkim:read+spf:read+dmarc:read - Adicionar o hook pre-commit para o lint de sintaxe local
- Montar um workflow CI que valida os arquivos DNS modificados em PR
- Conectar o Terraform a um hook pre-apply que bloqueia os registros inválidos
- Ativar um canary pós-deploy: um e-mail de teste enviado após cada
apply - Estender aos outros protocolos: SPF, DMARC, BIMI no mesmo stage de validação
FAQ
Por que validar DKIM no CI/CD?
Um registro DKIM quebrado não gera erro visível no lado DNS. Ele só quebra a deliverability dos e-mails, o que se constata com algumas horas de atraso via os relatórios DMARC ou os retornos dos usuários. Validar em CI permite bloquear o merge antes que milhares de e-mails sejam enviados com uma assinatura inválida.
Qual a diferença entre validação local e validação via API?
Um linter local verifica apenas a sintaxe de um registro (tags, formato base64). A API CaptainDNS consulta o DNS publicado, parseia o registro como um receptor o veria e retorna um score calculado no backend. O local é rápido mas cego ao estado real publicado.
Como integrar a validação DKIM no GitHub Actions?
Crie um workflow acionado nas pull requests que modificam a pasta DNS. Recupere os pares domínio/seletor, chame o endpoint /v1/dkim/validate via curl, parseie a resposta com jq, e faça o job falhar se o estado for invalid ou se o score for inferior ao seu limite (50 é um valor de partida razoável).
Dá para bloquear um terraform apply em um DKIM inválido?
Sim. Execute terraform plan -out=tfplan e depois terraform show -json para extrair os registros TXT modificados. Valide cada registro via a API CaptainDNS antes de lançar terraform apply. Se a validação falhar, saia do script com um código diferente de zero para bloquear o apply.
Também é preciso validar SPF e DMARC na mesma pipeline?
Sim. DKIM, SPF e DMARC são interdependentes. Um DKIM válido isolado não garante a deliverability se o SPF falha ou se o DMARC não está alinhado. Valide os três no mesmo stage CI, com fail-fast no primeiro erro ou um relatório consolidado.
Quais secrets configurar para a API CaptainDNS no CI?
Um único secret basta: CAPTAINDNS_API_KEY. Provisione-o a partir do console, dê escopo aos endpoints necessários (leitura DKIM/SPF/DMARC) e armazene-o no secret manager do seu CI. Evite chaves admin em uma pipeline CI, que precisa apenas de leitura.
O que fazer se o registro passa localmente mas falha após o deploy?
Três causas frequentes: propagação DNS incompleta (aguarde 30 minutos), provider DNS que fez split do TXT sem aspas (verifique com dig +short TXT seletor._domainkey.dominio), ou cache negativo em um resolver intermediário. O smoke test pós-deploy deve tentar novamente com backoff exponencial antes de disparar um rollback.
Glossário
- Shift-left: prática que consiste em deslocar os controles de qualidade para as fases iniciais do ciclo de desenvolvimento (PR, pre-commit), em vez de deixá-los na produção.
- Pre-commit hook: script executado localmente antes de cada commit Git, que pode bloquear o commit em caso de erro.
- Pre-apply hook: script executado antes de
terraform apply(ou equivalente Pulumi/OpenTofu) que valida as mudanças planejadas. - Canary: teste pós-deployment com um volume reduzido que detecta as regressões antes que afetem todo o tráfego.
- Score DKIM: valor normalizado (0 a 100) calculado pela API CaptainDNS a partir da sintaxe, do tamanho da chave, do algoritmo e das recomendações RFC.
- ARC (Authenticated Received Chain): protocolo RFC 8617 que preserva os resultados de autenticação DKIM/SPF durante o encaminhamento de e-mails.
Teste sua integração com o DKIM Syntax Check: a ferramenta expõe a mesma API utilizada em CI/CD, ideal para validar um registro antes de adicioná-lo à sua pipeline.
Guias DKIM relacionados para continuar
- Guia completo DKIM: funcionamento, configuração DNS, RSA vs Ed25519 e integração com SPF/DMARC.
- DKIM fail: causas e correções: diagnóstico das 7 causas principais de um
dkim=faile suas correções passo a passo.


