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:

admin

Users in this group are granted administrative rights to mailman and can login to the monitoring system.

lldap_admin

Users in this group can create other users and groups in lldap.

lldap_password_manager

This group is only meant for services that need to query information from the LDAP server.

lldap_strict_readonly

Same as above.

mail

Users in this group have an standard email address.

mailalias-<name>

Mail for <name>@domain is delivered to every member of this group, see Mail Aliases and Shared Mailboxes.

sharedmail-<name>

Members of this group can open the shared mailbox <name>, see Mail Aliases and Shared Mailboxes.

mailman

Users in this group have access to mailman.

nextcloud

This group grants access to nextcloud.

staff

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.

Mail Aliases and Shared Mailboxes

Every user in the mail group has a mailbox at username@domain. Additional addresses are groups with a naming convention, managed like any other group at https://auth.naturbeobachtung.de. Only members that are also in the mail group are considered.

Mail alias

Create a group mailalias-<name> and add users to it. Mail for <name>@domain is delivered to the personal mailbox of every member. A group with several members works as a distribution list. Delete the group to remove the alias.

Shared mailbox

Create a group sharedmail-<name> and add users to it. Mail for <name>@domain is stored in a separate mailbox that every member can open by logging in as username/<name> with their own password (see Accessing Your Mailbox). The mailbox stays on disk after the group is deleted and has to be removed manually if desired.

Group names are case insensitive. The <name> part may contain letters, digits, dots, dashes and underscores.

The same groups decide who may send as an address. On the submission port a login may use its own address, any alias it is a member of and any shared mailbox it is a member of (or is logged in to) as the envelope sender, and the From header has to be that same address. Anything else, including addresses outside the domain, is rejected during submission: this is what keeps a single leaked mail password from sending signed mail in somebody else’s name.

A few addresses are expected to exist on every mail domain. The lldap role creates them as mailalias-<name> groups (ae_lldap_mail_operator_aliases) and adds the mail operators - the uids listed in ae_lldap_mail_operators in host_vars - to them on each run; an operator who is not also in the mail group receives nothing, as with any alias. This is deliberately a list of its own and not the admin group: administering an application does not make somebody the postmaster. Further members can be added by hand and stay:

postmaster, abuse

Required by the mail standards and the addresses other operators and abuse desks write to when something is wrong with mail from this server.

dmarc

Receives the aggregate DMARC reports other providers send about mail claiming to be from this domain, once dmarc_policy in tf/config.auto.tfvars names it. Run the playbook before tofu apply so the address exists when the first report arrives. Reports are XML attachments, one per reporter and day; a message from a large provider that lists a source other than this server is the signal that somebody sends as this domain from elsewhere.

With the dmarc-exporter role enabled this is not an alias but a mailbox of its own. The exporter reads the reports from it, turns them into Prometheus metrics and moves them to its Archive folder; the DMARC reports dashboard in Grafana shows pass rate, volume and failures per reporting provider, DKIM against SPF alignment, and which domains signed or sent the mail. The mail operators may open the raw reports as username/dmarc. On a host that ran without the exporter, remove the members of the old mailalias-dmarc group once the exporter is deployed, or postfix keeps expanding the alias to them.

tlsrpt

Receives SMTP TLS reports once tlsrpt_rua names it. They are what turns the MTA-STS policy’s testing mode into information: a sender that could not verify the policy or the certificate says so here, while the mail still gets delivered.

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_*):

greylist

4

The sending server is told to try again later, most spam bots never do.

add_header

5.5

The message is delivered with an X-Spam: Yes header; a global sieve script files it into the Junk folder of the recipient.

reject

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 Junk are learned as spam, messages moved out of Junk (except into Trash) 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

127.255.255.254

Query through a public or open resolver

dnswl.org

127.0.0.255

Over 100’000 queries a day from that resolver, counted across all of its users

URIBL

127.0.0.1

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.