From 2895391d63d8791d1c79383f09f65184b37e7603 Mon Sep 17 00:00:00 2001 From: Stefan Schueffler Date: Thu, 27 Jun 2024 15:54:00 +0200 Subject: [PATCH] Add support for CNAME delegated DKIM keys In order to use a DKIM public key, it has to be published in the DNS system. The most easy setup simply puts it directly into the appropriate TXT record. A more advanced setup also allows them to be delegated to some other record pointed to by a CNAME record. As an example, both variants are equivalent in terms of DKIM key validation: selector1._domainkey.example.org IN TXT "v=DKIM1; p=..." and selector1._domainkey.example.org IN CNAME "somekey.mailprovider.org." somekey.mailprovider.org IN TXT "v=DKIM1; p=..." So, the actual key management can be delegated to some other party by means of using a CNAME. This is of special importance for infrastructure automation and DKIM key rotation (we just ask the customer / domain dns owner once to add the appropriate CNAME pointing to the corresponding record on our own domain, and then we're able to modify the DKIM keys on our own without requiring the customers to update their DNS records all the time). As key rotation is a current best practice, the added CNAME support will ease the respective implementation. This commit changes the code to first check a TXT record (as before), and in addition checks a CNAME record (and if found, check the record it is referring to - up to a limit of 10 recursion steps to avoid endless recursions). --- app/models/concerns/has_dns_checks.rb | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/has_dns_checks.rb b/app/models/concerns/has_dns_checks.rb index 789aeffb..7e0e15dc 100644 --- a/app/models/concerns/has_dns_checks.rb +++ b/app/models/concerns/has_dns_checks.rb @@ -73,18 +73,27 @@ def check_spf_record! def check_dkim_record domain = "#{dkim_record_name}.#{name}" + check_dkim_record_recursive(domain, domain, 0) + end + + def check_dkim_record_recursive(originaldomain, domain, level) records = resolver.txt(domain) if records.empty? - self.dkim_status = "Missing" - self.dkim_error = "No TXT records were returned for #{domain}" + records = resolver.cname(domain) + if (!records.empty? && records.size == 1 && level < 10) + return check_dkim_record_recursive(originaldomain, records.first, level+1) + else + self.dkim_status = "Missing" + self.dkim_error = "No TXT records were returned for #{originaldomain}" + end else sanitised_dkim_record = records.first.strip.ends_with?(";") ? records.first.strip : "#{records.first.strip};" if records.size > 1 self.dkim_status = "Invalid" - self.dkim_error = "There are #{records.size} records for at #{domain}. There should only be one." + self.dkim_error = "There are #{records.size} records for at #{originaldomain}. There should only be one." elsif sanitised_dkim_record != dkim_record self.dkim_status = "Invalid" - self.dkim_error = "The DKIM record at #{domain} does not match the record we have provided. Please check it has been copied correctly." + self.dkim_error = "The DKIM record at #{originaldomain} does not match the record we have provided. Please check it has been copied correctly." else self.dkim_status = "OK" self.dkim_error = nil