A client asked why some of the emails their web portal sends were landing in junk folders. They run everything on Microsoft 365: Outlook for people, and the portal sends its own mail (activation links, appointment confirmations) through the Microsoft Graph API from a shared mailbox. Their domain had an SPF record and nothing else. No DKIM, no DMARC.

Setting up DKIM on Microsoft 365 comes down to two DNS records and one switch. Doing it well is easier once you can see how the pieces fit together, because the records you publish point at keys you never see.

Three checks, three questions

Receiving mail servers ask three separate questions about every message, and the three records answer one each.

SPF asks whether the server that delivered the message is allowed to send for the domain in the envelope sender (the Return-Path, which the recipient never sees). It is a list of permitted senders in a TXT record. It breaks when a message is forwarded, because the forwarding server is not on your list.

DKIM asks whether someone holding the domain's private key signed this message, and whether it has changed since. The signature travels inside the message, so it survives forwarding.

DMARC asks whether at least one of those two passed for the domain in the visible From address, and tells the receiver what to do if neither did. That alignment with the From address is the part SPF and DKIM do not check on their own.

Since 2024 Gmail and Yahoo have required every sender to have SPF or DKIM, and anyone sending in bulk to have both plus DMARC. A small practice is nowhere near bulk, but mail that passes all three gets treated better than mail that passes one.

Where the keys live

DKIM signing works like this. The sending server hashes the message body and a chosen set of headers, signs the result with a private key, and adds a DKIM-Signature header. Two fields in that header matter here: d=, the signing domain, and s=, the selector. The receiving server combines them, looks up a TXT record at selector._domainkey.domain, finds the public key there and checks the signature.

On Microsoft 365 the private key lives inside Exchange Online and you never handle it. You never publish the public key yourself either. What goes into your DNS are two CNAME records that point at names Microsoft controls, and Microsoft publishes the public keys at those names:

selector1._domainkey.example.co.uk  CNAME  selector1-example-co-uk0i._domainkey.ExampleCo.k-v1.dkim.mail.microsoft
selector2._domainkey.example.co.uk  CNAME  selector2-example-co-uk0i._domainkey.ExampleCo.k-v1.dkim.mail.microsoft

The indirection is what makes key rotation painless. Microsoft can generate a new key pair, publish it under the selector that is not currently in use, switch signing over and retire the old key, all without anyone touching your DNS again. Two selectors exist so that one can be prepared while the other signs.

If you have set this up before, the target format may look unfamiliar. Older tenants and most guides show targets under the tenant's onmicrosoft.com domain, such as selector1-example-co-uk._domainkey.ExampleCo.onmicrosoft.com. Newer setups point under dkim.mail.microsoft instead, with a partition such as k-v1 and a short suffix on the domain part. In this tenant that suffix matched the one in the domain's MX host, example-co-uk0i.mail.protection.outlook.com. Do not build these values by hand. Copy them from the portal.

The order of operations

1. Create the keys. In the Microsoft Defender portal go to Email & collaboration, Policies & rules, Threat policies, Email authentication settings, and open the DKIM tab. Select your domain and choose Create DKIM keys. The panel then shows the two CNAME records.

2. Publish the CNAMEs at your DNS host. This client's DNS is at IONOS, and its record form wants only the left-hand part as the host name: selector1._domainkey. It appends the domain itself, and the preview line shows the full name before you save. Pasting the full name into that field is a common way to end up with selector1._domainkey.example.co.uk.example.co.uk. IONOS also stored the target in lower case. That makes no difference, because DNS names are not case sensitive.

3. Check the records yourself. Within a few minutes both records answered correctly from IONOS's own name servers and from every public resolver we tried:

dig +short CNAME selector1._domainkey.example.co.uk @1.1.1.1
dig +short CNAME selector1._domainkey.example.co.uk @8.8.8.8

4. Turn signing on. Back in the DKIM panel, the switch is called "Sign messages for this domain with DKIM signatures". Our first attempt was refused:

