dkim-rotate(7)
dkim-rotate πβ¨
dkim-rotate β Principles of Operation π
okay bestie, sit down, grab a strawberry soda π, because this one is genuinely iconic and also kind of a banger in the "privacy rights" genre. This is section 7 of the manual, so it's the β¨conceptsβ¨ page, not the "type these flags" page. iykyk.
INTRODUCTION π¦
dkim-rotate is a tool for managing DKIM (email antispam) keys in a manner that avoids unnecessarily making emails nonrepudiable. and like. that's the whole vibe. that's the mission. π
Problem statement π
Using a static or nearly-static DKIM signing key enables anyone who obtains a copy of an email to verify its authenticity.
This can be used to verify the authenticity of data from a data breach, for example. This is not a desirable property, from the point of view of an email system's users, and wasn't an intended consequence of DKIM's antispam function.
For fuller discussion of the nonrepudiability problem with DKIM, see the blog post Ok Google: please publish your DKIM secret keys, referenced in the SEE ALSO section. go read it, it's giving "wake up babe" energy π
Solution - function of dkim-rotate π
We periodically generate a new key. We deadvertise old keys (removing them from the set advertised in the DNS). We publish the private halves of old keys.
The overall result is that because old emails are forgeable (by anyone, since the private key has been published), emails become no longer nonrepudiable.
We add appropriate warnings, and alter the DNS, to alert naive verifiers to the situation. so it's like, yes the old key is out there, but also there's a big flashing sign. β οΈ
Output and state files π
dkim-rotate will maintain and update the following output files and directories:
/var/lib/dkim-rotate/instance/zoneβ Zonefile in standard master file syntax. Created by taking the config file, editing the serial number before;!SERIAL, and appendingTXTRR definitions. The appended RRs have single-character alphabetic labels, the selectors. Seedkim-rotate(5)./var/lib/dkim-rotate/instance/priv/keyname.pemβ Private key for use by the MTA. See MTA CONFIGURATION./var/lib/dkim-rotate/instance/eximβ File in format suitable for Exim${lsearch }. See MTA CONFIGURATION./var/lib/dkim-rotate/instance/pub/β Directory where private keys are published (deliberately leaked). See KEY PUBLICATION./var/lib/dkim-rotate/instance/stateβ Principal state file./var/lib/dkim-rotate/instance/...β Other state and temporary files are stored here.
(Each dkim-rotate instance is completely separate; they do not share state, or configuration.) no sharing bestie, we do not pass the aux π
SELECTORS πΈ
dkim-rotate maintains a collection of DKIM keys. The (currently advertised) keys each have a "selector", dkim-rotate uses a small fixed set of selectors, in rotation. Each selector is an (ASCII lowercase) letter, so dkim-rotate supports use of up to 26 selectors. The default is 12.
Current keys - selectors β¨
A DKIM signature found in an email indicates where to find the key. It includes a "selector", which is a set of DNS labels to be prepended to the base DKIM domain for the mail domain which originated the email and by whose authority the message is being signed.
A selector can be reused as soon as the key which was previously using that selector should longer be advertised. When creating keys, dkim-rotate will automatically choose a suitable available selector.
The selector in DKIM terms is (usually) the dkim-rotate selector plus a fixed label indicating the signing authority (ie, the dkim-rotate instance). dkim-rotate itself does not know the actual DKIM selectors; the suffix is added in the MTA and DNS configurations. it's like, not its business fr fr π«Ά
DNS selector advertisement π
dkim-rotate outputs a DNS zonefile, complete with serial number, as /var/lib/dkim-rotate/instance/zone.
Usually, this will be published directly by a nameserver, as a dedicated DNS zone, not used for other purposes. This allows the management of the mail domains' zones to be separated from the DKIM system. lowkey elegant π
Let us imagine that the dkim-rotate output zone is dkim-rotate.example.net. Within that zone, dkim-rotate will create DKIM TXT records, which look like this in the output zone file:
k IN TXT "v=DKIM1; h=sha256; s=email; n=...; p=..."
This implies the following RRset:
k.dkim-rotate.example.net. IN TXT "v=DKIM1; ..."
A mail domain (let us imagine, example.com), which wishes to indicate that this system is authorised to make DKIM signatures, will use a set of CNAMEs to delegate that authority:
$ORIGIN example.com.
a.example-net._domainkey CNAME a.dkim-rotate.example.net.
b.example-net._domainkey CNAME b.dkim-rotate.example.net.
c.example-net._domainkey CNAME c.dkim-rotate.example.net.
d.example-net._domainkey CNAME d.dkim-rotate.example.net.
e.example-net._domainkey CNAME e.dkim-rotate.example.net.
f.example-net._domainkey CNAME f.dkim-rotate.example.net.
g.example-net._domainkey CNAME g.dkim-rotate.example.net.
h.example-net._domainkey CNAME h.dkim-rotate.example.net.
i.example-net._domainkey CNAME i.dkim-rotate.example.net.
j.example-net._domainkey CNAME j.dkim-rotate.example.net.
k.example-net._domainkey CNAME k.dkim-rotate.example.net.
l.example-net._domainkey CNAME l.dkim-rotate.example.net.
So, overall, we have something like this:
example.com. MX mx0.example.com.
k.example-net._domainkey.example.com. CNAME k.dkim-rotate.example.net.
k.dkim-rotate.example.net. TXT "v=DKIM1; ..."
DNS output file and nameserver configuration π
The zonefile is written to /var/lib/dkim-rotate/instance/zone.
After it has been updated, dkim-rotate runs rndc reload (or the configured dns_reload command).
If this all occurs successfully, dkim-rotate assumes that dns_lag later, the new DNS records (and any deletions) are available everywhere.
dkim-rotate does not use DNS Dynamic Update. nope. not doing it. π
π MTA CONFIGURATION π
okay so dkim-rotate is literally handing the selector and the private key to the MTA. It does this by writing /var/lib/dkim-rotate/instance/exim. That file is in a key-colon-value format, which is super convenient for Exim's lsearch lookup facility. iykyk.
After this file gets updated, dkim-rotate runs the configured mta_reload command. That command is just true (a no-op) by default β and Exim doesn't even need it. no cap.
π MTA configuration output format π
The output file is lines of the form:
key: value
It may also contain #-comment lines. The values are literal text, without any quotes (and therefore cannot contain newlines). The keys are:
privkeyβ the filename of the private key to use. This will be in the form/var/lib/dkim-rotate/instance/priv/hex.pem.selectorβ the selector under which the corresponding public key is advertised.header_noteβ some text which it would be useful to put into the email headers. It startsNOTE REGARDING DKIM KEY COMPROMISE. This could be put into anote=orwarning=tag in the actualDKIM-Signatureheader. If that is not possible (e.g. Exim doesn't support it) it could be put intoDKIM-Signature-Warning, say. It is probably a good idea to arrange that it is itself covered by the signature, to make it more complicated for an adversary to strip it out.url,readme_urlβ URLs for the instance's public WWW directory, theREADME.txtfile, corresponding to this instance.key_reveal_urlβ the URL at which the private key will be revealed to the world, after the key has been retired. (Obviously, when this URL appears in the/eximfile, the URL is not yet valid.)
π οΈ Example Exim configuration π οΈ
DKIM signing is done with additional options on the smtp transport. The mailserver ought not to be a signing oracle for arbitrary incoming emails which are being relayed (eg via forward files) β only for emails generated locally, or from appropriately authorised places. And we should choose, for the signing domain, the domain which appears in the From: header, and sign only if DKIM is enabled for that domain.
The required config looks something like this:
smtp:
driver = smtp
# ... other options ...
# lookup fd caching ensures coherence of all of these, see exim 4.94 spec 9.8
dkim_domain = ${if and{ \
{ match_domain {${domain:$h_from:}} {+dkim_domains} } \
{ !def:h_dkim-signature: } \
{ !def:h_list-id: } \
{ or{ \
{ def:authenticated_id } \
{ match_ip {$sender_host_address} {+relay_hosts} } \
}} \
} {${domain:$h_from:}} {} }
dkim_selector = ${lookup {selector} lsearch {/var/lib/dkim-rotate/example-net/exim} }.example-net
dkim_private_key = ${lookup {privkey} lsearch {/var/lib/dkim-rotate/example-net/exim} }
dkim_sign_headers = _DKIM_SIGN_HEADERS : DKIM-Signature-Warning
headers_add = ${if and{ \
{ match_domain {${domain:$h_from:}} {+dkim_domains} } \
{ !def:h_dkim-signature: } \
{ !def:h_list-id: } \
{ or{ \
{ def:authenticated_id } \
{ match_ip {$sender_host_address} {+relay_hosts} } \
}} \
} {DKIM-Signature-Warning: ${lookup {header_note} lsearch {/var/lib/dkim-rotate/example-net/exim} }} }
πͺ Example Exim configuration (perl version) πͺ
It is a shame that Exim doesn't seem to have better and more cooked facilities for controlling dkim signing. The required configuration is quite annoying repetitive. fr fr.
The following Perl can generate something like the config above:
sub dkim_lookup { "\${lookup {$_[0]} lsearch {/var/lib/dkim-rotate/example-net/exim} }" }
my $dkim_domain_expr = "\${domain:\$h_from:}";
my $dkim_condition = <<END;
and{ \\
{ match_domain {$dkim_domain_expr} {+dkim_domains} } \\
{ !def:h_dkim-signature: } \\
{ !def:h_list-id: } \\
{ or{ \\
{ def:authenticated_id } \\
{ match_ip {\$sender_host_address} {+relay_hosts} } \\
}} \\
END
my $dkim_smtp_options = <<END;
# lookup fd caching ensures coherence of all of these, see exim 4.94 spec 9.8
dkim_domain = \${if $dkim_condition } {$dkim_domain_expr} {} }
dkim_selector = ${\ dkim_lookup('selector')}.example-net
dkim_private_key = ${\ dkim_lookup('privkey')}
dkim_sign_headers = _DKIM_SIGN_HEADERS : DKIM-Signature-Warning
headers_add = \${if $dkim_condition } {DKIM-Signature-Warning: ${\ dkim_lookup('header_note')}} }
END
$dkim_smtp_options =~ s{^(.*\S)\s*\\$}{ sprintf "%-70s\t\\", $1 }mge;
π KEY PUBLICATION π
dkim-rotate publishes secret keys by writing them to a directory /var/lib/dkim-rotate/instance/pub/. This is an ever-growing archive. Nothing ahould be deleted from it.
This directory should be made available via webserver, and the corresponding URL configured via the pub_url config directive.
dkim-rotate will make subdirectories 00 to ff here. These are radix prefix directories which exist both to avoid the creation of a very large single directory of key files, and to make it harder to enumerate the private keys.
In particular, these subdirectories are not globally-readable, although they are globally-executable. The webserver should run without privilege, so that the individual keys can be read, but the directories cannot be listed (and won't be archived by any crawlers).
π README π
dkim-rotate will make a README.txt file in the pub/ directory.
(Currently there is no way to configure the contents of this file.)
π± KEY LIFECYCLE π±
π Key statuses and lifecycle π
-1β meaning: advertised; not yet used β time_t (in statefile): first advertised β selector: advertised (DNS) β how many: 0/1+0β meaning: signing β time_t (in statefile): (first used; not relevant) β selector: advertised (DNS) β how many: 0/1; usually 1+N..β meaning: emails percolating β time_t (in statefile): last used for signing β selector: advertised (DNS) β how many: [0 .. sel_limit]+X..β meaning: deadvertisment propagating β time_t (in statefile): last advertised β selector: archival only β how many: [0 ..]; usually 0/1Rβ meaning: revealed β time_t (in statefile): (not longer in statefile) β selector: archival only β how many: many
β³ Example key lifecycle β³
-0200 [0][1] generate and
advertise
dns_lag (4h) + 2h slop
+0400 start signing
1d key rollover interval
+0400 +1d stop signing
email_lag (3.5d timeout + 4h retry) +
2h slop
-0200 +5d deadvertise
dns_lag (4h) + 2h slop
+0400 +5d reveal
[0] -0200 means β2200 the previous dayβ
[1] If a free selector is already available, this might be generated and advertised at +0400 -1d.
β° Example cron configuration β°
#mins hrs dom mon dow command
26 22 * * * dkim-rotate --minor
26 4 * * * dkim-rotate --major
These jobs should be scheduled in a suitable local time (in the timezone of the mail server's users), because it is good for all mails sent on a particular calendar day to become un-nonrepudiable (and un-deliverable) at once.
To cope nicely with timezone changes the interval between --minor and the main run should be at least dns_lag + 1h + an allowance for processing time etc. The suggested configuration has a 6h interval, which suits the default dns_lag of 4h.
π AUTHOR π
Copyright 2022 Ian Jackson and contributors to dkim-rotate. There is NO WARRANTY. SPDX-License-Identifier: GPL-3.0-or-later
π SEE ALSO π
dkim-rotate(5)β Configuration filedkim-rotate(1)β Command line referenceRFC6376β DKIM Signatures- Ok Google: please publish your DKIM secret keys β article by Matthew Green
https://blog.cryptographyengineering.com/2020/11/16/ok-google-please-publish-your-dkim-secret-keys/
And that's the dkim-rotate(7) page, bestie π source/version: dkim-rotate(7), and the date isn't in this footer β so just take this cute sign-off instead. periodt. π
π¦ where this page comes from
- ubuntu resolute: dkim-rotate 1.1build1
- ubuntu stonking: dkim-rotate 1.1build1
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms