More Mail Server Tinkering
This morning I received an e-mail from a service I evidently subscribed to, that reported that my DANE DNS record was incorrect.
I've been trying to shore up my mail server's ability to identify itself, and join in trying to reduce imposter mail. The various DNS records are trying to convey information that matches what my mail server has, so that no one else can send mail on any of my domains' behalf.
The first is the simple MX record check. A mail server able to send from my domain should be listed in my MX servers. This is confirmed with a TXT SPF record that also lists the names and IPs of allowed servers. They should match.
There is a TXT DKIM record that contains a key that compliments the digital signature emitted from my mail server, so that only a server with the correct certificate could impersonate mine.
And there's a DMARC record, that provides details about where to report potential abuses and what to do with them, which is more about that reporting than prevention.
Finally, I've been trying to get the TLSA DANE records to work. Similar to the DKIM, there's a key in the DNS that matches the public data from a certificate on my server, and similar to DMARC, it has some instructions on how to handle misses.
I had everything working well, but knew there would be occasional blips, as the certificates I use on my server are from Let's Encrypt, and get replaced within 90 days. I set up an alert so that I could get in and intervene when the certificate renews, but the renewal process is a little fuzzy, renewing the certificates close to their ends, but not necessarily on that day. It seems this one (at least) renewed a couple days before my alert.
I did a quick DNS edit and added the new key to the DANE record. Then I set out to make a script to do this for me. As part of the Let's Encrypt renewal process, there's a folder that's checked for scripts to run when certificates are renewed. I use this to have a centralized Let's Encrypt "server" running, that does the cron job to do the renewals, and then the certificates are copied to other places where they're used, but where having that Let's Encrypt process is untenable, such as on my routers. The scripts generally copy the latest live certificate files to wherever they're necessary and then issue appropriate service restart or reload commands, usually via some SSH command, as the services aren't generally on the same server.
I added one today that leverages the API of the Cloudflare CDN to update the DNS records. Here, a little obfuscated, is the meat of the script:
# Set for the appropriate domain
DOMAIN='my.domain'
# Get the Zone ID from Cloudflare UI (Overview page)
ZONEID='ZONE_ID_FROM_CLOUDFLARE'
# This file contains the credentials used by certbot renew, for the Cloudflare API
. /etc/letsencrypt/certbot.cred
# Or provide the appropriate values for dns_cloudflare_email and dns_cloudflare_api_key
####
# To confirm the DNS record IDs, list the records and find the TLSA records
#
# curl "https://api.cloudflare.com/client/v4/zones/${ZONEID}/dns_records" -H "X-Auth-Email: ${dns_cloudflare_email}" -H "X-Auth-Key: ${dns_cloudflare_api_key}" | jq .
#
# The response will contain the parts like these (OBFUSCATED):
# {
# "id": "DNS_RECORD_ID",
# "zone_id": "ZONE_ID",
# "zone_name": "DOMAIN",
# "name": "_25._tcp.mail.DOMAIN",
# "type": "TLSA",
# "content": "3 1 1 CERTIFICATE_SHA",
# "proxiable": false,
# "proxied": false,
# "ttl": 1,
# "data": {
# "certificate": "CERTIFICATE_SHA",
# "matching_type": 1,
# "selector": 1,
# "usage": 3
# },
# "settings": {},
# "meta": {},
# "comment": null,
# "tags": [],
# "created_on": "DATE",
# "modified_on": "DATE"
# },
#
# And the other two, as suggested by the DANE docs, trimmed for brevity:
#
# "id": "DNS_RECORD_ID2",
# "name": "_465._tcp.mail.DOMAIN",
#
# "id": "DNS_RECORD_ID3",
# "name": "_587._tcp.mail.DOMAIN",
#
####
# This should create the correct DNS record for DANE TLSA when the certificate is updated
SHA=`openssl x509 -in /etc/letsencrypt/live/${DOMAIN}/fullchain.pem -noout -pubkey | openssl pkey -pubin -outform DER | openssl sha256 | awk '{ print $NF; }'`
DNSRECORD='DNS_RECORD_ID'
DATA="{\"type\":\"TLSA\",\"name\":\"_25._tcp.mail.${DOMAIN}\",\"content\":\"3 1 1 ${SHA}\",\"ttl\":1,\"proxied\":false,\"data\":{\"certificate\":\"${SHA}\",\"usage\":3,\"selector\":1,\"matching_type\":1}}"
curl -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONEID}/dns_records/${DNSRECORD}" \
-H "X-Auth-Email: ${dns_cloudflare_email}" -H "X-Auth-Key: ${dns_cloudflare_api_key}" \
-H "Content-Type: application/json" --data "${DATA}"
DNSRECORD='DNS_RECORD_ID2'
DATA="{\"type\":\"TLSA\",\"name\":\"_465._tcp.mail.${DOMAIN}\",\"content\":\"3 1 1 ${SHA}\",\"ttl\":1,\"proxied\":false,\"data\":{\"certificate\":\"${SHA}\",\"usage\":3,\"selector\":1,\"matching_type\":1}}"
curl -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONEID}/dns_records/${DNSRECORD}" \
-H "X-Auth-Email: ${dns_cloudflare_email}" -H "X-Auth-Key: ${dns_cloudflare_api_key}" \
-H "Content-Type: application/json" --data "${DATA}"
DNSRECORD='DNS_RECORD_ID3'
DATA="{\"type\":\"TLSA\",\"name\":\"_587._tcp.mail.${DOMAIN}\",\"content\":\"3 1 1 ${SHA}\",\"ttl\":1,\"proxied\":false,\"data\":{\"certificate\":\"${SHA}\",\"usage\":3,\"selector\":1,\"matching_type\":1}}"
curl -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONEID}/dns_records/${DNSRECORD}" \
-H "X-Auth-Email: ${dns_cloudflare_email}" -H "X-Auth-Key: ${dns_cloudflare_api_key}" \
-H "Content-Type: application/json" --data "${DATA}"
Of course, find and set the DOMAIN and ZONE_ID, and possibly the API values, to whatever is needed for the domain in question. Make the curl command to fetch the DNS records to find the appropriate domain record ID. The DANE instructions I followed suggested providing records for all three of the MTA ports, although the spec doesn't currently cover the TLS ports, so I had to find all three of the records, and make the corresponding DNS updates to all three.
Now I shouldn't have to scurry back in my bash history to try to find the commands. Hopefully this helps someone else, too!