From dd2d691fc7becc8f23fa5674fc7d3dd401a4899d Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Wed, 16 Nov 2022 21:18:29 +0100 Subject: [PATCH] Replace opendkim with dkimpy --- api/mailinabox.yml | 4 +- management/dns_update.py | 53 +++++++++++++++-------- management/mail_log.py | 2 +- management/status_checks.py | 2 +- setup/dkim.sh | 86 +++++++++++++++++-------------------- setup/mail-postfix.sh | 6 ++- 6 files changed, 81 insertions(+), 72 deletions(-) diff --git a/api/mailinabox.yml b/api/mailinabox.yml index f3290fb9..2b45fbd1 100644 --- a/api/mailinabox.yml +++ b/api/mailinabox.yml @@ -1262,7 +1262,7 @@ paths: $ref: '#/components/schemas/MailUserAddResponse' example: | mail user added - updated DNS: OpenDKIM configuration + updated DNS: DKIM configuration 400: description: Bad request content: @@ -1863,7 +1863,7 @@ components: type: string example: | mail user added - updated DNS: OpenDKIM configuration + updated DNS: DKIM configuration description: | Mail user add response. diff --git a/management/dns_update.py b/management/dns_update.py index 2bfc104f..f8c7edc8 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -109,21 +109,22 @@ def do_dns_update(env, force=False): except: shell('check_call', ["/usr/sbin/service", "nsd", "restart"]) - # Write the OpenDKIM configuration tables for all of the mail domains. + # Write the DKIM configuration tables for all of the mail domains. from mailconfig import get_mail_domains - if write_opendkim_tables(get_mail_domains(env), env): - # Settings changed. Kick opendkim. - shell('check_call', ["/usr/sbin/service", "opendkim", "restart"]) + + if write_dkim_tables(get_mail_domains(env), env): + # Settings changed. Kick dkimpy. + shell('check_call', ["/usr/sbin/service", "dkimpy-milter", "restart"]) if len(updated_domains) == 0: # If this is the only thing that changed? - updated_domains.append("OpenDKIM configuration") + updated_domains.append("DKIM configuration") # Clear bind9's DNS cache so our own DNS resolver is up to date. # (ignore errors with trap=True) shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True) if len(updated_domains) == 0: - # if nothing was updated (except maybe OpenDKIM's files), don't show any output + # if nothing was updated (except maybe DKIM's files), don't show any output return "" else: return "updated DNS: " + ",".join(updated_domains) + "\n" @@ -295,10 +296,18 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) if not has_rec(None, "TXT", prefix="v=spf1 "): records.append((None, "TXT", 'v=spf1 mx -all', "Recommended. Specifies that only the box is permitted to send @%s mail." % domain)) - # Append the DKIM TXT record to the zone as generated by OpenDKIM. + # Append the DKIM TXT record to the zone as generated by DKIMpy. # Skip if the user has set a DKIM record already. - opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt') - with open(opendkim_record_file) as orf: + dkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/box-rsa.dns') + with open(dkim_record_file) as orf: + m = re.match(r'(\S+)\s+IN\s+TXT\s+\( ((?:"[^"]+"\s+)+)\)', orf.read(), re.S) + val = "".join(re.findall(r'"([^"]+)"', m.group(2))) + if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "): + records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain)) + + # Also add a ed25519 DKIM record + dkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/box-ed25519.dns') + with open(dkim_record_file) as orf: m = re.match(r'(\S+)\s+IN\s+TXT\s+\( ((?:"[^"]+"\s+)+)\)', orf.read(), re.S) val = "".join(re.findall(r'"([^"]+)"', m.group(2))) if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "): @@ -760,14 +769,15 @@ def sign_zone(domain, zonefile, env): ######################################################################## -def write_opendkim_tables(domains, env): - # Append a record to OpenDKIM's KeyTable and SigningTable for each domain +def write_dkim_tables(domains, env): + # Append a record to DKIMpy's KeyTable and SigningTable for each domain # that we send mail from (zones and all subdomains). - opendkim_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private') + dkim_rsa_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/box-rsa.key') + dkim_ed_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/box-ed25519.key') - if not os.path.exists(opendkim_key_file): - # Looks like OpenDKIM is not installed. + if not os.path.exists(dkim_rsa_key_file) or not os.path.exists(dkim_ed_key_file): + # Looks like DKIMpy is not installed. return False config = { @@ -789,7 +799,12 @@ def write_opendkim_tables(domains, env): # signing domain must match the sender's From: domain. "KeyTable": "".join( - "{domain} {domain}:mail:{key_file}\n".format(domain=domain, key_file=opendkim_key_file) + "{domain} {domain}:box-rsa:{key_file}\n".format(domain=domain, key_file=dkim_rsa_key_file) + for domain in domains + ), + "KeyTableEd25519": + "".join( + "{domain} {domain}:box-ed25519:{key_file}\n".format(domain=domain, key_file=dkim_ed_key_file) for domain in domains ), } @@ -797,18 +812,18 @@ def write_opendkim_tables(domains, env): did_update = False for filename, content in config.items(): # Don't write the file if it doesn't need an update. - if os.path.exists("/etc/opendkim/" + filename): - with open("/etc/opendkim/" + filename) as f: + if os.path.exists("/etc/dkim/" + filename): + with open("/etc/dkim/" + filename) as f: if f.read() == content: continue # The contents needs to change. - with open("/etc/opendkim/" + filename, "w") as f: + with open("/etc/dkim/" + filename, "w") as f: f.write(content) did_update = True # Return whether the files changed. If they didn't change, there's - # no need to kick the opendkim process. + # no need to kick the dkimpy process. return did_update ######################################################################## diff --git a/management/mail_log.py b/management/mail_log.py index bdf757cc..5fa27876 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -376,7 +376,7 @@ def scan_mail_log_line(line, collector): if SCAN_BLOCKED: scan_postfix_smtpd_line(date, log, collector) elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup", "postfix/scache", - "spampd", "postfix/anvil", "postfix/master", "opendkim", "postfix/lmtp", + "spampd", "postfix/anvil", "postfix/master", "dkimpy", "postfix/lmtp", "postfix/tlsmgr", "anvil"): # nothing to look at return True diff --git a/management/status_checks.py b/management/status_checks.py index 0d555441..6f172eb4 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -28,7 +28,7 @@ def get_services(): { "name": "Dovecot LMTP LDA", "port": 10026, "public": False, }, { "name": "Postgrey", "port": 10023, "public": False, }, { "name": "Spamassassin", "port": 10025, "public": False, }, - { "name": "OpenDKIM", "port": 8891, "public": False, }, + { "name": "DKIMpy", "port": 8892, "public": False, }, { "name": "OpenDMARC", "port": 8893, "public": False, }, { "name": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, }, { "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, }, diff --git a/setup/dkim.sh b/setup/dkim.sh index b2541a12..c9ba1e8b 100755 --- a/setup/dkim.sh +++ b/setup/dkim.sh @@ -1,46 +1,43 @@ #!/bin/bash -# OpenDKIM +# DKIM # -------- # -# OpenDKIM provides a service that puts a DKIM signature on outbound mail. +# DKIMpy provides a service that puts a DKIM signature on outbound mail. # # The DNS configuration for DKIM is done in the management daemon. source setup/functions.sh # load our functions source /etc/mailinabox.conf # load global vars -# Install DKIM... -echo Installing OpenDKIM/OpenDMARC... -apt_install opendkim opendkim-tools opendmarc +# Remove openDKIM if present +apt-get purge -qq -y opendkim opendkim-tools + +# Install DKIMpy-Milter +echo Installing DKIMpy/OpenDMARC... +apt_install dkimpy-milter python3-dkim opendmarc # Make sure configuration directories exist. -mkdir -p /etc/opendkim; +mkdir -p /etc/dkim; mkdir -p $STORAGE_ROOT/mail/dkim # Used in InternalHosts and ExternalIgnoreList configuration directives. # Not quite sure why. -echo "127.0.0.1" > /etc/opendkim/TrustedHosts +echo "127.0.0.1" > /etc/dkim/TrustedHosts # We need to at least create these files, since we reference them later. -# Otherwise, opendkim startup will fail -touch /etc/opendkim/KeyTable -touch /etc/opendkim/SigningTable +touch /etc/dkim/KeyTable +touch /etc/dkim/SigningTable -if grep -q "ExternalIgnoreList" /etc/opendkim.conf; then - true # already done #NODOC -else - # Add various configuration options to the end of `opendkim.conf`. - cat >> /etc/opendkim.conf << EOF; -Canonicalization relaxed/simple -MinimumKeyBits 1024 -ExternalIgnoreList refile:/etc/opendkim/TrustedHosts -InternalHosts refile:/etc/opendkim/TrustedHosts -KeyTable refile:/etc/opendkim/KeyTable -SigningTable refile:/etc/opendkim/SigningTable -Socket inet:8891@127.0.0.1 -RequireSafeKeys false -EOF -fi +tools/editconf.py /etc/dkimpy-milter/dkimpy-milter.conf -s \ + "MacroList=daemon_name|ORIGINATING" \ + "MacroListVerify=daemon_name|VERIFYING" \ + "Canonicalization=relaxed/simple" \ + "MinimumKeyBits=1024" \ + "InternalHosts=refile:/etc/dkim/TrustedHosts" \ + "KeyTable=refile:/etc/dkim/KeyTable" \ + "KeyTableEd25519=refile:/etc/dkim/KeyTableEd25519" \ + "SigningTable=refile:/etc/dkim/SigningTable" \ + "Socket=inet:8892@127.0.0.1" # Create a new DKIM key. This creates mail.private and mail.txt # in $STORAGE_ROOT/mail/dkim. The former is the private key and @@ -48,16 +45,20 @@ fi # in our DNS setup. Note that the files are named after the # 'selector' of the key, which we can change later on to support # key rotation. -# -# A 1024-bit key is seen as a minimum standard by several providers -# such as Google. But they and others use a 2048 bit key, so we'll -# do the same. Keys beyond 2048 bits may exceed DNS record limits. -if [ ! -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then - opendkim-genkey -b 2048 -r -s mail -D $STORAGE_ROOT/mail/dkim +if [ ! -f "$STORAGE_ROOT/mail/dkim/box-rsa.key" ]; then + # All defaults are supposed to be ok, default key for rsa is 2048 bit + dknewkey --ktype rsa $STORAGE_ROOT/mail/dkim/box-rsa + dknewkey --ktype ed25519 $STORAGE_ROOT/mail/dkim/box-ed25519 + + # Force them into the format dns_update.py expects + sed -i 's/v=DKIM1;/box-rsa._domainkey IN TXT ( "v=DKIM1; s=email;/' $STORAGE_ROOT/mail/dkim/box-rsa.dns + echo '" )' >> $STORAGE_ROOT/mail/dkim/box-rsa.dns + sed -i 's/v=DKIM1;/box-ed25519._domainkey IN TXT ( "v=DKIM1; s=email;/' $STORAGE_ROOT/mail/dkim/box-ed25519.dns + echo '" )' >> $STORAGE_ROOT/mail/dkim/box-ed25519.dns fi -# Ensure files are owned by the opendkim user and are private otherwise. -chown -R opendkim:opendkim $STORAGE_ROOT/mail/dkim +# Ensure files are owned by the dkimpy-milter user and are private otherwise. +chown -R dkimpy-milter:dkimpy-milter $STORAGE_ROOT/mail/dkim chmod go-rwx $STORAGE_ROOT/mail/dkim tools/editconf.py /etc/opendmarc.conf -s \ @@ -88,29 +89,20 @@ tools/editconf.py /etc/opendmarc.conf -s \ tools/editconf.py /etc/opendmarc.conf -s \ "FailureReportsOnNone=true" -# AlwaysAddARHeader Adds an "Authentication-Results:" header field even to -# unsigned messages from domains with no "signs all" policy. The reported DKIM -# result will be "none" in such cases. Normally unsigned mail from non-strict -# domains does not cause the results header field to be added. This added header -# is used by spamassassin to evaluate the mail for spamminess. - -tools/editconf.py /etc/opendkim.conf -s \ - "AlwaysAddARHeader=true" - -# Add OpenDKIM and OpenDMARC as milters to postfix, which is how OpenDKIM +# Add DKIMpy and OpenDMARC as milters to postfix, which is how DKIMpy # intercepts outgoing mail to perform the signing (by adding a mail header) # and how they both intercept incoming mail to add Authentication-Results # headers. The order possibly/probably matters: OpenDMARC relies on the -# OpenDKIM Authentication-Results header already being present. +# DKIM Authentication-Results header already being present. # # Be careful. If we add other milters later, this needs to be concatenated # on the smtpd_milters line. # # The OpenDMARC milter is skipped in the SMTP submission listener by -# configuring smtpd_milters there to only list the OpenDKIM milter +# configuring smtpd_milters there to only list the DKIMpy milter # (see mail-postfix.sh). tools/editconf.py /etc/postfix/main.cf \ - "smtpd_milters=inet:127.0.0.1:8891 inet:127.0.0.1:8893"\ + "smtpd_milters=inet:127.0.0.1:8892 inet:127.0.0.1:8893"\ non_smtpd_milters=\$smtpd_milters \ milter_default_action=accept @@ -118,7 +110,7 @@ tools/editconf.py /etc/postfix/main.cf \ hide_output systemctl enable opendmarc # Restart services. -restart_service opendkim +restart_service dkimpy-milter restart_service opendmarc restart_service postfix diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 5787e8c9..bcc6d092 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -91,12 +91,14 @@ tools/editconf.py /etc/postfix/master.cf -s -w \ -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes -o syslog_name=postfix/submission - -o smtpd_milters=inet:127.0.0.1:8891 + -o smtpd_milters=inet:127.0.0.1:8892 + -o milter_macro_daemon_name=ORIGINATING -o cleanup_service_name=authclean" \ "submission=inet n - - - - smtpd -o smtpd_sasl_auth_enable=yes -o syslog_name=postfix/submission - -o smtpd_milters=inet:127.0.0.1:8891 + -o smtpd_milters=inet:127.0.0.1:8892 + -o milter_macro_daemon_name=ORIGINATING -o smtpd_tls_security_level=encrypt -o cleanup_service_name=authclean" \ "authclean=unix n - - - 0 cleanup