CNAME record does not exist for this config. Please publish the following two CNAME records first. [...] If you have already published the CNAME records, sync will take a few minutes to as many as 4 days based on your specific DNS.

The records existed and the whole internet could see them. Microsoft's check runs on its own schedule and remembers what it saw earlier. Nothing needed changing, and a later attempt went through. The status then read "Signing DKIM signatures for this domain".

One detail surprised us. After creating the keys and before enabling them, the names our CNAMEs pointed at did not exist yet. Asking Microsoft's own name servers directly returned NXDOMAIN for both. The public keys appeared around the time signing was switched on. If you test the full chain at step 3 and the end of it is missing, that is expected at this stage.

Signing starts later than the switch

The portal says the domain is signing from the moment you flip the switch. The mail servers take a while to catch up. Here is what we saw:

  • 11:23: signing enabled, status "Valid".
  • 11:30: a portal email arrived at Gmail with no DKIM signature at all.
  • 11:49: the next one carried d=example.co.uk; s=selector1 and passed.

So allow the best part of half an hour before deciding something is wrong.

The 11:30 message had a second lesson in its headers. Microsoft's own stamp on it read dkim=none (message not signed). Many guides say Microsoft 365 signs with the tenant's onmicrosoft.com domain until you set up your own. In this tenant that default domain had no keys either; its row in the same DKIM list read "No DKIM keys saved for this domain". Until our change, this tenant's mail left completely unsigned.

How to prove it works

From a terminal, the chain should resolve end to end and finish at a key:

dig +short TXT selector1._domainkey.example.co.uk
# v=DKIM1; k=rsa; p=MIIBIjANBgkqh...

dig +short TXT _dmarc.example.co.uk
# "v=DMARC1; p=none;"

From a mailbox, send a message to a Gmail address and open the three-dot menu, Show original. The header block at the top spells out the verdicts. Before the change it showed two lines:

SPF:   PASS with IP 2a01:111:f403:...
DMARC: 'PASS'

After the change there were three:

SPF:   PASS with IP 2a01:111:f403:...
DKIM:  'PASS' with domain example.co.uk
DMARC: 'PASS'

DMARC passed even before DKIM existed. Microsoft sets the envelope sender to the mailbox's own address on your domain, so SPF lines up with the From address and that is enough for DMARC. DKIM is still worth having because SPF fails as soon as a message is forwarded, and once you tighten DMARC you want a second check to fall back on.

For anyone sending from an application: mail sent through Graph's sendMail is submitted to Exchange Online like any other mailbox mail and gets signed the same way. The portal needed no code change.

DMARC: start at none

We published the gentlest possible policy:

_dmarc.example.co.uk  TXT  "v=DMARC1; p=none;"

p=none tells receivers the domain has a policy and asks them to deliver as normal whatever the result. The next step is p=quarantine, which sends failing mail to junk. Before taking it, list everything that sends as the domain: Microsoft 365, the website's contact form, a newsletter tool, a scanner that emails PDFs, an SMTP relay at the registrar. Send a test from each to Gmail and confirm DMARC passes. The SPF record here also included the registrar's mail servers, which is a hint that something besides Microsoft has sent as this domain at some point.

If you want evidence instead of tests, add a reporting address with rua=mailto:... and receivers will send daily summaries of who sent mail as your domain and what passed. Reports to an address on a different domain only arrive if that domain publishes a record agreeing to receive them, so the simplest choice is a mailbox on the domain itself.

The short version

  1. Defender portal, DKIM tab, Create DKIM keys.
  2. Publish the two selector CNAMEs exactly as shown, host name without the domain.
  3. Check them with dig from more than one resolver.
  4. Enable signing. If Microsoft says the CNAMEs do not exist, wait and retry.
  5. Allow half an hour, then confirm DKIM: 'PASS' with domain in Gmail's Show original.
  6. Publish DMARC at p=none, find every other sender, then tighten.

Total hands-on time was about fifteen minutes, most of it waiting for Microsoft to notice records that were already there.