# SPF record generation or validation in code

- **Pattern:** `ab-000925` (`deliverability-engineering.dns-auth.spf-validation`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000925
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000925)

## Why it matters

Without programmatic SPF validation, you have no guarantee that your sending domain's SPF record is correctly published — or published at all. RFC7208 alignment is a prerequisite for DMARC enforcement: if SPF fails, email from your domain reaches the spam folder or is rejected outright. Manual DNS management introduces silent misconfiguration: a deployment that adds a new sending IP without updating the SPF record will quietly fail authentication for weeks before anyone notices the deliverability drop.

## Severity rationale

Critical because an absent or misconfigured SPF record causes immediate authentication failure, breaking DMARC alignment and triggering spam classification or outright rejection at major mailbox providers.

## Remediation

Add startup or health-check validation using Node's built-in `dns` module — no third-party dependency required. Check that the SPF record is present and ends with `~all` or `-all`, not the dangerously permissive `+all`.

```ts
import { promises as dns } from 'node:dns'

export async function validateSpfRecord(sendingDomain: string): Promise<void> {
  const records = await dns.resolveTxt(sendingDomain)
  const spfRecord = records.flat().find(r => r.startsWith('v=spf1'))

  if (!spfRecord) throw new Error(`No SPF record found for ${sendingDomain}`)
  if (!spfRecord.includes('~all') && !spfRecord.includes('-all')) {
    throw new Error(`SPF record for ${sendingDomain} uses +all — too permissive`)
  }
}
```

For a SendGrid-only sender the record is `v=spf1 include:sendgrid.net ~all`; for SES in us-east-1 use `v=spf1 include:amazonses.com ~all`. Version-control both the record value and this validation call so SPF gaps surface in CI before reaching production.

## Detection

- **ID:** `spf-validation`
- **Severity:** `critical`
- **What to look for:** Enumerate all sending domains and for each, search for code that generates SPF record strings (`v=spf1 ... ~all` or `-all`) or calls DNS lookup functions to validate existing SPF records. Check for strings containing `v=spf1`, functions named `validateSpf`, `generateSpf`, `checkSpf`, or calls to DNS txt-record lookups. Also look for SPF record values in infrastructure-as-code files (Terraform, CloudFormation, CDK, Pulumi) or DNS configuration scripts.
- **Pass criteria:** At least 1 of the following: (a) code generates a valid SPF record string for the sending domain, (b) code validates that the SPF record is correctly configured by querying DNS, (c) the SPF record is defined in infrastructure-as-code and tracked in version control.
- **Fail criteria:** No code generates, validates, or version-controls SPF records. SPF is managed manually with no programmatic verification.
- **Skip (N/A) when:** The project does not send email.
- **Cross-reference:** Check `deliverability-engineering.dns-auth.dmarc-policy` — DMARC requires SPF alignment to pass, so SPF must be valid before DMARC can enforce.
- **Detail on fail:** Describe what is missing. Example: `"No SPF record generation or validation found — SPF is entirely manually managed with no programmatic checks"` or `"SPF record hardcoded in env vars but no validation code confirms it is published correctly to DNS"`
- **Remediation:** Add SPF validation on startup or as a health check using Node's built-in `dns` module:

  ```ts
  import { promises as dns } from 'node:dns'

  export async function validateSpfRecord(sendingDomain: string): Promise<void> {
    const records = await dns.resolveTxt(sendingDomain)
    const spfRecord = records.flat().find(r => r.startsWith('v=spf1'))

    if (!spfRecord) {
      throw new Error(`No SPF record found for ${sendingDomain}`)
    }

    // Verify SPF ends with ~all or -all (not +all)
    if (!spfRecord.includes('~all') && !spfRecord.includes('-all')) {
      throw new Error(`SPF record for ${sendingDomain} uses +all — too permissive`)
    }

    console.log(`SPF valid for ${sendingDomain}: ${spfRecord}`)
  }
  ```

  A minimal SPF record for a SendGrid-only sender:
  ```dns
  v=spf1 include:sendgrid.net ~all
  ```

  For SES in us-east-1:
  ```dns
  v=spf1 include:amazonses.com ~all
  ```

## External references

- external RFC7208 — https://www.rfc-editor.org/rfc/rfc7208
- iso-25010 reliability

Taxons: operational-readiness

HTML version: https://auditbuffet.com/patterns/ab-000925
