Ir al contenido principal

Monitores HTTP, marca blanca, dominio personalizado. Publicada en 3 minutos.

Monitores y grupos

  • Monitores HTTP ilimitados
  • Agrupación por servicio o región

100 % personalizable

  • Logo y paleta de colores
  • Título y metadatos SEO
  • Contenido libre
Nuevo Nueva funcionalidad

Marca blanca

  • Sin menciones a CaptainDNS
  • Tu dominio mediante CNAME
  • TLS automático

Tiempo real e historial

  • Sincronizada con tus monitores
  • Historial de 30 días
  • Incidentes y mantenimientos

Validar DKIM en tu CI/CD: detectar registros rotos antes de producción

Por CaptainDNS
Publicado el 19 de mayo de 2026

Pipeline CI/CD con validación DKIM: hooks pre-commit, GitHub Actions, GitLab CI, Terraform y smoke test post-deploy
TL;DR
  • Un registro DKIM roto rara vez falla en las pruebas funcionales pero rompe la entregabilidad desde el primer correo enviado tras el deploy
  • Los linters locales detectan la sintaxis; la API CaptainDNS /v1/dkim/validate verifica el estado real publicado en el DNS
  • GitHub Actions, GitLab CI y los hooks de Terraform pueden bloquear un merge o un apply ante un score DKIM insuficiente
  • La validación pre-commit ahorra minutos de CI pero no sustituye una comprobación del estado DNS publicado
  • Combinar las validaciones SPF, DKIM y DMARC en un mismo stage evita regresiones en cascada

Tienes tu DNS versionado en Git, tus rotaciones de claves DKIM pasan por una pull request, y todo va bien hasta el día en que Gmail empieza a marcar tus correos como spam. La causa: una etiqueta p= truncada por un proveedor DNS, o una firma que ya no coincide con el selector publicado. El bug no aparece en terraform plan, ni en las pruebas de integración de la aplicación.

La respuesta cabe en una palabra: shift-left. En lugar de descubrir el problema en producción, valida cada modificación DKIM antes del merge, luego antes del apply, y después del despliegue. Este artículo muestra cómo integrar estos controles en GitHub Actions, GitLab CI, Terraform y un hook pre-commit, con ejemplos copy-paste.

Para comprobar rápidamente la sintaxis de un registro, el DKIM Syntax Check ofrece una API pública reutilizable desde cualquier pipeline.

Por qué un registro DKIM roto llega a producción

Cuatro causas principales explican que un DKIM inválido sortee las defensas estándar de un despliegue DNS.

Edición manual de los registros DNS. Un operador copia una clave pública desde un proveedor transaccional, olvida un carácter o intercambia dos bloques base64. Sin validación sintáctica, el registro se publica tal cual.

Rotación de claves sin validación. El nuevo par se genera, la clave pública se codifica, pero el pipeline de rotación no revisa que la cadena base64 siga siendo coherente tras la exportación. Un truncamiento a 254 caracteres en ciertos paneles de administración basta para romper la firma.

Proveedor DNS que parte en dos chunks. Una cadena TXT de más de 255 caracteres debe ser fragmentada por el servidor DNS, que la vuelve a concatenar con comillas. Algunos proveedores (o algunas UI) cortan la cadena sin comillas: el resolver recibe dos cadenas distintas y la clave reconstruida es inválida.

Sin canary post-deploy. El bug solo aparece en los primeros correos enviados tras el cambio. Para cuando llega la información (informe DMARC, ticket de soporte), miles de mensajes ya están clasificados como spam.

Shift-left: validar antes del merge

El principio es simple: cualquier modificación de un registro DNS ligado al correo pasa por una validación automática en pull request. Tienes dos opciones para validar.

Linter local open source. Herramientas como dkim-checker (CLI Go basada en github.com/emersion/go-msgauth/dkim) o scripts caseros verifican la sintaxis de un registro, la coherencia de las etiquetas (v=DKIM1, k=rsa, p=...) y la longitud de la clave. Ventaja: cero dependencia de red, rápido. Inconveniente: no sabe si la clave está realmente publicada y accesible.

Linter cloud vía API. El endpoint CaptainDNS /v1/dkim/validate acepta un dominio y un selector, consulta el DNS publicado, parsea el registro y devuelve un score normalizado, un estado (valid, invalid, warning) y recomendaciones. Ventaja: valida el estado real observado desde el exterior, igual que un MX que recibiera el correo. Inconveniente: depende de la red, del cupo de la API y del DNS.

