Administration¶
In the following chapter we explain the inner workings of Æther including monitoring and usermanagement.
Managing User accounts¶
- User management is done through a centralized LDAP server which is connected to:
Nextcloud
Postfix (MTA, through dovecot)
Dovecot (MDA)
Mailman (Malinglists)
Grafana (Monitoring)
Users can be added to groups which grant access to different services:
|
Users in this group are granted administrative rights to mailman and can login to the monitoring system. |
|
Users in this group can create other users and groups in lldap. |
|
This group is only meant for services that need to query information from the LDAP server. |
|
Same as above. |
|
Users in this group have an standard email address. |
|
Mail for |
|
Members of this group can open the shared mailbox |
|
Users in this group have access to mailman. |
|
This group grants access to nextcloud. |
|
This group is meant to give access to user management or mailinglist management without being able to change system settings. |
Creating New Accounts¶
New user accounts can be created via the webinterface at https://auth.naturbeobachtung.de. Don’t set a password during creation, instead use the password reset form to generate an email that gets sent to the users address. After creation, groups can be added at the bottom of the user detail page.
Deleting Accounts¶
Accounts can be deleted via the user overview page. Once deleted only the account on the lldap server is gone. Mailboxes and files in Nextcloud still exist on disk and have to be deleted manually if desired.
Spam Filtering¶
Incoming mail passes through rspamd before Postfix accepts it. Rspamd adds up the scores of its rules and acts on the total (ae_rspamd_action_*):
|
4 |
The sending server is told to try again later, most spam bots never do. |
|
5.5 |
The message is delivered with an |
|
12 |
The message is refused during the SMTP dialogue. |
The rules fall into three groups. Reputation lookups ask DNS blocklists (Spamhaus, dnswl.org, SURBL, URIBL and others) about the sending server and the domains in the message; these need the resolver described under Blocklists and DNS. Static rules look at the message itself; among them FROM_SUSPICIOUS_TLD adds ae_rspamd_suspicious_tld_score for sender domains ending in one of ae_rspamd_suspicious_tlds, the cheap top-level domains that carry most of the spam that otherwise passes SPF, DKIM and DMARC. AE_UNSIGNED_HTML_ONLY adds ae_rspamd_unsigned_html_only_score when a message is HTML-only and its sender domain has neither signed it with DKIM nor published a DMARC policy; that combination described nearly all the spam that was scoring just below add_header and never a message with a negative score. Finally the Bayes classifier compares the words of a message with what it has been trained on, contributing up to -3 for ham and +5.1 for spam.
One verdict bypasses the score: a message that fails DMARC is refused when its sender domain publishes p=reject and filed into Junk when it publishes p=quarantine (ae_rspamd_honour_dmarc_policy). That is what those domains ask every receiver to do, and it is what stops a forged bank or PayPal message that otherwise collects a middling score. The cost is mail from such a domain that came through a forwarder or a mailing list without ARC or From rewriting; in two weeks of logs on both deployments no message that failed a p=reject policy had scored below 4.8.
Messages filed into Junk are deleted after ae_dovecot_junk_autoexpunge (30 days); Trash is kept unless ae_dovecot_trash_autoexpunge is set. Only the folders with the \Junk and \Trash special-use flags are affected, a Spam folder that a mail client created next to them is left alone.
Outgoing mail¶
Mail that users send through the server is rate limited per login by rspamd, counted in recipients (ae_rspamd_submission_rate_limits). Each entry is a bucket: an idle account can send burst recipients at once, and no single message may have more than that - a mailing list is the tool for more; afterwards the bucket drains at rate. With the defaults an account sends 50 recipients an hour and 150 a day. Above the limit Postfix answers with a temporary error, which a mail client shows as „try again later“. Logins in ae_rspamd_submission_rate_limit_exempt_users are never counted, webmail logs in as the user and counts like any client, while unauthenticated mail (from other servers, mailing lists) has no login and is not affected.
Grafana raises „Outgoing mail rate limit hit“ whenever an account runs into the limit; a timer counts the hits of the last hour from the rspamd log into ae_rspamd_ratelimited_last_hour, because rspamd’s own counters cannot tell a rate limit from greylisting - both answer with a temporary error. A real user asks what happened; a stolen password keeps hitting the limit until it is changed. The rspamd log names the login:
journalctl -u ae-rspamd | grep 'ratelimit "ae_submission'
The limit exists because of what happens without it: in 2025 a stolen password sent spam through one account for three months, a few hundred messages an hour, until the bounces alone had filled the mailbox with 800,000 messages and dovecot could no longer open it. When that happens again anyway, change the password in the LDAP server, disconnect the account with podman exec ae-dovecot doveadm kick <login>, and clean the mailbox on disk: bounce notices are the files below /ae/userdata/<login>/mail/cur whose From header is the local MAILER-DAEMON and whose subject is Undelivered Mail Returned to Sender. Move them out of the maildir, delete dovecot.index* next to cur (the message list is rebuilt from the files, flags live in the file names), and run podman exec ae-dovecot doveadm force-resync -u <login> INBOX.
Mailing lists¶
A post to a list is a message this server sends in somebody else’s name, and that is exactly what DMARC is meant to stop: the author’s SPF check fails because the mail comes from our address, and the author’s DKIM signature breaks when Mailman adds the subject prefix. A provider that honours the author’s p=reject (or p=quarantine) policy then refuses or junks the copy for every member it hosts. The lists therefore rewrite the From header to the list address („Author via List <list@domain>“, dmarc_mitigate_action: munge_from) and put the author into Reply-To. What leaves is a message from our domain: rspamd signs it with our DKIM keys, the envelope sender list-bounces@ passes SPF, and since the lists live on the apex domain both align even with strict alignment. The mailman role sets ae_mailman_list_settings on every list on each run - Postorius still shows the settings to the list owners, but a change there does not survive the next run. Rewriting unconditionally, rather than only when the author’s domain publishes a policy, spares authors at p=none domains the failure reports about this server.
Rspamd also adds an ARC seal to everything it passes on, in our own name (arc.conf). A seal records what this server saw when the message arrived - the author’s SPF, DKIM and DMARC results - so a receiver that trusts this server can accept a forwarded message whose original signatures no longer verify. That covers the mail the From rewriting does not: the copies of messages to list-owner@ that Mailman forwards to the owners with the sender’s From intact. The rspamd log shows what happened to a post; every copy of a list post carries DKIM_SIGNED and ARC_SIGNED:
journalctl -u ae-rspamd | grep 'from: <[^>]*-bounces@'
Training Bayes¶
Bayes learns from three sources:
Messages a user moves into
Junkare learned as spam, messages moved out ofJunk(except intoTrash) as ham. This works from any mail client.Mail that users send through the server is learned as ham automatically.
Mail that the other rules already tag as spam is learned as spam automatically.
Bayes stays silent until both classes have at least ae_rspamd_bayes_min_learns messages; the counts are shown by:
podman exec ae-rspamd rspamc -h /run/rspamd-sockets/worker-controller stat
To train from mail that is already on disk, point the helper at a maildir folder or at single message files; messages it already knows are skipped:
ae-rspamd-learn spam /ae/userdata/buero/mail/.Junk
ae-rspamd-learn ham /ae/userdata/buero/mail/.Sent
Resetting Bayes¶
Until September 2026 every message with a negative score was learned as ham. Spam that passes SPF, DKIM and DMARC scores slightly below zero, so Bayes was trained on spam and ended up vouching for it. A database with that history is worth less than an empty one. To start over, stop rspamd, flush its Redis instance (it only holds caches: the Bayes tokens, greylisting and rate-limit state and the neural network) and train it from the mail on disk:
systemctl stop ae-rspamd.service
podman exec ae-redis-rspamd redis-cli FLUSHALL
systemctl start ae-rspamd.service
ae-rspamd-learn spam /ae/userdata/*/mail/.Junk
ae-rspamd-learn ham /ae/userdata/*/mail/.Sent
Check the Junk folders for false positives before learning them, and look at the stat output afterwards. Both classes have to reach ae_rspamd_bayes_min_learns before Bayes says anything at all; the spam side is the one that lags, because everything it knows comes from mail that was filed as Junk. Lowering the variable is safe: BAYES_SPAM is worth 5.1 at most and the add_header threshold is 5.5, so a Bayes verdict on a thin corpus still cannot move a message into Junk without a second signal agreeing.
Blocklists and DNS¶
The blocklists count queries per resolver and refuse the shared ones that most servers forward to, because they cannot tell the individual users behind them apart. Every list has its own way of saying so, and rspamd reads all of them as „not listed“:
List |
Answer |
Meaning |
|---|---|---|
Spamhaus |
|
Query through a public or open resolver |
dnswl.org |
|
Over 100’000 queries a day from that resolver, counted across all of its users |
URIBL |
|
Query blocked, possibly due to high volume |
Two things fix this together. Rspamd resolves through the unbound container, which recurses from the root servers itself instead of forwarding, so the lists see this machine. Since container names would no longer resolve that way, unbound passes the dns.podman zone back to podman’s own DNS, and rspamd’s configuration names the Redis container with that zone spelled out. And Spamhaus, which refuses shared resolvers regardless of volume, gets queried under a key, see Spamhaus DQS below.
Rspamd is told the resolver’s address directly (local.d/options.inc) rather than through the container’s resolv.conf. That file points at podman’s own DNS, which forwards to unbound but treats „no such name“ as a reason to try its next upstream, and returns what a shared resolver says: for a blocklist, the refusal code. Rspamd probes every list with a random name that has to come back as non-existent, so behind podman’s DNS it saw dnswl.org and senderscore fail a hundred times a day and switched them off for a while each time.
Losing dnswl.org matters as much as losing a blocklist: it is a whitelist, and without it known-good senders no longer get their negative score, which pushes ordinary mail towards the Junk folder.
To check the resolver, ask it for the test entries from inside the rspamd container. Each has to return an address in 127.0.0.0/8 other than the refusal codes above:
podman exec ae-rspamd getent hosts 2.0.0.127.zen.spamhaus.org
podman exec ae-rspamd getent hosts 2.0.0.127.list.dnswl.org
podman exec ae-rspamd getent hosts test.uribl.com.multi.uribl.com
Do not judge this by /etc/resolv.conf inside the container: it lists podman’s own DNS addresses, and getent goes through those, not through rspamd’s resolver setting. The lookups above prove that unbound answers; that rspamd uses it shows in its log, which must stay free of these lines:
journalctl -u ae-rspamd | grep rspamd_monitored_dns_cb
Each one is a list that rspamd has just switched off because its probe came back with an answer instead of „no such name“. A random name asked of unbound has to come back NXDOMAIN (dig @10.88.0.53 1.0.0.127.list.dnswl.org on the host), while the same question to podman’s DNS at 10.88.0.1 comes back with the refusal code; that difference is why rspamd bypasses it.
To check the whole path rather than just DNS, scan a message carrying Spamhaus‘ test domain. This only scans, it sends nothing:
printf 'From: t@example.com\nTo: postmaster@example.com\nSubject: t\n\nhttp://dbltest.com/\n' > /tmp/t.eml
podman cp /tmp/t.eml ae-rspamd:/tmp/t.eml
podman exec ae-rspamd rspamc -h /run/rspamd-sockets/worker-controller symbols /tmp/t.eml
DBL_SPAM has to appear in the symbol list. Note that URIBL’s own test domain does not work this way, because rspamd reduces a URL to its registrable domain before querying, and only the longer test label is listed.
If the resolver is down, rspamd keeps running without any blocklist and Postfix still accepts mail, because milter_default_action is accept.
Spamhaus DQS¶
The Spamhaus lists (ZEN for sending addresses, DBL for domains) are the most valuable of the blocklists and refuse shared resolvers outright. The Data Query Service (DQS) identifies the deployment by a key in the queried name and is free for non-commercial use at this volume. It also adds the Zero Reputation Domains list, which flags domains registered or reactivated within the last day.
Register at https://www.spamhaus.com/dqs/, copy the key from the portal (section „Datafeed Query Service“) into host_vars/<host>.yml and deploy the rspamd role:
ae_rspamd_spamhaus_dqs_key: "abcdefghijklmnopqrstuvwxyz"
ansible-playbook -i hosts.ini setup.yml --tags mail
The rules are the ones Spamhaus publishes for rspamd (local.d/rbl.conf and local.d/rbl_group.conf), without the Hash Blocklist of the paid tier. Confirm the key works, the answer has to list the three test codes and not 127.255.255.254:
podman exec ae-rspamd getent hosts 2.0.0.127.<key>.zen.dq.spamhaus.net
Over time the symbol counters (rspamc ... counters) show hits for SPAMHAUS_ZEN, DBL_* and ZRD_*.
Monitoring¶
In order to make sure all services are operating smoothly, Æther collects non user specific metrics about the system. E.g. CPU and memory usage, list and status of all processes, number of sent emails, number of requests to the webserver. These metrics can be accessed at: https://monitoring.naturbeobachtung.de. Via alerts it is possible to receive emails if certain parameters are outside of specified limits.