Skip to content
Merged
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
213 changes: 109 additions & 104 deletions cmdeploy/src/cmdeploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,107 @@ 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="Create opendkim user",
user="opendkim",
groups=["opendkim"],
system=True,
)
server.user(
name="Add postfix user to opendkim group for socket access",
user="postfix",
groups=["opendkim"],
system=True,
)

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,
)

apt.packages(
name="apt install opendkim opendkim-tools",
packages=["opendkim", "opendkim-tools"],
)

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

Expand Down Expand Up @@ -305,105 +406,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):
Expand Down Expand Up @@ -494,15 +499,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,
)
Comment on lines 505 to 511
Copy link
Contributor

@missytake missytake Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, starting the opendkim service during cmdeploy fails for me (on c1):

Jan 23 17:06:42 c1 systemd[1]: Starting opendkim.service - OpenDKIM Milter...
Jan 23 17:06:42 c1 opendkim[359568]: opendkim: milter socket must be specified
Jan 23 17:06:42 c1 opendkim[359568]:         (use "-?" for help)
Jan 23 17:06:42 c1 systemd[1]: opendkim.service: Control process exited, code=exited, status=78/CONFIG
Jan 23 17:06:42 c1 systemd[1]: opendkim.service: Failed with result 'exit-code'.
Jan 23 17:06:42 c1 systemd[1]: Failed to start opendkim.service - OpenDKIM Milter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But /etc/opendkim.conf has it:

Socket                  local:/var/spool/postfix/opendkim/opendkim.sock

The file however does not exist:

ls /var/spool/postfix/opendkim/opendkim.sock
ls: cannot access '/var/spool/postfix/opendkim/opendkim.sock': No such file or directory

Maybe it is created by postfix? Should we maybe reconfigure postfix first so it creates the socket?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restarted opendkim with systemctl restart opendkim, it started and created the socket. :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem deploying to c2:

...
--> Starting operation: apt install opendkim opendkim-tools 
    [c2.testrun.org] Success

--> Starting operation: Files/Template (src=/home/user/src/deltachat/chatmail/cmdeploy/src/cmdeploy/opendkim/opendkim.conf, dest=/etc/opendkim.conf, user=root, group=root, mode=644, config={'domain_name': 'c2.testrun.org', 'opendkim_selector': 'opendkim'})
    [c2.testrun.org] Success

--> Starting operation: Files/Put (src=/home/user/src/deltachat/chatmail/cmdeploy/src/cmdeploy/opendkim/screen.lua, dest=/etc/opendkim/screen.lua, user=root, group=root, mode=644)
    [c2.testrun.org] Success

--> Starting operation: Files/Put (src=/home/user/src/deltachat/chatmail/cmdeploy/src/cmdeploy/opendkim/final.lua, dest=/etc/opendkim/final.lua, user=root, group=root, mode=644)
    [c2.testrun.org] Success

--> Starting operation: Add opendkim directory to /etc 
    [c2.testrun.org] No changes

--> Starting operation: Files/Template (src=/home/user/src/deltachat/chatmail/cmdeploy/src/cmdeploy/opendkim/KeyTable, dest=/etc/dkimkeys/KeyTable, user=opendkim, group=opendkim, mode=644, config={'domain_name': 'c2.testrun.org', 'opendkim_selector': 'opendkim'})
    [c2.testrun.org] Success

--> Starting operation: Files/Template (src=/home/user/src/deltachat/chatmail/cmdeploy/src/cmdeploy/opendkim/SigningTable, dest=/etc/dkimkeys/SigningTable, user=opendkim, group=opendkim, mode=644, config={'domain_name': 'c2.testrun.org', 'opendkim_selector': 'opendkim'})
    [c2.testrun.org] Success

--> Starting operation: Add opendkim socket directory to /var/spool/postfix 
    [c2.testrun.org] Success

--> Starting operation: Generate OpenDKIM domain keys 
    [c2.testrun.org] Success

--> Starting operation: Start and enable OpenDKIM 
    [c2.testrun.org] Job for opendkim.service failed because the control process exited with error code.
    [c2.testrun.org] See "systemctl status opendkim.service" and "journalctl -xeu opendkim.service" for details.
    [c2.testrun.org] Error: executed 0/2 commands
--> pyinfra error: No hosts remaining!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply running cmdeploy run second time works, but I cannot figure out why /etc/opendkim.conf is consistently not found and read the first time.


systemd.service(
Expand Down
13 changes: 4 additions & 9 deletions cmdeploy/src/cmdeploy/dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ def read_dkim_entries(entry):
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...")
Expand All @@ -72,9 +69,7 @@ def read_dkim_entries(entry):
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)
Expand Down Expand Up @@ -140,16 +135,16 @@ def read_dkim_entries(entry):
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('"'):
dkim_lines.append(" " + line)
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)
Expand Down
2 changes: 1 addition & 1 deletion cmdeploy/src/cmdeploy/opendkim/KeyTable
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions cmdeploy/src/cmdeploy/opendkim/final.lua
Original file line number Diff line number Diff line change
@@ -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
27 changes: 13 additions & 14 deletions cmdeploy/src/cmdeploy/opendkim/opendkim.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,29 +24,26 @@ 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
# "opendkim" in that case.
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 changes: 21 additions & 0 deletions cmdeploy/src/cmdeploy/opendkim/screen.lua
Original file line number Diff line number Diff line change
@@ -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
3 changes: 0 additions & 3 deletions cmdeploy/src/cmdeploy/postfix/main.cf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading