How to check DKIM selector

Read the s= tag of the DKIM-Signature header on a message the domain sent. No DNS query lists the selectors a domain publishes, so the message is the only authoritative source. Guessing common names finds some of them and never tells you the set is complete.

Why check this

You need a selector before you can run How to check DKIM record, and the usual moment is a support ticket: mail from a new provider is failing DKIM at one receiver, and nobody in the room knows which selector the provider signs with. The failure it prevents is a wasted afternoon spent querying a selector the provider stopped using two rotations ago.

Say the hard part first. DNS answers questions of the form "what is at this name". It has no query that enumerates the children of _domainkey.<domain>, and no tool can invent one. A page or a product that offers a "DKIM lookup without selector" is guessing from a list, the same as step 4 below, and its silence about a selector means nothing.

Prerequisites

const fs = require('fs');
const head = fs.readFileSync(process.argv[2], 'utf8').split(/\r?\n\r?\n/)[0];
for (const field of head.split(/\r?\n(?![ \t])/)) {
  if (!/^DKIM-Signature:/i.test(field)) continue;
  const flat = field.replace(/\r?\n[ \t]+/g, ' ').slice('DKIM-Signature:'.length);
  const t = {};
  for (const p of flat.split(';')) { const i = p.indexOf('='); if (i > 0) t[p.slice(0, i).trim()] = p.slice(i + 1).trim(); }
  console.log('selector (s=) : ' + t.s);
  console.log('domain   (d=) : ' + t.d);
  console.log('lookup name   : ' + t.s + '._domainkey.' + t.d);
  console.log('algorithm (a=): ' + t.a);
  console.log('canon     (c=): ' + t.c);
  console.log('signed    (h=): ' + t.h);
}
const { Resolver } = require('dns');
const r = new Resolver();
r.setServers(['8.8.8.8']);                 // Node's default resolver is not always usable

const GUESSES = ['default', 'google', 'selector1', 'selector2', 's1', 's2', 'k1', 'k2',
  'mail', 'dkim', 'smtp', 'mandrill', 'zendesk1', 'sendgrid', 'mailjet', 'protonmail'];

const domain = process.argv[2];
const get = (n) => new Promise((res) => r.resolveTxt(n, (e, x) => res(e ? null : x[0].join(''))));
const keyed = (rec) => /(^|;)\s*p=\s*[A-Za-z0-9+/]/.test(rec);

(async () => {
  // A name nobody would publish. An answer here means a wildcard, so every guess below is worthless.
  const control = await get('zq7x4k9n2v._domainkey.' + domain);
  if (control !== null) {
    console.log('WILDCARD at *._domainkey.' + domain);
    console.log('  control name answered: ' + control);
    console.log('  guessing cannot find a selector on this domain. Read s= from a message instead.');
    return;
  }
  console.log('control name: no TXT record, so an answer below is a real selector');
  let found = 0;
  for (const s of GUESSES) {
    const rec = await get(s + '._domainkey.' + domain);
    if (rec === null) continue;
    found += 1;
    console.log('  ' + s.padEnd(11) + (keyed(rec) ? 'key published' : 'record present, p= empty (revoked)'));
  }
  console.log('guessed ' + GUESSES.length + ' names, found ' + found);
  console.log('this is not the domain\'s selector list. It is the part of it these guesses cover.');
})();

Steps

  1. Step 1.

    Print the whole DKIM-Signature header, folded lines included.

    awk '/^DKIM-Signature:/{f=1;print;next} f&&/^[ \t]/{print;next} f{exit}' message.eml
    
    DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=build.example; s=ci2026;
    t=1789502400; h=from:to:subject:date:message-id:mime-version:content-type;
    bh=Zis1WdJ93u+EtNEcsRue0z7MwsjZ8A2hL7/YzPYFX98=;
    b=okbboUlhK01AQeF2jwaU8bZFWP6qj6cyA182m2I+BIBYPwxrK5ABcpYpAwuoq6WmVK2m
    XHRZnr7IvHh+7YMWN/ZEBpQiKQ5T3gfzlQIvbzJYTPUtKVG4s8p43RgUaDa+hgDzfE1I
    hnBnStWFvasT9/6b7V1qPCbOVpQpEWzHPw+PA69iwJx+jvlsvwFfZN9rxHKlr5QsAyg7
    6Kik3m6yLpbVlC1LyXZ9islb8MKbe4lKNkhKiydfzDyIlwGy45KIIHnfBD8n6N+aqTEV
    /2RVNqslLm3Jp8Yv/fRkqIFkg4Op21OiGWl7sBod6B4NBRSJ5alruX+ftZxUDSJjS3pG
    vQ==

    A header field continues while the next line begins with a space or a tab, which is why a one-line grep loses most of it.

  2. Step 2.

    Split the tags and build the DNS name from s= and d=.

    node dkim-sig-tags.js message.eml
    
    selector (s=) : ci2026
    domain   (d=) : build.example
    lookup name   : ci2026._domainkey.build.example
    algorithm (a=): rsa-sha256
    canon     (c=): relaxed/relaxed
    signed    (h=): from:to:subject:date:message-id:mime-version:content-type

    The name to query comes from s= and d= together. d= is the signing domain, which is not always the domain in the From: header.

  3. Step 3.

    Ask the parent name for a listing and read what DNS returns instead.

    nslookup -type=TXT _domainkey.github.com 8.8.8.8
    
    Server:  dns.google
    Address:  8.8.8.8
    
    github.com
    primary name server = ns-1707.awsdns-21.co.uk
    responsible mail addr = awsdns-hostmaster.amazon.com
    serial  = 1
    refresh = 7200 (2 hours)
    retry   = 900 (15 mins)
    expire  = 1209600 (14 days)
    default TTL = 86400 (1 day)

    An SOA record and no answer section. The name has no TXT data, and asking it yields no list of the selectors below it.

  4. Step 4.

    Guess a list of common selectors, with a control query for a wildcard.

    node dkim-selectors.js github.com
    
    control name: no TXT record, so an answer below is a real selector
    google     key published
    selector1  key published
    s1         key published
    s2         key published
    k1         key published
    k2         key published
    zendesk1   key published
    guessed 16 names, found 7
    this is not the domain's selector list. It is the part of it these guesses cover.
  5. Step 5.

    Run the same script against a domain whose zone answers everything.

    node dkim-selectors.js example.com
    
    WILDCARD at *._domainkey.example.com
    control name answered: v=DKIM1; p=
    guessing cannot find a selector on this domain. Read s= from a message instead.

    Without the control query, this domain reports sixteen selectors out of sixteen guesses and every one of them is the same empty record.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | s= in a signature header | The selector that signed this message | Query <s>._domainkey.<d> and read the key. | | Several DKIM-Signature headers | The domain and a relay both signed | Each carries its own s= and d=. Check the one whose d= is yours. | | d= differs from the From: domain | The provider signed with its own domain | DKIM passes and DMARC can still fail on alignment. | | Control name answers | A wildcard at *._domainkey | Guessing is useless here. Get a message. | | Guessing found nothing | These sixteen names are not in use | The domain may still sign. Absence of a hit is not absence of a selector. | | The record at s= is missing | The selector rotated out, or the zone is wrong | Compare against a message sent today, not one from last quarter. |

Common mistakes

Sign: A lookup tool reports no DKIM for a domain that signs every message it sends.Cause: The tool tried its own guess list and missed. Selectors are arbitrary labels: github.com uses seven that a sixteen-name list happens to catch, and a provider is free to use a random string nobody would guess. Only the s= tag on a real message is evidence.
Sign: A guessing script reports that every common selector exists on the domain.Cause: A wildcard record at *._domainkey answers any name below it. example.com returns v=DKIM1; p= for a selector invented on the spot, so the script must query one nonsense name first and discard the whole run when that answers.
Sign: You copy the selector from one message and the next message fails to verify with it.Cause: A sending platform rotates selectors on its own schedule and may run two at once, one per key. The selector belongs to the message you read it from, so take it from the message that is actually failing.
Sign: grep on the .eml returns one line of the signature and the s= tag is not on it.Cause: Header fields fold across lines, and the tags land in whichever order the signer wrote them. Read the field until the first line that does not start with a space or a tab, which is what step 1 does.

What to check next

FAQ

How to check DKIM without a selector?

You cannot look one up. What you can do is guess from a list, as step 4 does, and treat a miss as no information. The reliable route is a message from the domain, or the sending provider's own setup screen.

Where is the selector in a raw message?

In the s= tag of the DKIM-Signature header. The header is usually near the top of the file, above From:, because signers prepend it.

Can a domain have more than one selector?

Yes, and busy domains have several at once: one per sending platform, plus an old one kept live through a rotation. github.com answered seven of sixteen guessed names on 2026-09-11.

Does the selector name mean anything?

No. It is an opaque label. Dated names like 20230601 and provider names like zendesk1 are conventions, not rules, and nothing in DNS enforces them.

Verified

Verified by Maks Vernynode 22.23.2nslookup Windows 10.0.22631awk 5.0.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate6 minpublished updated Maks Verny