From ded1b55ebd9feaad47bb4f01d20c5cd75cd9e8b0 Mon Sep 17 00:00:00 2001 From: "github@kiekerjan.isdronken.nl" Date: Sat, 11 Dec 2021 00:54:56 +0100 Subject: [PATCH] First steps in migrating to dkimpy-milter --- README.md | 2 ++ api/mailinabox.yml | 4 +-- management/dns_update.py | 53 ++++++++++++++++++---------- management/mail_log.py | 2 +- management/status_checks.py | 2 +- setup/dkim.sh | 70 ++++++++++++++++++++----------------- setup/mail-postfix.sh | 6 ++-- 7 files changed, 81 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index a3847729..4b706dc2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ Functionality changes and additions Using check-dnsbl.py from https://github.com/gsauthof/utility * Updated ssl security for web and email Removed older cryptos following internet.nl recommendations +* Replace opendkim with dkimpy (https://launchpad.net/dkimpy-milter) + Added support for Ed25519 signing Bug fixes * Munin error report fixed [see github issue](https://github.com/mail-in-a-box/mailinabox/issues/1555) 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 529abe27..1b755460 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -105,21 +105,22 @@ def do_dns_update(env, force=False): if len(updated_domains) > 0: 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" @@ -303,10 +304,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/dkim2/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/dkim2/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; "): @@ -817,14 +826,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/dkim2/box-rsa.key') + dkim_ed_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim2/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) || not os.path.exists(dkim_ed_key_file): + # Looks like DKIMpy is not installed. return False config = { @@ -846,7 +856,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 ), } @@ -854,18 +869,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 59c32c6e..69c182b0 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 90b4d175..03f7eff0 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..5aa60c16 100755 --- a/setup/dkim.sh +++ b/setup/dkim.sh @@ -1,8 +1,8 @@ #!/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. @@ -10,34 +10,34 @@ 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 +echo Installing DKIMpy/OpenDMARC... +apt_install dkimpy-milter opendmarc # Make sure configuration directories exist. -mkdir -p /etc/opendkim; -mkdir -p $STORAGE_ROOT/mail/dkim +mkdir -p /etc/dkim; +mkdir -p $STORAGE_ROOT/mail/dkim2 # 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 +if grep -q "ExternalIgnoreList" /etc/dkimpy-milter/dkimpy-milter.conf; then true # already done #NODOC else - # Add various configuration options to the end of `opendkim.conf`. - cat >> /etc/opendkim.conf << EOF; + # Add various configuration options to the end of `dkimpy-milter.conf`. + cat >> /etc/dkimpy-milter/dkimpy-milter.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 +ExternalIgnoreList refile:/etc/dkim/TrustedHosts +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 RequireSafeKeys false EOF fi @@ -48,17 +48,21 @@ 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/dkim2/box-rsa.key" ]; then + # All defaults are supposed to be ok, default key for rsa is 2048 bit + dknewkey --ktype rsa $STORAGE_ROOT/mail/dkim2/box-rsa + dknewkey --ktype ed25519 $STORAGE_ROOT/mail/dkim2/box-ed25519 + + # Force them into the format dns_update.py expects + sed -i 's/v=DKIM1;/box-rsa._domainkey IN TXT ( "v=DKIM1;/' $STORAGE_ROOT/mail/dkim2/box-rsa.dns + echo '" )' >> box-rsa.dns + sed -i 's/v=DKIM1;/box-ed25519._domainkey IN TXT ( "v=DKIM1;/' $STORAGE_ROOT/mail/dkim2/box-ed25519.dns + echo '" )' >> box-ed25519.dns fi -# Ensure files are owned by the opendkim user and are private otherwise. -chown -R opendkim:opendkim $STORAGE_ROOT/mail/dkim -chmod go-rwx $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/dkim2 +chmod go-rwx $STORAGE_ROOT/mail/dkim2 tools/editconf.py /etc/opendmarc.conf -s \ "Syslog=true" \ @@ -94,23 +98,23 @@ tools/editconf.py /etc/opendmarc.conf -s \ # 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 \ +tools/editconf.py /etc/dkimpy-milter/dkimpy-milter.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 +122,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 f0aa5d4e..308e1b53 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=VERIFYING -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