CriterioLinter localAPI CaptainDNS
Sintaxis (v=, k=, p=)
Clave publicada en el DNSNo
Tamaño efectivo de la claveParcial
Score normalizadoNoSí (calculado en el backend)
Dependencia de redNo
Caso de usopre-commit, lint sintácticovalidación PR, post-deploy

Nota importante: el score, los umbrales y las recomendaciones los calcula la API CaptainDNS en el backend. El cliente CI consume un valor ya normalizado, no hace ningún cálculo.

Combina ambos: linter local en pre-commit para iteraciones rápidas, API en CI para la validación final antes del merge.

Pipeline GitHub Actions para validar una pull request DNS

Aquí tienes un workflow que se dispara en cada pull request que modifica ficheros DNS. Extrae los registros DKIM, llama a la API CaptainDNS y bloquea el merge si el estado es invalid o si el score es 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

La clave API se aprovisiona desde tu consola CaptainDNS y luego se guarda como secret en GitHub (Settings > Secrets and variables > Actions). Usa una clave con scope dkim:read para limitar el radio de acción en caso de fuga.

Pipeline GitHub Actions que valida un registro DKIM a través de la API CaptainDNS

GitLab CI: pipeline con stage validate

En GitLab la lógica es la misma. El .gitlab-ci.yml siguiente añade una caché para no volver a validar registros sin cambios y envía una notificación a Slack si hay un fallo.

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 caché de GitLab guarda las respuestas entre dos runs de la misma rama. Si un selector no ha cambiado, la API se sigue llamando (el estado DNS puede evolucionar al margen del código), pero el historial permite rastrear regresiones.

Terraform pre-apply: validación antes de publicar en DNS

Cuando tu DNS lo gestiona Terraform, Pulumi u OpenTofu, la validación lógica va antes de apply. El hook siguiente extrae los TXT modificados del plan JSON, los valida vía la API CaptainDNS y bloquea el apply si la validación falla.

#!/usr/bin/env bash
# scripts/dkim-preapply.sh
set -euo pipefail

terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json

# Extraer los 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

Engancha este script en un wrapper make plan && make validate && make apply, o en el stage de Atlantis si usas ese patrón GitOps. La misma lógica vale para Pulumi (vía pulumi preview --json) y OpenTofu, ya que el formato de plan JSON es compatible.

Lint local en pre-commit

Para ahorrar minutos de CI, añade un hook pre-commit que valide la sintaxis de los registros DKIM antes incluso del push. El framework pre-commit.com permite orquestar varios hooks 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

El script dkim-lint.sh puede apoyarse en un parser local Go o Python que verifique la presencia de las etiquetas obligatorias (v=DKIM1, p=...) y la validez base64 de la clave pública. Es suficiente para detectar el 80 % de los errores de copia y pega.

Límite a conocer: un lint pre-commit no puede comprobar el estado real publicado en el DNS, ni los conflictos con un selector ya activo. La validación API en CI sigue siendo imprescindible para esos casos.

Pruebas de integración post-deploy

Una vez aplicado el DNS, verifica que la cadena de extremo a extremo funciona. Un smoke test envía un correo real y revisa la cabecera Authentication-Results.

#!/usr/bin/env bash
# scripts/dkim-smoke-test.sh
set -euo pipefail

# Enviar un correo de prueba con swaks
swaks --to test-canary@captaindns.com \
      --from noreply@captaindns.com \
      --server smtp.captaindns.com \
      --header "Subject: DKIM canary $(date +%s)" \
      --body "Canary message"

# Esperar 60 segundos a que llegue el correo
sleep 60

# Comprobar la cabecera Authentication-Results vía IMAP o un endpoint propio
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

También puedes usar servicios sintéticos como Mail-Tester o GlockApps, que exponen una API para recuperar los resultados de autenticación de un correo enviado a una dirección desechable. El bucle de retroalimentación pasa a ser: enviar el correo, leer el resultado y hacer rollback del DNS si DKIM falla.

Para una verificación puntual sin pipeline, el DKIM Record Check consulta el DNS y muestra el estado completo del selector en segundos.

Combinar con SPF / DMARC

Validar DKIM por sí solo no basta. Un DKIM valid pero un SPF roto produce dmarc=fail en los correos no firmados por DKIM. Un DMARC mal alineado degrada la entregabilidad incluso con un DKIM perfecto.

Por tanto el pipeline completo debe validar los tres protocolos en un mismo 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 llama al endpoint correspondiente: /v1/spf/validate, /v1/dkim/validate, /v1/dmarc/validate. Compartir el cliente HTTP, el parseo de la respuesta y los umbrales en un módulo común evita la duplicación.

