Replace rspamd with OpenDKIM
OpenDKIM configuration has two Lua scripts defining strict DKIM policy. screen.lua filters out signatures that do not correspond to the From: domain so they are not even checked. final.lua rejects mail if it is not outgoing and has no valid DKIM signatures. OpenDKIM is configured as a milter on port 25 smtpd to check DKIM signatures and on mail reinjecting smtpd to sign outgoing messages with DKIM signatures.
This commit is contained in:
parent
a6b333672d
commit
7c7f1cad7f
@ -126,6 +126,101 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
|
||||
"""Configures OpenDKIM"""
|
||||
need_restart = False
|
||||
|
||||
server.group(name="Create opendkim group", group="opendkim", system=True)
|
||||
server.user(
|
||||
name="Add postfix user to opendkim group for socket access",
|
||||
user="postfix",
|
||||
groups=["opendkim"],
|
||||
system=True,
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
name="apt install opendkim opendkim-tools",
|
||||
packages=["opendkim", "opendkim-tools"],
|
||||
)
|
||||
|
||||
main_config = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/opendkim.conf"),
|
||||
dest="/etc/opendkim.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||
)
|
||||
need_restart |= main_config.changed
|
||||
|
||||
screen_script = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/screen.lua"),
|
||||
dest="/etc/opendkim/screen.lua",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= screen_script.changed
|
||||
|
||||
final_script = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/final.lua"),
|
||||
dest="/etc/opendkim/final.lua",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= final_script.changed
|
||||
|
||||
files.directory(
|
||||
name="Add opendkim directory to /etc",
|
||||
path="/etc/opendkim",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="750",
|
||||
present=True,
|
||||
)
|
||||
|
||||
keytable = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/KeyTable"),
|
||||
dest="/etc/dkimkeys/KeyTable",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="644",
|
||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||
)
|
||||
need_restart |= keytable.changed
|
||||
|
||||
signing_table = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/SigningTable"),
|
||||
dest="/etc/dkimkeys/SigningTable",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="644",
|
||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||
)
|
||||
need_restart |= signing_table.changed
|
||||
files.directory(
|
||||
name="Add opendkim socket directory to /var/spool/postfix",
|
||||
path="/var/spool/postfix/opendkim",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="750",
|
||||
present=True,
|
||||
)
|
||||
|
||||
if not host.get_fact(File, f"/etc/dkimkeys/{dkim_selector}.private"):
|
||||
server.shell(
|
||||
name="Generate OpenDKIM domain keys",
|
||||
commands=[
|
||||
f"opendkim-genkey -D /etc/dkimkeys -d {domain} -s {dkim_selector}"
|
||||
],
|
||||
_sudo=True,
|
||||
_sudo_user="opendkim",
|
||||
)
|
||||
|
||||
return need_restart
|
||||
|
||||
|
||||
def _install_mta_sts_daemon() -> bool:
|
||||
need_restart = False
|
||||
|
||||
@ -305,105 +400,9 @@ def _configure_nginx(domain: str, debug: bool = False) -> bool:
|
||||
return need_restart
|
||||
|
||||
|
||||
def remove_opendkim() -> None:
|
||||
"""Remove OpenDKIM, deprecated"""
|
||||
files.file(
|
||||
name="Remove legacy opendkim.conf",
|
||||
path="/etc/opendkim.conf",
|
||||
present=False,
|
||||
)
|
||||
|
||||
files.directory(
|
||||
name="Remove legacy opendkim socket directory from /var/spool/postfix",
|
||||
path="/var/spool/postfix/opendkim",
|
||||
present=False,
|
||||
)
|
||||
|
||||
apt.packages(name="Remove openDKIM", packages="opendkim", present=False)
|
||||
|
||||
|
||||
def _configure_rspamd(dkim_selector: str, mail_domain: str) -> bool:
|
||||
"""Configures rspamd for Rate Limiting."""
|
||||
need_restart = False
|
||||
|
||||
apt.packages(
|
||||
name="apt install rspamd",
|
||||
packages="rspamd",
|
||||
)
|
||||
|
||||
for module in ["phishing", "rbl", "hfilter", "ratelimit"]:
|
||||
disabled_module_conf = files.put(
|
||||
name=f"disable {module} rspamd plugin",
|
||||
src=importlib.resources.files(__package__).joinpath("rspamd/disabled.conf"),
|
||||
dest=f"/etc/rspamd/local.d/{module}.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= disabled_module_conf.changed
|
||||
|
||||
options_inc = files.put(
|
||||
name="disable fuzzy checks",
|
||||
src=importlib.resources.files(__package__).joinpath("rspamd/options.inc"),
|
||||
dest="/etc/rspamd/local.d/options.inc",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= options_inc.changed
|
||||
|
||||
# https://rspamd.com/doc/modules/force_actions.html
|
||||
force_actions_conf = files.put(
|
||||
name="Set up rules to reject on DKIM, SPF and DMARC fails",
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"rspamd/force_actions.conf"
|
||||
),
|
||||
dest="/etc/rspamd/local.d/force_actions.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= force_actions_conf.changed
|
||||
|
||||
dkim_directory = "/var/lib/rspamd/dkim/"
|
||||
dkim_key_path = f"{dkim_directory}{mail_domain}.{dkim_selector}.key"
|
||||
dkim_dns_file = f"{dkim_directory}{mail_domain}.{dkim_selector}.zone"
|
||||
|
||||
dkim_config = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"rspamd/dkim_signing.conf.j2"
|
||||
),
|
||||
dest="/etc/rspamd/local.d/dkim_signing.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
config={
|
||||
"dkim_selector": str(dkim_selector),
|
||||
"mail_domain": mail_domain,
|
||||
"dkim_key_path": dkim_key_path,
|
||||
},
|
||||
)
|
||||
need_restart |= dkim_config.changed
|
||||
|
||||
files.directory(
|
||||
name="ensure DKIM key directory exists",
|
||||
path=dkim_directory,
|
||||
present=True,
|
||||
user="_rspamd",
|
||||
group="_rspamd",
|
||||
)
|
||||
|
||||
if not host.get_fact(File, dkim_key_path):
|
||||
server.shell(
|
||||
name="Generate DKIM domain keys with rspamd",
|
||||
commands=[
|
||||
f"rspamadm dkim_keygen -b 2048 -s {dkim_selector} -d {mail_domain} -k {dkim_key_path} > {dkim_dns_file}"
|
||||
],
|
||||
_sudo=True,
|
||||
_sudo_user="_rspamd",
|
||||
)
|
||||
|
||||
return need_restart
|
||||
def _remove_rspamd() -> None:
|
||||
"""Remove rspamd"""
|
||||
apt.packages(name="Remove rspamd", packages="rspamd", present=False)
|
||||
|
||||
|
||||
def check_config(config):
|
||||
@ -494,15 +493,15 @@ def deploy_chatmail(config_path: Path) -> None:
|
||||
mta_sts_need_restart = _install_mta_sts_daemon()
|
||||
nginx_need_restart = _configure_nginx(mail_domain)
|
||||
|
||||
remove_opendkim()
|
||||
rspamd_need_restart = _configure_rspamd("dkim", mail_domain)
|
||||
_remove_rspamd()
|
||||
opendkim_need_restart = _configure_opendkim(mail_domain, "opendkim")
|
||||
|
||||
systemd.service(
|
||||
name="Start and enable rspamd",
|
||||
service="rspamd.service",
|
||||
name="Start and enable OpenDKIM",
|
||||
service="opendkim.service",
|
||||
running=True,
|
||||
enabled=True,
|
||||
restarted=rspamd_need_restart,
|
||||
restarted=opendkim_need_restart,
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
|
@ -61,9 +61,6 @@ def show_dns(args, out) -> int:
|
||||
continue
|
||||
line = line.replace("\t", " ")
|
||||
lines.append(line)
|
||||
lines[0] = f"dkim._domainkey.{mail_domain}. IN TXT " + lines[0].strip(
|
||||
"dkim._domainkey IN TXT "
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
print("Checking your DKIM keys and DNS entries...")
|
||||
@ -72,9 +69,7 @@ def show_dns(args, out) -> int:
|
||||
except subprocess.CalledProcessError:
|
||||
print("Please run `cmdeploy run` first.")
|
||||
return 1
|
||||
dkim_entry = read_dkim_entries(
|
||||
out.shell_output(f"{ssh} -- cat /var/lib/rspamd/dkim/{mail_domain}.dkim.zone")
|
||||
)
|
||||
dkim_entry = read_dkim_entries(out.shell_output(f"{ssh} -- opendkim-genzone -F"))
|
||||
|
||||
ipv6 = dns.get_ipv6()
|
||||
reverse_ipv6 = dns.check_ptr_record(ipv6, mail_domain)
|
||||
@ -140,7 +135,7 @@ def show_dns(args, out) -> int:
|
||||
continue
|
||||
if current != value:
|
||||
to_print.append(line)
|
||||
if " IN TXT ( " in line:
|
||||
if "IN TXT ( " in line:
|
||||
started_dkim_parsing = True
|
||||
dkim_lines = [line]
|
||||
if started_dkim_parsing and line.startswith('"'):
|
||||
@ -148,8 +143,8 @@ def show_dns(args, out) -> int:
|
||||
domain, data = "\n".join(dkim_lines).split(" IN TXT ")
|
||||
current = dns.get("TXT", domain.strip()[:-1])
|
||||
if current:
|
||||
current = "( %s" % (current.replace('" "', '"\n "'))
|
||||
if current != data:
|
||||
current = "( %s )" % (current.replace('" "', '"\n "'))
|
||||
if current.replace(";", "\\;") != data:
|
||||
to_print.append(dkim_entry)
|
||||
else:
|
||||
to_print.append(dkim_entry)
|
||||
|
@ -1 +1 @@
|
||||
dkim._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/dkim.private
|
||||
{{ config.opendkim_selector }}._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/{{ config.opendkim_selector }}.private
|
||||
|
28
cmdeploy/src/cmdeploy/opendkim/final.lua
Normal file
28
cmdeploy/src/cmdeploy/opendkim/final.lua
Normal file
@ -0,0 +1,28 @@
|
||||
if odkim.internal_ip(ctx) == 1 then
|
||||
-- Outgoing message will be signed,
|
||||
-- no need to look for signatures.
|
||||
return nil
|
||||
end
|
||||
|
||||
nsigs = odkim.get_sigcount(ctx)
|
||||
if nsigs == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, nsigs do
|
||||
sig = odkim.get_sighandle(ctx, i - 1)
|
||||
sigres = odkim.sig_result(sig)
|
||||
|
||||
-- All signatures that do not correspond to From:
|
||||
-- were ignored in screen.lua and return sigres -1.
|
||||
--
|
||||
-- Any valid signature that was not ignored like this
|
||||
-- means the message is acceptable.
|
||||
if sigres == 0 then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
odkim.set_reply(ctx, "554", "5.7.1", "No valid DKIM signature found")
|
||||
odkim.set_result(ctx, SMFIS_REJECT)
|
||||
return nil
|
@ -8,10 +8,12 @@ SyslogSuccess yes
|
||||
# oversigned, because it is often the identity key used by reputation systems
|
||||
# and thus somewhat security sensitive.
|
||||
Canonicalization relaxed/simple
|
||||
#Mode sv
|
||||
#SubDomains no
|
||||
OversignHeaders From
|
||||
|
||||
On-BadSignature reject
|
||||
On-KeyNotFound reject
|
||||
On-NoSignature reject
|
||||
|
||||
# Signing domain, selector, and key (required). For example, perform signing
|
||||
# for domain "example.com" with selector "2020" (2020._domainkey.example.com),
|
||||
# using the private key stored in /etc/dkimkeys/example.private. More granular
|
||||
@ -22,6 +24,15 @@ KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private
|
||||
KeyTable /etc/dkimkeys/KeyTable
|
||||
SigningTable refile:/etc/dkimkeys/SigningTable
|
||||
|
||||
# Sign Autocrypt header in addition to the default specified in RFC 6376.
|
||||
SignHeaders *,+autocrypt
|
||||
|
||||
# Script to ignore signatures that do not correspond to the From: domain.
|
||||
ScreenPolicyScript /etc/opendkim/screen.lua
|
||||
|
||||
# Script to reject mails without a valid DKIM signature.
|
||||
FinalPolicyScript /etc/opendkim/final.lua
|
||||
|
||||
# In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
|
||||
# using a local socket with MTAs that access the socket as a non-privileged
|
||||
# user (for example, Postfix). You may need to add user "postfix" to group
|
||||
@ -29,22 +40,10 @@ SigningTable refile:/etc/dkimkeys/SigningTable
|
||||
UserID opendkim
|
||||
UMask 007
|
||||
|
||||
# Socket for the MTA connection (required). If the MTA is inside a chroot jail,
|
||||
# it must be ensured that the socket is accessible. In Debian, Postfix runs in
|
||||
# a chroot in /var/spool/postfix, therefore a Unix socket would have to be
|
||||
# configured as shown on the last line below.
|
||||
#Socket local:/run/opendkim/opendkim.sock
|
||||
#Socket inet:8891@localhost
|
||||
#Socket inet:8891
|
||||
Socket local:/var/spool/postfix/opendkim/opendkim.sock
|
||||
|
||||
PidFile /run/opendkim/opendkim.pid
|
||||
|
||||
# Hosts for which to sign rather than verify, default is 127.0.0.1. See the
|
||||
# OPERATION section of opendkim(8) for more information.
|
||||
#InternalHosts 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12
|
||||
|
||||
# The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided
|
||||
# by the package dns-root-data.
|
||||
TrustAnchorFile /usr/share/dns/root.key
|
||||
#Nameservers 127.0.0.1
|
||||
|
21
cmdeploy/src/cmdeploy/opendkim/screen.lua
Normal file
21
cmdeploy/src/cmdeploy/opendkim/screen.lua
Normal file
@ -0,0 +1,21 @@
|
||||
-- Ignore signatures that do not correspond to the From: domain.
|
||||
|
||||
from_domain = odkim.get_fromdomain(ctx)
|
||||
if from_domain == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
n = odkim.get_sigcount(ctx)
|
||||
if n == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, n do
|
||||
sig = odkim.get_sighandle(ctx, i - 1)
|
||||
sig_domain = odkim.sig_getdomain(sig)
|
||||
if from_domain ~= sig_domain then
|
||||
odkim.sig_ignore(sig)
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
@ -45,6 +45,3 @@ inet_protocols = all
|
||||
|
||||
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||
virtual_mailbox_domains = {{ config.mail_domain }}
|
||||
|
||||
smtpd_milters = inet:127.0.0.1:11332
|
||||
non_smtpd_milters = $smtpd_milters
|
||||
|
@ -11,9 +11,10 @@
|
||||
# ==========================================================================
|
||||
{% if debug == true %}
|
||||
smtp inet n - y - - smtpd -v
|
||||
{% else %}
|
||||
{%- else %}
|
||||
smtp inet n - y - - smtpd
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
submission inet n - y - - smtpd
|
||||
-o syslog_name=postfix/submission
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
@ -78,6 +79,7 @@ filter unix - n n - - lmtp
|
||||
# Local SMTP server for reinjecting filered mail.
|
||||
localhost:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
|
||||
-o syslog_name=postfix/reinject
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
-o cleanup_service_name=authclean
|
||||
|
||||
# Cleanup `Received` headers for authenticated mail
|
||||
|
@ -1 +0,0 @@
|
||||
enabled = false;
|
@ -1,10 +0,0 @@
|
||||
selector = {{ config.dkim_selector }}
|
||||
use_esld = false # don't cut c1.testrun.org down to testrun.org
|
||||
domain = {
|
||||
{{ config.mail_domain }} {
|
||||
selectors [
|
||||
selector = {{ config.dkim_selector }}
|
||||
path = {{ config.dkim_key_path }}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
rules {
|
||||
## Reject on missing or invalid DKIM signatures.
|
||||
##
|
||||
## We require DKIM signature on incoming mails regardless of DMARC policy.
|
||||
|
||||
# R_DKIM_REJECT: DKIM reject inserted by `dkim` module.
|
||||
REJECT_INVALID_DKIM {
|
||||
action = "reject";
|
||||
expression = "R_DKIM_REJECT";
|
||||
message = "Rejected due to invalid DKIM signature";
|
||||
}
|
||||
|
||||
# R_DKIM_PERMFAIL: permanent failure inserted by `dkim` module e.g. no DKIM DNS record found.
|
||||
REJECT_PERMFAIL_DKIM {
|
||||
action = "reject";
|
||||
expression = "R_DKIM_PERMFAIL";
|
||||
message = "Rejected due to missing DKIM DNS entry";
|
||||
}
|
||||
|
||||
# No DKIM signature (R_DKIM_NA symbol inserted by `dkim` module).
|
||||
REJECT_MISSING_DKIM {
|
||||
action = "reject";
|
||||
expression = "R_DKIM_NA";
|
||||
message = "Rejected due to missing DKIM signature";
|
||||
}
|
||||
|
||||
|
||||
## Reject on SPF failure.
|
||||
|
||||
# - SPF failure (R_SPF_FAIL)
|
||||
# - SPF permanent failure, e.g. failed to resolve DNS record referenced from SPF (R_SPF_PERMFAIL)
|
||||
REJECT_SPF {
|
||||
action = "reject";
|
||||
expression = "R_SPF_FAIL | R_SPF_PERMFAIL";
|
||||
message = "Rejected due to failed SPF check";
|
||||
}
|
||||
|
||||
# Reject on DMARC policy check failure.
|
||||
REJECT_DMARC {
|
||||
action = "reject";
|
||||
expression = "DMARC_POLICY_REJECT";
|
||||
message = "Rejected due to DMARC policy";
|
||||
}
|
||||
|
||||
|
||||
# Do not reject if:
|
||||
# - R_DKIM_TEMPFAIL, it is a DNS resolution failure
|
||||
# and we do not want to lose messages because of faulty network.
|
||||
#
|
||||
# - R_SPF_SOFTFAIL
|
||||
# - R_SPF_NEUTRAL
|
||||
# - R_SPF_DNSFAIL
|
||||
# - R_SPF_NA
|
||||
#
|
||||
# - DMARC_DNSFAIL
|
||||
# - DMARC_NA
|
||||
# - DMARC_POLICY_SOFTFAIL
|
||||
# - DMARC_POLICY_QUARANTINE
|
||||
# - DMARC_BAD_POLICY
|
||||
}
|
@ -1 +0,0 @@
|
||||
filters = "dkim";
|
Loading…
Reference in New Issue
Block a user