Maddy Mail Server 0.9.6
Overview
This guide shows how to launch and run the Maddy Mail Server 0.9.6 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version in the SecureAnyCloud Mail Servers and Newsletters listing on AWS Marketplace. You choose 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 that your mail clients send through, the IMAP server that they read from, and the DKIM signer for outgoing mail. Its whole behaviour lives in one plain-text file, /etc/maddy/maddy.conf, and you manage mailboxes 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.
What you get:
- 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. It 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, and 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 this 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. You can resize it 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). SecureAnyCloud 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 SecureAnyCloud 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 the restriction: 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.
With the default EC2 restriction in place:
- Receiving mail on port 25 from other mail servers works. Inbound is unaffected.
- IMAP mailbox access on 993 works.
- Authenticated submission from your mail clients on 465 and 587 works.
- Sending directly to recipient mail servers on port 25 works only after AWS lifts the limit.
- Sending through an authenticated smart host on port 587 or 465 works (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, because it holds all mail. Make it bigger if you expect large mailboxes. - Use a security group that opens only what you intend to serve. The rules are listed below.
- Port 22, TCP: SSH administration. Open it to your management address only.
- Port 25, TCP: inbound SMTP from other mail servers (STARTTLS offered). Open it to the internet if you receive external mail.
- Port 465, TCP: authenticated submission over TLS. Open it to your users' networks.
- Port 587, TCP: authenticated submission with STARTTLS. Open it to your users' networks.
- Port 993, TCP: IMAP over TLS. Open it to 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
Connect over SSH. For Ubuntu 24.04, the SSH user is 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.servicemaddy.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 -u0.9.6 linux/amd64 go1.27.3 *:25 *:465 *:587 *:993Your 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.txtThe file is -rw------- root:root and records the following keys:
- 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 that 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, run:
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. It also checks that the things that must not work really do not work:
sudo /usr/local/sbin/maddy-selftestok: 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_OKYou can send a message yourself with swaks, which is installed. Pipe the password in so that it never appears on a command line. The tool 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 that 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 ByeThe output shows STARTTLS and no AUTH line. An AUTH sent anyway is refused with 523, even with the right password. After STARTTLS, the same port offers AUTH PLAIN. This behaviour comes from insecure_auth no on the submission and imap blocks of /etc/maddy/maddy.conf:
sudo grep -n 'insecure_auth' /etc/maddy/maddy.confEvery TLS listener serves the instance's own certificate:
echo | openssl s_client -connect 127.0.0.1:993 2>/dev/null | openssl x509 -noout -subjectThis 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. An authenticated user cannot send as somebody else, because the server answers 553 5.7.0 Unauthorized use of sender address.
sudo grep -n -A3 'default_destination' /etc/maddy/maddy.conf | head -8Managing 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 that the database files keep the right owner. creds create asks for the password at a prompt, so the password never appears on a command line:
sudo -u maddy maddy creds list
sudo -u maddy maddy imap-acct listsudo -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.netUsernames are full email addresses, and a user can send only 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, follow these steps:
- Give the instance an Elastic IP and an A record for your mail host name (for example
mail.example.net). 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.networks, with port 80 opened while the command runs. Copy or link its files there. - Restart Maddy and check the log:
sudo systemctl restart maddy.service
sudo journalctl -u maddy.service -n 20 --no-pagerWhen Maddy starts with the new domain, it creates a DKIM key for the domain 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 these records:
- MX record: name example.net, value 10 mail.example.net.
- A record: name mail.example.net, value the instance's Elastic IP
- TXT (SPF) record: name example.net, value v=spf1 mx -all (add your relay's include if you use one)
- TXT (DKIM) record: name default._domainkey.example.net, value the contents of /var/lib/maddy/dkim_keys/example.net_default.dns
- TXT (DMARC) record: name _dmarc.example.net, value v=DMARC1; p=quarantine; rua=mailto:postmaster@example.net
- PTR record: the Elastic IP, pointing to mail.example.net (set in the EC2 console)
The DKIM record for the domain that first boot created is already on the instance:
sudo sh -c 'cut -c1-40 /var/lib/maddy/dkim_keys/*.dns'; echoConnecting a mail client
Use these settings in your mail client:
- 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
EC2 limits outbound port 25, so 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. The queue then 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, because Maddy refuses to start if a block is left unused. Only root and the maddy group can read the file. Then restart Maddy and watch the log:
sudo systemctl restart maddy.service
sudo journalctl -u maddy.service -fPublish 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- /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-pagerIf maddy.service shows a failed start condition, first boot has not completed. Check the first-boot log:
sudo journalctl -u maddy-firstboot.service -n 20 --no-pagerBackups
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.serviceUpdating
Ubuntu security updates are applied by unattended-upgrades:
systemctl is-enabled unattended-upgrades.serviceFor Maddy itself, SecureAnyCloud 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, and 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. SecureAnyCloud builds Maddy from the GPG-signed upstream source release with Go 1.27.3 and upstream's own build script. The build updates 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 that the binary calls. The details, including the exact source and change, are in the image:
head -12 /usr/share/doc/maddy/SOURCE.txtLicensing
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
SecureAnyCloud supports this image through the AWS Marketplace listing and at secureanycloud.com. The Mail Servers and Newsletters hub describes the other products in this listing.
Deploy on AWS
Launch Maddy Mail Server 0.9.6 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com