Un único stage CI que valida SPF, DKIM, DMARC y BIMI en paralelo vía la API CaptainDNS

Plan de adopción recomendado

  1. Inventario: lista tus selectores DKIM activos por dominio y subdominio
  2. Aprovisionar una clave API CaptainDNS con scope dkim:read + spf:read + dmarc:read
  3. Añadir el hook pre-commit para el lint de sintaxis local
  4. Montar un workflow CI que valide los ficheros DNS modificados en PR
  5. Conectar Terraform a un hook pre-apply que bloquee los registros inválidos
  6. Activar un canary post-deploy: un correo de prueba después de cada apply
  7. Extender a los demás protocolos: SPF, DMARC, BIMI en el mismo stage de validación

FAQ

¿Por qué validar DKIM en la CI/CD?

Un registro DKIM roto no provoca un error visible en el DNS. Solo rompe la entregabilidad de los correos, lo que se detecta con horas de retraso vía los informes DMARC o las quejas de los usuarios. Validar en CI permite bloquear el merge antes de que miles de correos salgan con una firma inválida.

¿Cuál es la diferencia entre validación local y validación por API?

Un linter local solo comprueba la sintaxis de un registro (etiquetas, formato base64). La API CaptainDNS consulta el DNS publicado, parsea el registro tal como lo vería un receptor y devuelve un score calculado en el backend. Lo local es rápido pero ciego al estado real publicado.

¿Cómo integrar la validación DKIM en GitHub Actions?

Crea un workflow disparado en las pull requests que modifican la carpeta DNS. Recoge los pares dominio/selector, llama al endpoint /v1/dkim/validate con curl, parsea la respuesta con jq y haz fallar el job si el estado es invalid o si el score es inferior a tu umbral (50 es un punto de partida razonable).

¿Se puede bloquear un terraform apply ante un DKIM inválido?

Sí. Ejecuta terraform plan -out=tfplan y luego terraform show -json para extraer los registros TXT modificados. Valida cada registro vía la API CaptainDNS antes de lanzar terraform apply. Si la validación falla, sal del script con un código distinto de cero para bloquear el apply.

¿Hay que validar también SPF y DMARC en el mismo pipeline?

Sí. DKIM, SPF y DMARC son interdependientes. Un DKIM válido aislado no garantiza la entregabilidad si SPF falla o si DMARC no está alineado. Valida los tres en el mismo stage CI, con fail-fast al primer fallo o un informe consolidado.

¿Qué secrets configurar para la API CaptainDNS en la CI?

Un solo secret basta: CAPTAINDNS_API_KEY. Aprovisónalo desde la consola, dale scope a los endpoints necesarios (lectura DKIM/SPF/DMARC) y guárdalo en el gestor de secrets de tu CI. Evita las claves de administrador en un pipeline CI, que solo necesita lectura.

¿Qué hacer si el registro pasa localmente pero falla tras el deploy?

Tres causas frecuentes: propagación DNS incompleta (espera 30 minutos), proveedor DNS que ha partido el TXT sin comillas (comprueba con dig +short TXT selector._domainkey.dominio), o caché negativa en un resolver intermedio. El smoke test post-deploy debe reintentar con backoff exponencial antes de disparar un rollback.

Glosario

  • Shift-left: práctica consistente en desplazar los controles de calidad a las fases tempranas del ciclo de desarrollo (PR, pre-commit), en lugar de dejarlos en producción.
  • Pre-commit hook: script ejecutado localmente antes de cada commit Git, que puede bloquear el commit si detecta un error.
  • Pre-apply hook: script ejecutado antes de terraform apply (o el equivalente Pulumi/OpenTofu) que valida los cambios planificados.
  • Canary: prueba post-despliegue con volumen reducido que detecta las regresiones antes de que afecten a todo el tráfico.
  • Score DKIM: valor normalizado (0 a 100) calculado por la API CaptainDNS a partir de la sintaxis, el tamaño de la clave, el algoritmo y las recomendaciones RFC.
  • ARC (Authenticated Received Chain): protocolo RFC 8617 que preserva los resultados de autenticación DKIM/SPF al reenviar correos.

Prueba tu integración con el DKIM Syntax Check: la herramienta expone la misma API utilizada en CI/CD, ideal para validar un registro antes de añadirlo a tu pipeline.


Guías DKIM relacionadas para continuar

Fuentes

Artículos relacionados