Maddy Mail Server 0.9.6 on AWS User Guide
Overview
This guide covers launching and running the Maddy Mail Server 0.9.6 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Mail Servers and Newsletters listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Maddy is a composable, all in one mail server written in Go. One binary is the SMTP transfer agent that receives mail for your domains, the submission server your mail clients send through, the IMAP server they read from, and the DKIM signer for outgoing mail. Its whole behaviour lives in one plain text file, /etc/maddy/maddy.conf, and mailboxes are managed with the maddy command line. There is no web interface and no separate database server: credentials and mail are kept in SQLite on the instance.
The cloudimg image adds:
- No credential ships in the image. First boot creates this instance's TLS key and certificate, its DKIM signing key and its postmaster mailbox password, and writes the mailbox details to
/root/maddy-credentials.txt(root only). Maddy does not start until first boot has finished. - Authentication only over TLS. Submission on 465 is TLS from the first byte; on 587 the server refuses AUTH, even with the right password, until STARTTLS has been used. IMAP is served on 993 over TLS only; plaintext IMAP on 143 is not served at all.
- Never an open relay. Port 25 accepts mail only for your own domains and refuses every other recipient. The build proves it with a trap listener: an unauthenticated relay attempt is refused and nothing arrives, while an authenticated submission does arrive, DKIM signed with the instance's own key.
- Mail on its own volume. Mailboxes, credentials, the outbound queue and the DKIM keys live on a separate 20 GiB EBS volume mounted at
/var/lib/maddy, which you can resize independently. - Locked down service. Maddy runs as the unprivileged
maddyuser with only the capability to bind mail ports, a read only system and no access to the EC2 instance metadata service. The AMI requires IMDSv2. - Verified and current. Maddy 0.9.6 fixes four security advisories in 0.9.5 (see Release notes). cloudimg builds it from the signed upstream source with the current Go release, and Ubuntu 24.04 LTS is fully patched at build time with unattended security upgrades on.
Maddy is free software by its authors under the GNU General Public License v3. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by the Maddy project.
Before you start: EC2 limits outbound port 25
Amazon EC2 restricts outbound traffic on TCP port 25 by default. The build of this image measured it: port 25 to a public mail exchanger was blocked while port 587 was reachable. You can ask AWS to lift the restriction for your account with the request to remove email sending limitations form.
| Works with the default EC2 restriction | |
|---|---|
| Receiving mail on port 25 from other mail servers | Yes, inbound is unaffected |
| IMAP mailbox access on 993 | Yes |
| Authenticated submission from your mail clients on 465 and 587 | Yes |
| Sending directly to recipient mail servers on port 25 | Only after AWS lifts the limit |
| Sending through an authenticated smart host on port 587 or 465 | Yes (see Sending mail out through a relay) |
Launching the instance
- Subscribe to the listing on AWS Marketplace and choose Maddy Mail Server 0.9.6 on Ubuntu 24.04 as the software version.
- Use m5.large (2 vCPU, 8 GiB) or larger. Maddy itself is light; the headroom is for the operating system, page cache and spam checks under load.
- Choose your key pair. The AMI requires IMDSv2.
- Keep the second EBS volume (20 GiB,
/var/lib/maddy) in the launch; it holds all mail. Make it bigger if you expect large mailboxes. - Use a security group that opens only what you intend to serve:
| Port | Protocol | Purpose | Open to |
|---|---|---|---|
| 22 | TCP | SSH administration | Your management address only |
| 25 | TCP | Inbound SMTP from other mail servers (STARTTLS offered) | The internet, if you receive external mail |
| 465 | TCP | Authenticated submission, TLS | Your users' networks |
| 587 | TCP | Authenticated submission, STARTTLS | Your users' networks |
| 993 | TCP | IMAP over TLS | Your users' networks |
Nothing else listens. Port 143 (plaintext IMAP) is not served.
First boot runs maddy-firstboot.service before Maddy starts: it sets the mail domain to the instance's own name, creates the TLS certificate, the DKIM key and the postmaster mailbox, and writes the credentials file. It usually finishes in a few seconds.
Connecting to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i /path/to/your-key.pem ubuntu@<public-ip>
Confirming the server is running
systemctl is-active maddy.service
systemctl show -p Result --value maddy-firstboot.service
maddy.service reports active and the first boot unit reports success.
Check the release and the listening ports:
maddy version | head -1
sudo ss -lntp | awk 'NR>1 && /maddy/ {print $4}' | sort -u
0.9.6 linux/amd64 go1.27.3
*:25
*:465
*:587
*:993
Your per instance credentials
sudo stat -c '%A %U:%G %n' /root/maddy-credentials.txt
sudo grep -E '^MADDY_(ADMIN_EMAIL|PRIMARY_DOMAIN|SUBMISSION|IMAPS|DKIM_DNS_RECORD_FILE)=' /root/maddy-credentials.txt
The file is -rw------- root:root and records:
| Key | What it is |
|---|---|
MADDY_ADMIN_EMAIL |
The only mailbox that exists, postmaster@ the instance's name (for example postmaster@ip-172-31-91-27.ec2.internal) |
MADDY_PASSWORD |
That mailbox's password, generated on this instance |
MADDY_PRIMARY_DOMAIN / MADDY_HOSTNAME |
The mail domain and server name first boot set (the instance's own name) |
MADDY_SUBMISSION / MADDY_IMAPS |
Where to point a mail client |
MADDY_DKIM_DNS_RECORD_FILE |
The DKIM DNS record for the domain |
To read the postmaster password when you need it:
sudo grep -E '^MADDY_PASSWORD=' /root/maddy-credentials.txt | cut -d= -f2-
Two instances launched from this AMI never share any of these values or keys.
Proving mail actually flows
The image ships a self test that does a real round trip and checks that the things which must not work really do not:
sudo /usr/local/sbin/maddy-selftest
ok: 465: own certificate, wrong password 535, right password 235, message submitted
ok: 993: own certificate, wrong password refused (AUTHENTICATIONFAILED), right password OK, token delivered
ok: 587: AUTH not advertised and refused before STARTTLS (523 with the right password); after STARTTLS unauthenticated MAIL 502 5.7.0, right password 235
ok: 25: STARTTLS with own certificate, AUTH not offered, and refused with the right password (535)
ok: 143: connection refused (plaintext IMAP is not served)
SELFTEST_OK
You can send a message yourself with swaks, which is installed. Pipe the password in so it never appears on a command line (it reads it at the Password: prompt):
ME=$(sudo grep '^MADDY_ADMIN_EMAIL=' /root/maddy-credentials.txt | cut -d= -f2-)
sudo grep '^MADDY_PASSWORD=' /root/maddy-credentials.txt | cut -d= -f2- | swaks --server 127.0.0.1 --port 587 --tls --auth PLAIN --auth-user "$ME" --from "$ME" --to "$ME" --header "Subject: hello from maddy" 2>&1 | grep -E '^<~ (235|250 2.0.0 OK)'
235 2.0.0 Authentication succeeded and a final 250 2.0.0 OK: queued show the message was accepted; it is delivered straight into the postmaster mailbox.
Authentication only after TLS
Before STARTTLS, port 587 does not offer AUTH:
printf 'EHLO check.example\r\nQUIT\r\n' | nc -w 5 127.0.0.1 587 | tr -d '\r'
220 ip-172-31-91-27.ec2.internal ESMTP Service Ready
250-Hello check.example
250-PIPELINING
250-8BITMIME
250-ENHANCEDSTATUSCODES
250-CHUNKING
250-STARTTLS
250-SMTPUTF8
250-SIZE 33554432
250 LIMITS RCPTMAX=20000
221 2.0.0 Bye
There is STARTTLS and no AUTH line, and an AUTH sent anyway is refused with 523 even with the right password. After STARTTLS the same port offers AUTH PLAIN. This is insecure_auth no on the submission and imap blocks of /etc/maddy/maddy.conf:
sudo grep -n 'insecure_auth' /etc/maddy/maddy.conf
Every TLS listener serves the instance's own certificate:
echo | openssl s_client -connect 127.0.0.1:993 2>/dev/null | openssl x509 -noout -subject
This server is not an open relay
Port 25 is the inbound mail exchanger. It accepts mail addressed to your own domains ($(local_domains) in the configuration) and refuses every other recipient with 550 5.1.1 User doesn't exist, so nobody can relay through it. It offers no authentication at all. Outgoing mail goes only through submission (465/587), which requires a login, and an authenticated user cannot send as somebody else: the server answers 553 5.7.0 Unauthorized use of sender address.
sudo grep -n -A3 'default_destination' /etc/maddy/maddy.conf | head -8
Managing mailboxes
Maddy keeps two records per mailbox: the login (maddy creds) and the IMAP storage account (maddy imap-acct). Run the commands as the maddy user so the database files keep the right owner. creds create asks for the password at a prompt, so it never appears on a command line:
sudo -u maddy maddy creds list
sudo -u maddy maddy imap-acct list
sudo -u maddy maddy creds create alice@example.net
sudo -u maddy maddy imap-acct create alice@example.net
sudo -u maddy maddy creds password alice@example.net
sudo -u maddy maddy creds remove alice@example.net
sudo -u maddy maddy imap-acct remove alice@example.net
Usernames are full e-mail addresses, and a user may only send from their own address.
Using your own domain
First boot sets the mail domain to the instance's own name so the server works straight away. To serve your real domain:
- Give the instance an Elastic IP, an A record for your mail host name (for example
mail.example.net), and set the Elastic IP's reverse DNS to that name in the EC2 console. - Edit
/etc/maddy/maddy.confand set the two variables at the top:
$(hostname) = mail.example.net
$(primary_domain) = example.net
- Put a certificate for the host name in
/etc/maddy/certs/mail.example.net/asfullchain.pemandprivkey.pem(ownerroot:maddy, key mode0640). A Let's Encrypt certificate fromcertbot certonly --standalone -d mail.example.net(with port 80 opened while it runs) works; copy or link its files there. - Restart and check:
sudo systemctl restart maddy.service
sudo journalctl -u maddy.service -n 20 --no-pager
When it starts with the new domain, Maddy creates a DKIM key for it and logs where the DNS record is (/var/lib/maddy/dkim_keys/example.net_default.dns). Create your mailboxes in the new domain as shown above.
DNS records
For each domain, publish:
| Record | Name | Value |
|---|---|---|
| MX | example.net |
10 mail.example.net. |
| A | mail.example.net |
The instance's Elastic IP |
| TXT (SPF) | example.net |
v=spf1 mx -all (add your relay's include if you use one) |
| TXT (DKIM) | default._domainkey.example.net |
The contents of /var/lib/maddy/dkim_keys/example.net_default.dns |
| TXT (DMARC) | _dmarc.example.net |
v=DMARC1; p=quarantine; rua=mailto:postmaster@example.net |
| PTR | The Elastic IP | mail.example.net (set in the EC2 console) |
The DKIM record for the domain first boot created is already there:
sudo sh -c 'cut -c1-40 /var/lib/maddy/dkim_keys/*.dns'; echo
Connecting a mail client
| Setting | Value |
|---|---|
| Incoming (IMAP) | Your mail host name, port 993, SSL/TLS |
| Outgoing (SMTP submission) | Your mail host name, port 465, SSL/TLS (or 587, STARTTLS) |
| Username | The full mailbox address, for example alice@example.net |
| Password | The mailbox password |
Until you install a real certificate, clients warn that the instance's self signed certificate is not trusted.
Sending mail out through a relay
Because EC2 limits outbound port 25, send through an authenticated smart host (Amazon SES SMTP or your mail provider). In /etc/maddy/maddy.conf, replace the whole target.remote outbound_delivery { ... } block with a target.smtp block of the same name, so the queue hands every outgoing message to the relay:
target.smtp outbound_delivery {
targets tcp://email-smtp.us-east-1.amazonaws.com:587
starttls yes
auth plain "your-relay-username" "your-relay-password"
}
Keep the block name outbound_delivery; Maddy refuses to start if a block is left unused. The file is readable only by root and the maddy group. Then restart and watch the log:
sudo systemctl restart maddy.service
sudo journalctl -u maddy.service -f
Publish the SPF and DKIM records your relay provider asks for as well.
Where everything lives
sudo ls /etc/maddy /var/lib/maddy
findmnt /var/lib/maddy
| Path | Contents |
|---|---|
/usr/local/bin/maddy |
The Maddy binary |
/etc/maddy/maddy.conf |
The whole server configuration |
/etc/maddy/certs/<host>/ |
The TLS certificate and key |
/var/lib/maddy/credentials.db |
Mailbox logins (bcrypt password hashes) |
/var/lib/maddy/imapsql.db, messages/ |
Mailboxes and message bodies |
/var/lib/maddy/remote_queue/ |
The outbound queue |
/var/lib/maddy/dkim_keys/ |
DKIM private keys and their DNS records |
/root/maddy-credentials.txt |
This instance's postmaster details, mode 0600 |
/usr/share/doc/maddy/ |
The licence (COPYING) and how the binary was built (SOURCE.txt) |
/var/lib/maddy is the separate data volume.
Logs and troubleshooting
sudo journalctl -u maddy.service -n 20 --no-pager
If maddy.service shows a failed start condition, first boot has not completed:
sudo journalctl -u maddy-firstboot.service -n 20 --no-pager
Backups
Everything stateful is on the /var/lib/maddy volume. EBS snapshots of that volume (or AWS Backup) are the simplest option. For a file copy, stop Maddy first:
sudo systemctl stop maddy.service
sudo tar -czf /root/maddy-backup-$(date +%F).tar.gz -C /var/lib maddy
sudo systemctl start maddy.service
Updating
Ubuntu security updates are applied by unattended-upgrades:
systemctl is-enabled unattended-upgrades.service
For Maddy itself, cloudimg publishes a new version of the listing for each upstream release. Launch the new version, move the /var/lib/maddy volume (or its snapshot) and /etc/maddy across, then retire the old instance.
Release notes
0.9.6 replaces 0.9.5. It fixes four advisories: GHSA-f2p9-pvm4-fhrj (the rate limiter never released its per key buckets, so an authenticated user could stop outbound delivery for everyone; this image's default configuration was affected in 0.9.5), GHSA-g8mw-9qf4-cg5x (auth.plain_separate without a pass directive accepted any password; not used here), GHSA-vc2f-9mpf-273q (a TLS-Required: No header from an unauthenticated sender disabled outbound TLS policy) and GHSA-rpmv-mh3g-cf5c (proxy_protocol with an empty trust list trusted every peer; not used here).
How the binary is built. cloudimg builds Maddy from the GPG signed upstream source release with Go 1.27.3 and upstream's own build script, updating one dependency (golang.org/x/net to v0.60.0) for its HTTP/2 fixes. Upstream's tests pass and Go's vulnerability checker reports nothing the binary calls. The details, including the exact source and change, are in the image:
head -12 /usr/share/doc/maddy/SOURCE.txt
Licensing
Maddy is free software under the GNU General Public License v3 or later. The licence text is at /usr/share/doc/maddy/COPYING, and SOURCE.txt next to it identifies the exact corresponding source.
Support
cloudimg supports this image through the AWS Marketplace listing and at cloudimg.co.uk. The other products in this listing are on the Mail Servers and Newsletters hub.