Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace opendkim with dkimpy #2220

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/mailinabox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -1863,7 +1863,7 @@ components:
type: string
example: |
mail user added
updated DNS: OpenDKIM configuration
updated DNS: DKIM configuration
description: |
Mail user add response.

Expand Down
53 changes: 34 additions & 19 deletions management/dns_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,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"
Expand Down Expand Up @@ -289,10 +290,18 @@ def has_rec(qname, rtype, prefix=None):
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, encoding="utf-8") as orf:
dkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.dns')
with open(dkim_record_file, encoding="utf-8") 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, encoding="utf-8") 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; "):
Expand Down Expand Up @@ -748,14 +757,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/mail.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 = {
Expand All @@ -777,26 +787,31 @@ def write_opendkim_tables(domains, env):
# signing domain must match the sender's From: domain.
"KeyTable":
"".join(
f"{domain} {domain}:mail:{opendkim_key_file}\n"
f"{domain} {domain}:mail:{dkim_rsa_key_file}\n"
for domain in domains
),
"KeyTableEd25519":
"".join(
f"{domain} {domain}:box-ed25519:{dkim_ed_key_file}\n"
for domain in domains
),
}

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, encoding="utf-8") as f:
if os.path.exists("/etc/dkim/" + filename):
with open("/etc/dkim/" + filename, encoding="utf-8") as f:
if f.read() == content:
continue

# The contents needs to change.
with open("/etc/opendkim/" + filename, "w", encoding="utf-8") as f:
with open("/etc/dkim/" + filename, "w", encoding="utf-8") 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

########################################################################
Expand Down
2 changes: 1 addition & 1 deletion management/mail_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,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
Expand Down
2 changes: 1 addition & 1 deletion management/status_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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, },
Expand Down
120 changes: 68 additions & 52 deletions setup/dkim.sh
Original file line number Diff line number Diff line change
@@ -1,64 +1,89 @@
#!/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 "$STORAGE_ROOT/mail/dkim"
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

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:[email protected]
RequireSafeKeys false
EOF
fi

# Create a new DKIM key. This creates mail.private and mail.txt
touch /etc/dkim/KeyTable
touch /etc/dkim/SigningTable

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:[email protected]"

# Create a new DKIM key. This creates mail.key and mail.dns
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
# the latter is the suggested DNS TXT entry which we'll include
# 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/mail.key" ]; then
# Check if there is an existing rsa key
if [ -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then
# Re-use existing key
cp -f $STORAGE_ROOT/mail/dkim/mail.private $STORAGE_ROOT/mail/dkim/mail.key
cp -f $STORAGE_ROOT/mail/dkim/mail.txt $STORAGE_ROOT/mail/dkim/mail.dns
else
# All defaults are supposed to be ok, default key for rsa is 2048 bit
dknewkey --ktype rsa $STORAGE_ROOT/mail/dkim/mail

# Force dns entry into the format dns_update.py expects
# We use selector mail for the rsa key, to be compatible with earlier installations of Mail-in-a-Box
sed -i 's/v=DKIM1;/mail._domainkey IN TXT ( "v=DKIM1; s=email;/' $STORAGE_ROOT/mail/dkim/mail.dns
echo '" )' >> $STORAGE_ROOT/mail/dkim/mail.dns
fi

# Change format from pkcs#8 to pkcs#1, dkimpy seemingly is not able to handle the #8 format
# See bug https://bugs.launchpad.net/dkimpy/+bug/1978835
line=$(head -n 1 mail.key)
if [ ! "$line" = "-----BEGIN RSA PRIVATE KEY-----" ]; then
# Generate pkcs#1 key from the pkcs#8 key
openssl pkey -in $STORAGE_ROOT/mail/dkim/mail.key -traditional -out $STORAGE_ROOT/mail/dkim/mail.key.1
mv -f $STORAGE_ROOT/mail/dkim/mail.key $STORAGE_ROOT/mail/dkim/mail.key.8
cp -f $STORAGE_ROOT/mail/dkim/mail.key.1 $STORAGE_ROOT/mail/dkim/mail.key
fi
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"
if [ ! -f "$STORAGE_ROOT/mail/dkim/box-ed25519.key" ]; then
# Generate ed25519 key
dknewkey --ktype ed25519 $STORAGE_ROOT/mail/dkim/box-ed25519

# For the ed25519 dns entry, we use selector box-ed25519
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 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 \
"Syslog=true" \
Expand Down Expand Up @@ -88,37 +113,28 @@ tools/editconf.py /etc/opendmarc.conf -s \
tools/editconf.py /etc/opendmarc.conf -s \
"FailureReportsOnNone=false"

# 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

# We need to explicitly enable the opendmarc service, or it will not start
hide_output systemctl enable opendmarc

# Restart services.
restart_service opendkim
restart_service dkimpy-milter
restart_service opendmarc
restart_service postfix

6 changes: 4 additions & 2 deletions setup/mail-postfix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,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
Expand Down