Mm
Streaming & Messaging AWS

Maddy Mail Server 0.9.6 on AWS User Guide

| Product: Maddy Mail Server 0.9.6 on Ubuntu 24.04 on AWS

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 maddy user 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

  1. Subscribe to the listing on AWS Marketplace and choose Maddy Mail Server 0.9.6 on Ubuntu 24.04 as the software version.
  2. 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.
  3. Choose your key pair. The AMI requires IMDSv2.
  4. 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.
  5. 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:

  1. 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.
  2. Edit /etc/maddy/maddy.conf and set the two variables at the top:
$(hostname) = mail.example.net
$(primary_domain) = example.net
  1. Put a certificate for the host name in /etc/maddy/certs/mail.example.net/ as fullchain.pem and privkey.pem (owner root:maddy, key mode 0640). A Let's Encrypt certificate from certbot certonly --standalone -d mail.example.net (with port 80 opened while it runs) works; copy or link its files there.
  2. 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.