Zum Inhalt springen

Open source / Sentinel / dns-sentinel

dns-sentinel

DNS and email security auditor: SPF (10-lookup limit, +all, ptr), DMARC, DKIM key strength, MX/null MX, MTA-STS, TLS-RPT, CAA, DNSSEC via DoH, dangling CNAME and subdomain takeover, wildcards and AXFR, with a fix for each. Text, JSON, SARIF, Markdown; GitHub Action. Zero deps.

Auf npm
$ npm install dns-sentinel

README

dns-sentinel

npm version CI Node License: MIT provenance

Audit your domains' DNS and email security before attackers or spammers find the gaps.

dns-sentinel checks SPF, DMARC, DKIM, MX / null MX, MTA-STS, TLS-RPT, CAA and DNSSEC. It also finds dangling CNAMEs and subdomain takeovers, wildcard records and open zone transfers. Every finding comes with a severity and a concrete fix, usually the exact DNS record to publish.

CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies. Lookups use node:dns/promises (system or chosen resolvers) by default. DNS-over-HTTPS is optional, and the DNSSEC check uses it by default to read the validating resolver's AD flag.

Checks

AreaRules (default severity)
SPFspf-missing (high; medium for non-mail domains), spf-multiple (high), spf-too-many-lookups (high; nested includes, a, mx, exists, redirect counted against the 10-lookup limit), spf-void-lookups (medium), spf-pass-all (critical, +all), spf-neutral-all (medium, ?all), spf-no-all (medium), spf-ptr (medium, including via includes), spf-invalid (syntax errors, include targets without SPF, include loops, terms after all), spf-softfail-all (info)
DMARCdmarc-missing (high), dmarc-multiple (high), dmarc-invalid (high), dmarc-policy-none (medium; high for non-mail domains), dmarc-no-rua (low), dmarc-pct (medium, pct < 100), dmarc-weak-subdomain (medium, sp weaker than p). Subdomains fall back to the organisational domain's record and its sp
DKIMSelectors from --dkim-selector, plus ~30 common selectors with --dkim-common. dkim-weak-key (critical for RSA < 1024 bits, medium for < 2048), dkim-revoked (low, empty p=), dkim-testing (low, t=y), dkim-missing (medium), dkim-invalid (medium), dkim-not-found (low)
MXmx-missing (recommends a null MX MX 0 . for domains that don't receive mail), null-mx-misconfigured (high), null-mx-spf (medium), mx-dangling (high), mx-cname (low), mx-ip-literal (medium)
MTA-STS / TLS-RPTmta-sts-missing (low), mta-sts-invalid (medium: TXT id, policy fetch over HTTPS, policy syntax), mta-sts-testing (low), mta-sts-mx-mismatch (medium; high in enforce mode), tlsrpt-missing (low), tlsrpt-invalid (low)
CAAClosest-ancestor lookup (RFC 8659). caa-missing (low), caa-permissive (low; no issue tags or > 3 CAs), caa-critical-unknown (high), caa-no-iodef (info)
DNSSECDS and DNSKEY presence plus the AD flag from a validating DoH resolver. dnssec-disabled (medium), dnssec-no-ds (medium), dnssec-broken (high: SERVFAIL, or DS without validation), dnssec-weak-algorithm (RSASHA1/DSA keys, SHA-1-only DS)
Dangling CNAMEsFor --subdomain, --subdomains-file or --common-subdomains: follows the CNAME chain and flags dangling-cname (high: target NXDOMAIN; medium: no addresses). subdomain-takeover (critical) when the target is a takeover-prone service and either doesn't exist or serves the service's "unclaimed" page (HTTP fingerprint). takeover-prone-cname (info) otherwise. Services include AWS S3 and Elastic Beanstalk, GitHub Pages, Heroku, Azure (App Service, Cloud Services, Traffic Manager, Blob, CDN, Front Door…), Shopify, Fastly, Pantheon, Tumblr, Ghost, Surge, Bitbucket, Netlify, Webflow, Zendesk, ReadMe, Unbounce, Help Scout, Strikingly, Fly.io, Agile CRM, Canny, Statuspage, WordPress.com and Google Cloud Storage
Wildcardswildcard-record (low), plus takeover checks on wildcard CNAMEs
Zone transferzone-transfer (high). AXFR is attempted against every NS only with --axfr (off by default)
Configallow-expired (medium) when an allowlist entry has passed its expiry date

Install and run

npx dns-sentinel example.com
npx dns-sentinel example.com example.org --dkim-selector google,s1 -s www -s docs -s status
npx dns-sentinel example.com --subdomains-file subdomains.txt --fail-on medium -f sarif > dns.sarif
npx dns-sentinel example.com --doh                        # all lookups over DNS-over-HTTPS (Cloudflare)
npx dns-sentinel example.com --resolver 9.9.9.9 --axfr    # pick a resolver; attempt zone transfers
npx dns-sentinel example.com --checks spf,dmarc,dkim --dkim-common

Sample output

A live run against louiswright.co.uk (--doh -s www -s mail --dkim-common --axfr):

dns-sentinel — 1 domain(s) · resolver: DoH https://cloudflare-dns.com/dns-query; DNSSEC: DoH https://cloudflare-dns.com/dns-query

louiswright.co.uk
  MX: 30 athena.hosts.co.uk, 30 hermes.hosts.co.uk
  SPF: v=spf1 include:stspg-customer.com include:spf.hosts.co.uk include:_spf.firebasemail.com ~all (7 lookups)
  DMARC: v=DMARC1; p=none;
  DKIM: no keys found
  MTA-STS: none
  TLS-RPT: none
  CAA: none
  DNSSEC: off
  Wildcard: CNAME s.multiscreensite.com
  www.louiswright.co.uk: CNAME → cname.vercel-dns.com
  mail.louiswright.co.uk: CNAME → s.multiscreensite.com → global.multiscreensite.com → a3c02b2530d6f27ca.awsglobalaccelerator.com
  AXFR ns1.phase8.net: refused or unreachable
  AXFR ns2.phase8.net: refused or unreachable
  AXFR ns0.phase8.net: refused or unreachable
  MEDIUM   DMARC policy is p=none: spoofed mail is reported but still delivered [dmarc-policy-none]
           ↳ fix: Once reports show your legitimate mail passes, move to `p=quarantine` and then `p=reject`.
  MEDIUM   DNSSEC is not enabled for louiswright.co.uk; answers can be spoofed by cache poisoning [dnssec-disabled]
           ↳ fix: Enable DNSSEC signing at your DNS provider and publish the DS record at your registrar.
  LOW      No CAA records for louiswright.co.uk (or its parents), so any certificate authority may issue for it [caa-missing]
           ↳ fix: Restrict issuance to your CAs, e.g. `louiswright.co.uk. CAA 0 issue "letsencrypt.org"` and `CAA 0 iodef "mailto:security@louiswright.co.uk"`.
  LOW      No DKIM key found on 31 common selectors [dkim-not-found]
           ↳ fix: Pass your real selector with --dkim-selector (find it in the s= tag of a DKIM-Signature header).
  LOW      DMARC has no rua= address, so you receive no aggregate reports [dmarc-no-rua]
           ↳ fix: Add `rua=mailto:dmarc-reports@louiswright.co.uk` (or a DMARC reporting service).
  LOW      louiswright.co.uk has no MTA-STS, so inbound SMTP TLS can be downgraded by an attacker [mta-sts-missing]
           ↳ fix: Publish `_mta-sts.louiswright.co.uk. TXT "v=STSv1; id=20260101"` and a policy at https://mta-sts.louiswright.co.uk/.well-known/mta-sts.txt (start with mode: testing).
  LOW      louiswright.co.uk has no TLS-RPT record, so you won't hear about SMTP TLS failures [tlsrpt-missing]
           ↳ fix: Publish `_smtp._tls.louiswright.co.uk. TXT "v=TLSRPTv1; rua=mailto:tls-reports@louiswright.co.uk"`.
  LOW      louiswright.co.uk has a wildcard record (CNAME s.multiscreensite.com): every possible subdomain resolves, which helps phishing and hides typos and dangling records [wildcard-record]
           ↳ fix: Remove the wildcard and create explicit records, unless you need it (e.g. multi-tenant hosting).
  INFO     SPF ends in "~all" (softfail). With an enforcing DMARC policy this is fine; otherwise -all is stronger [spf-softfail-all]
           ↳ fix: Consider `-all` once you are confident all senders are listed.

2 medium, 6 low, 1 info
✔ Passing (fail-on: high).

Options

OptionDescription
<domain…>One or more domains
--checks <list> / --skip <list>Choose checks: spf, dmarc, dkim, mx, mta-sts, tls-rpt, caa, dnssec, cname, wildcard, axfr (default: all except axfr)
--dkim-selector <s>DKIM selector(s) to test (repeatable or comma-separated)
--dkim-commonAlso try ~30 common selectors (google, selector1, k1, s1, default, …)
-s, --subdomain <name>Subdomain to check for dangling CNAMEs (www or www.example.com; repeatable)
--subdomains-file <file>Subdomains, one per line (- for stdin)
--common-subdomainsAlso check ~40 common names (www, blog, docs, status, shop, …)
--axfrAttempt AXFR against each nameserver
--no-httpSkip HTTP: no MTA-STS policy fetch, no takeover fingerprints
-r, --resolver <ip[:port]>DNS server(s) for node:dns (repeatable; default: system)
--doh [url]Use DNS-over-HTTPS for every lookup (default https://cloudflare-dns.com/dns-query)
--dnssec-doh <url> / --no-dnssec-dohDoH endpoint for the DNSSEC check, or check it with plain DNS to --resolver (AD bit)
--timeout <ms>Per-lookup timeout (default 5000)
-c, --config <file>JSON config (default .dns-sentinel.json)
-f, --formattext (default), json, sarif, markdown
--json/--sarif/--markdown <file>Also write that format to a file
--fail-on <sev>critical, high (default), medium, low, info or none
--list-rulesList rules, severities and groups

Exit codes: 0 passed, 1 findings at or above --fail-on, 2 usage/config error or a domain could not be checked (NXDOMAIN, SERVFAIL, timeouts).

Configuration

.dns-sentinel.json:

{
  "domains": ["example.com", "example.org"],
  "dkimSelectors": ["google", "s1"],
  "subdomains": ["www", "docs", "status", "shop"],
  "failOn": "medium",
  "severity": { "caa-no-iodef": "off", "dnssec-disabled": "low" },
  "allow": [
    { "rule": "dmarc-policy-none", "domain": "example.org", "reason": "DMARC rollout, reviewing reports", "expires": "2026-12-31" },
    { "rule": "wildcard-record", "domain": "example.com", "reason": "Multi-tenant hosting" },
    { "rule": "takeover-prone-cname", "name": "docs.example.com", "reason": "GitHub Pages, owned by us" }
  ]
}

Allowlist entries match on rule (globs like dnssec-* allowed), and optionally domain and name (the DNS name the finding is about). Each needs a reason. After expires the entry stops applying, the finding comes back, and allow-expired is reported. Other keys: checks, axfr, dkimCommon, commonSubdomains, servers, doh, dnssecDoh, timeout, http, caaMaxIssuers.

GitHub Action

on:
  schedule: [{ cron: '0 6 * * 1' }]
permissions:
  contents: read
jobs:
  dns:
    runs-on: ubuntu-latest
    steps:
      - uses: MrLouisWright/dns-sentinel@<sha> # v1
        with:
          domains: example.com example.org
          subdomains: www docs status
          dkim-selectors: google,s1
          fail-on: medium

Inputs: domains, subdomains, subdomains-file, dkim-selectors, checks, axfr, doh, fail-on, config, sarif, json, job-summary, args.

Library

import { auditDomains, formatText, MockResolver } from 'dns-sentinel';

const report = await auditDomains(['example.com'], { dkimSelectors: ['google'], subdomains: ['www'], doh: true });
console.log(formatText(report, { failOn: 'high' }));

// Offline / tests: pass any resolver with query(name, type)
const resolver = new MockResolver({ 'example.com': { TXT: ['v=spf1 +all'], MX: [] } });
await auditDomains(['example.com'], { resolver, http: false });

SystemResolver (node:dns), WireResolver (UDP/TCP with DNSSEC bits), DohResolver (RFC 8484) and MockResolver are exported, as is the wire codec (encodeQuery, decodeMessage).

Rules reference

spf-missing              high      spf       No SPF record
spf-multiple             high      spf       Multiple SPF records (permerror)
spf-too-many-lookups     high      spf       SPF needs more than 10 DNS lookups (permerror)
spf-void-lookups         medium    spf       SPF has more than 2 void lookups
spf-pass-all             critical  spf       SPF +all lets anyone send as the domain
spf-neutral-all          medium    spf       SPF ?all (neutral) gives no protection
spf-no-all               medium    spf       SPF has no all mechanism or redirect
spf-softfail-all         info      spf       SPF ends in ~all (softfail)
spf-ptr                  medium    spf       SPF uses the deprecated ptr mechanism
spf-invalid              high      spf       SPF syntax error or broken include
dmarc-missing            high      dmarc     No DMARC record
dmarc-multiple           high      dmarc     Multiple DMARC records
dmarc-invalid            high      dmarc     Invalid DMARC record
dmarc-policy-none        medium    dmarc     DMARC p=none (monitoring only)
dmarc-no-rua             low       dmarc     DMARC without aggregate reports (rua)
dmarc-pct                medium    dmarc     DMARC pct below 100
dmarc-weak-subdomain     medium    dmarc     DMARC subdomain policy weaker than the domain policy
dkim-missing             medium    dkim      DKIM selector has no key
dkim-weak-key            medium    dkim      Weak DKIM key
dkim-revoked             low       dkim      Revoked DKIM key (empty p=)
dkim-testing             low       dkim      DKIM key in testing mode (t=y)
dkim-invalid             medium    dkim      Unparseable DKIM record
dkim-not-found           low       dkim      No DKIM key found on any checked selector
mx-missing               low       mx        No MX and no null MX
null-mx-misconfigured    high      mx        Null MX mixed with other MX records
null-mx-spf              medium    mx        Null MX but SPF authorises senders
mx-dangling              high      mx        MX host does not resolve
mx-cname                 low       mx        MX points to a CNAME
mx-ip-literal            medium    mx        MX is an IP address
mta-sts-missing          low       mta-sts   No MTA-STS
mta-sts-invalid          medium    mta-sts   Broken MTA-STS record or policy
mta-sts-testing          low       mta-sts   MTA-STS not enforcing
mta-sts-mx-mismatch      medium    mta-sts   MX host not covered by the MTA-STS policy
tlsrpt-missing           low       tls-rpt   No TLS-RPT record
tlsrpt-invalid           low       tls-rpt   Invalid TLS-RPT record
caa-missing              low       caa       No CAA records
caa-permissive           low       caa       Overly permissive CAA
caa-critical-unknown     high      caa       CAA critical flag on an unknown tag (blocks issuance)
caa-no-iodef             info      caa       CAA without iodef reporting
dnssec-disabled          medium    dnssec    DNSSEC not enabled
dnssec-no-ds             medium    dnssec    Zone signed but no DS at the parent
dnssec-broken            high      dnssec    DNSSEC validation fails
dnssec-weak-algorithm    medium    dnssec    Weak DNSSEC algorithm or digest
dangling-cname           high      cname     CNAME points to a name that does not exist
subdomain-takeover       critical  cname     Subdomain takeover possible
takeover-prone-cname     info      cname     CNAME to a takeover-prone service
wildcard-record          low       wildcard  Wildcard DNS record
zone-transfer            high      axfr      Zone transfer (AXFR) allowed
allow-expired            medium    config    Allowlist entry has expired

Support

If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.

Credit (optional)

Free to use under MIT, no attribution required. If dns-sentinel helps you, a link back is appreciated:

<a href="https://louiswright.co.uk">Built with dns-sentinel by Louis Wright</a>
[![Built with dns-sentinel by Louis Wright](https://img.shields.io/badge/built%20with-dns--sentinel-blue)](https://louiswright.co.uk)

BIMI, provider DKIM selectors, baselines and DMARC reports

npx dns-sentinel example.com --dkim-providers              # try selectors of providers seen in MX/SPF
npx dns-sentinel example.com --save-baseline dns.json      # snapshot today's records
npx dns-sentinel example.com --baseline dns.json           # later: report changed records / SPF lookup growth
npx dns-sentinel dmarc-report reports/*.xml.gz reports/*.zip   # summarise RUA aggregate reports
  • BIMI: default._bimi.<domain> is checked for syntax, an https:// SVG logo and a VMC/CMC certificate (a=), and DMARC must be at p=quarantine/reject with pct=100. A domain without BIMI is fine.
  • DKIM selectors by provider: Google Workspace (google), Microsoft 365 (selector1/selector2), SendGrid (s1/s2), Mailchimp (k1–k3), Amazon SES, Mailgun, Postmark, Zoho, Fastmail, Proton and more are recognised from MX and SPF includes.
  • Baselines: a JSON snapshot of SPF, DMARC, MX, MTA-STS, TLS-RPT, CAA, DKIM and BIMI. Any difference is reported as record-changed. spf-lookups-increased catches a provider quietly adding nested includes, and spf-lookups-near-limit warns at 8 of 10 lookups.
  • DMARC aggregate reports: .xml, .xml.gz and .zip attachments are parsed and summarised as messages, DMARC pass rate and failing source IPs. In code, use parseDmarcReport(buffer) and summarizeDmarcReports([...]).
  • ARC: checkArcHeaders(headers) validates an ARC chain from raw message headers (RFC 8617).

HSTS belongs to header-sentinel, and TLS handshakes and certificates to tls-sentinel; dns-sentinel stays with DNS records.

Module formats and types

Ships ESM (import) and a bundled CommonJS build (require), with TypeScript declarations generated from the source:

import * as lib from 'dns-sentinel';      // ESM
const lib = require('dns-sentinel');      // CommonJS

Tests live in test/ and run on Node 18, 20 and 22 in GitHub Actions, including a require+import check. Releases are published with npm publish --provenance.

License

MIT © Louis Wright (Louis Wright)