A hardcoded DKIM private key is a secret embedded in source code — readable by anyone with repository access and impossible to rotate without a code deployment and DNS change happening in exact coordination. CWE-321 (hard-coded cryptographic key) and CWE-338 (weak PRNG for key material) both apply. When a DKIM key is compromised, the only remediation is to publish a new key under a new selector and retire the old one; with a static selector and no secrets manager, that operation requires a full deployment cycle, during which your signing infrastructure is either broken or still using the exposed key.
Critical because a compromised DKIM private key allows adversaries to forge authenticated email from your domain, and a static selector makes key rotation operationally infeasible without a deployment and DNS coordination window.
Load the DKIM private key from a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) keyed by a selector that is controlled via environment variable, so rotating to a new key means publishing a new DNS record and updating one env var — no code deployment required.
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
const client = new SecretsManagerClient({ region: process.env.AWS_REGION })
export async function getDkimKey(): Promise<{ privateKey: string; selector: string }> {
const selector = process.env.DKIM_SELECTOR ?? 'k1'
const { SecretString } = await client.send(
new GetSecretValueCommand({ SecretId: `email/dkim/${selector}` })
)
const { privateKey } = JSON.parse(SecretString!)
return { privateKey, selector }
}
For nodemailer, pass the returned { privateKey, selector } directly into the dkim transport option. Never commit key material — store the secret in the manager and the selector name in .env.local (not checked in).
ID: deliverability-engineering.dns-auth.dkim-key-rotation
Severity: critical
What to look for: Count all DKIM key loading paths and classify each. Examine how DKIM private keys are loaded. Check if the private key is: (a) hardcoded as a string literal in source code, (b) loaded from an environment variable with no rotation mechanism, or (c) loaded dynamically from a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) with a selector that can be changed without code deployment. Also look for a DKIM selector value — if the selector is hardcoded as a static string (default, mail, k1) with no mechanism to rotate to a new selector, key rotation is not operationally feasible.
Pass criteria: DKIM private key is loaded from at least 1 secrets manager or key vault, not hardcoded in source. A hardcoded key literal does not count as pass. A DKIM selector rotation mechanism exists (either environment-configurable selector or code that supports multiple active selectors). Key rotation can be performed without a full code deployment.
Fail criteria: DKIM private key is hardcoded in source code, or the DKIM selector is static with no rotation path, making key rotation operationally impractical.
Skip (N/A) when: The project delegates DKIM signing entirely to the ESP (e.g., SendGrid or Mailgun sign on the sender's behalf using their own infrastructure) and no custom DKIM signing occurs in the codebase.
Detail on fail: Describe the exposure. Example: "DKIM private key hardcoded as string literal in src/lib/email/sign.ts" or "DKIM selector is static string 'mail' with no rotation mechanism — key compromise cannot be remediated without DNS change and code deployment"
Remediation: Load DKIM keys from a secrets manager and support selector rotation:
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
const secretsClient = new SecretsManagerClient({ region: process.env.AWS_REGION })
export async function getDkimKey(): Promise<{ privateKey: string; selector: string }> {
// Selector is environment-configurable — swap selector to rotate
const selector = process.env.DKIM_SELECTOR ?? 'k1'
const secretId = `email/dkim/${selector}`
const response = await secretsClient.send(
new GetSecretValueCommand({ SecretId: secretId })
)
const { privateKey } = JSON.parse(response.SecretString!)
return { privateKey, selector }
}
For nodemailer with DKIM:
const { privateKey, selector } = await getDkimKey()
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
dkim: {
domainName: process.env.SENDING_DOMAIN,
keySelector: selector,
privateKey
}
})