Developer Tools AWS

Mailpit 1.32.0 on AWS User Guide

| Product: Mailpit 1.32.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Mailpit 1.32.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Mailpit is an open source email and SMTP testing tool for developers. Point an application's SMTP settings at it and every message the application sends is captured instead of delivered: Mailpit accepts mail for any address, never delivers it anywhere, and shows each message in a modern web UI (HTML, plain text, headers, raw source and attachments) with an HTML Check that scores the message against real email clients and a Link Check that tests every link in it. Test code reads the same mail over a REST API and POP3, and the web UI updates live as mail arrives. It is a safe place to test sign up, password reset and notification emails without real mail ever leaving your environment.

The cloudimg image locks Mailpit down:

  • HTTPS from the first boot. nginx serves Mailpit on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • A password unique to your instance, on everything. HTTP Basic Auth is required for the user admin on every page, the REST API, the live feed and the attachments, and the same password is the POP3 login. It is generated on first boot and written to /var/lib/cloudimg/mailpit-credentials.txt (root only); nginx and Mailpit store only bcrypt hashes of it. No password is baked into the image.
  • SMTP stays on the instance until you open it. SMTP (port 1025), POP3 (1110) and Mailpit's own web server (8025) listen on 127.0.0.1 only, and the security group needs only 22, 80 and 443. Step 9 shows how to accept mail from your own application servers, and only from them.
  • Link checks cannot be turned against your network. Anyone who can send mail to Mailpit controls the links in that mail, and the Link Check fetches them. Mailpit refuses private, loopback and reserved addresses (including the EC2 instance metadata service), Mailpit and nginx are blocked from the metadata service at the kernel level as well, and another web site cannot make your signed in browser start a link check, an HTML check or a screenshot fetch. The HTML Check never downloads remote stylesheets or fonts, and Mailpit does not call GitHub to look for new releases.
  • Nothing leaves the instance unless you turn it on. Relaying captured messages to a real mail server, forwarding, webhooks, SpamAssassin and Prometheus metrics are all off. Step 14 shows how to switch them on.
  • A flood of mail cannot fill the disk. Messages live in a 1 GiB mail store of their own (created on first boot and reserved up front). When it is full, Mailpit answers new mail with a temporary SMTP error and the rest of the instance is unaffected. Messages over 10 MiB and recipients past 100 per message are refused, Mailpit keeps the newest 500 messages, and mail older than 3 days is removed.
  • The official release, scanned. Mailpit 1.32.0 is the official upstream release binary, checked against its published checksum and built with the current Go release. govulncheck finds no vulnerable code in it.
  • Hardened defaults. Mailpit runs as its own unprivileged user with no capabilities and a read only view of the system except its mail store. nginx refuses to start until first boot has generated the certificate and the password, drops requests for host names the instance does not serve, refuses cross site requests that delete or change mail, and the AMI requires IMDSv2.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Mailpit 1.32.0 (/opt/mailpit/mailpit, with its web UI built in), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (SSH), TCP 443 (Mailpit) and TCP 80 (redirects to HTTPS) from your own networks only.
  • To capture mail from other servers, their CIDR range (Step 9). Mailpit accepts mail on the instance itself without any extra rule.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Mailpit 1.32.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (Mailpit itself needs little; a smaller instance works for a team's test mail), your key pair, a security group as above, and a subnet your application servers can reach.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=mailpit}]'

Step 2: Connect to your instance

Version SSH user
Mailpit 1.32.0 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot generates the certificate and the password and creates the mail store, then Mailpit and nginx start:

systemctl is-active mailpit-store mailpit nginx mailpit-firstboot
sudo test -f /var/lib/cloudimg/mailpit-firstboot.done && echo FIRSTBOOT_DONE

All four units are active and the first boot marker is present:

active
active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network. Mailpit's SMTP (1025), POP3 (1110) and web server (8025) listen on 127.0.0.1 only:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:1025 users:(("mailpit"))
127.0.0.1:1110 users:(("mailpit"))
127.0.0.1:8025 users:(("mailpit"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))

Step 4: Get your URL and password

sudo cat /var/lib/cloudimg/mailpit-credentials.txt

The file is readable by root only and holds the URL, the user name, this instance's password and the mail ports:

# Mailpit 1.32.0 - generated on first boot by mailpit-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Only bcrypt hashes
# of the password are stored (nginx and Mailpit's POP3 auth file). Keep this file secret.

MAILPIT_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
MAILPIT_USERNAME=admin
MAILPIT_PASSWORD=<MAILPIT_PASSWORD>
MAILPIT_SMTP=127.0.0.1:1025
MAILPIT_POP3=127.0.0.1:1110
#
# Open MAILPIT_URL in a browser (self-signed certificate minted on this instance's first boot;
# port 80 redirects to https) and sign in with the username and password above. The same
# credential protects the REST API (/api/v1/...), the live feed and POP3.
# SMTP and POP3 listen on this instance only (127.0.0.1). To accept mail from your own servers,
# see the user guide (an override file plus a security group rule for your senders' CIDR).
# Self-test (send a message and read it back):  sudo mailpit-selftest
# Change the name it is served as:  sudo mailpit-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/mailpit-1-32-0-on-ubuntu-24-04-aws/

The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve Mailpit under your own DNS name, see Step 12.

Step 5: Sign in

Open the MAILPIT_URL in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (or install your own certificate, Step 12). Sign in as admin with the password from Step 4; the browser remembers the login for the session.

The inbox lists every captured message, newest first, whatever address it was sent to, and updates live as new mail arrives:

The Mailpit inbox on cloudimg listing four captured test messages: a welcome email, an order notice, a CI report and an invoice

Select a message to read it as HTML or plain text, view its headers and raw source, download it or its attachments, or delete it:

A captured welcome email open in Mailpit, showing its sender, recipient, subject and rendered HTML body

Step 6: Send a test message and read it back

Any SMTP client on the instance can send to 127.0.0.1 port 1025 with no login and no TLS. For example, with Python:

python3 - <<'PY'
import smtplib
from email.message import EmailMessage
m = EmailMessage()
m["From"] = "My App <app@example.test>"
m["To"] = "demo@example.test"
m["Subject"] = "Hello from my app"
m.set_content("This message was captured by Mailpit.\n")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
print("sent to demo@example.test")
PY
sent to demo@example.test

The message is now at the top of the inbox in the web UI. The image also has a self test that sends a uniquely tagged message, reads it back through nginx with your password and over POP3, deletes it and checks the security settings:

sudo mailpit-selftest
SELFTEST-OK https 301 + health + 401 noauth + 401 wrong + web UI + SMTP 127.0.0.1:1025 -> REST API + POP3 127.0.0.1:1110 round trip cloudimg-selftest-<timestamp> + 403 cross-origin delete + 403 cross-site link check + delete + 444 unknown host

Step 7: Use the REST API and POP3 in your tests

Mailpit's REST API (reference) searches and lists messages (GET /api/v1/search?query=..., GET /api/v1/messages), returns a message with its headers, text and HTML (GET /api/v1/message/<ID>), its raw source (.../<ID>/raw), and deletes messages (DELETE /api/v1/messages, DELETE /api/v1/search?query=...). Every call needs the same login as the web UI. Here the password is read from the credentials file and passed to curl on standard input, so it never appears in the process list:

sudo bash -c '
  F=/var/lib/cloudimg/mailpit-credentials.txt
  P=$(grep "^MAILPIT_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^MAILPIT_URL=" $F | sed -E "s#^MAILPIT_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" "$@"; }
  ID=$(api -G --data-urlencode "query=to:demo@example.test" "https://$H/api/v1/search" | python3 -c "import sys,json; print(json.load(sys.stdin)[\"messages\"][0][\"ID\"])")
  api "https://$H/api/v1/message/$ID" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(\"from:   \", d[\"From\"][\"Address\"])
print(\"to:     \", [t[\"Address\"] for t in d[\"To\"]])
print(\"subject:\", d[\"Subject\"])
print(\"body:   \", d[\"Text\"].strip())"
'
from:    app@example.test
to:      ['demo@example.test']
subject: Hello from my app
body:    This message was captured by Mailpit.

From another machine, call https://<your-mailpit-host>/api/v1/... with the same user and password (for example curl -u admin and let curl prompt for the password, or your test framework's HTTP client with Basic Auth). Note that reading a message with GET /api/v1/message/<ID> marks it as read, as in the web UI.

POP3 is on 127.0.0.1 only and takes the same user and password, which suits test code running on the instance (or through an SSH tunnel: ssh -i <your-key.pem> -L 1110:127.0.0.1:1110 ubuntu@<public-ip>). POP3 shows every captured message as one mailbox:

sudo python3 - <<'PY'
import poplib
kv = dict(l.split("=", 1) for l in open("/var/lib/cloudimg/mailpit-credentials.txt").read().splitlines()
          if "=" in l and not l.startswith("#"))
p = poplib.POP3("127.0.0.1", 1110)
p.user(kv["MAILPIT_USERNAME"])
p.pass_(kv["MAILPIT_PASSWORD"])
count, size = p.stat()
hits = [i for i in range(1, count + 1) if b"Subject: Hello from my app" in b"\n".join(p.retr(i)[1])]
print("POP3 login OK, the test message is in the mailbox:", bool(hits))
p.quit()
PY
POP3 login OK, the test message is in the mailbox: True

Clean up between test runs by deleting what a search matches:

sudo bash -c '
  F=/var/lib/cloudimg/mailpit-credentials.txt
  P=$(grep "^MAILPIT_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^MAILPIT_URL=" $F | sed -E "s#^MAILPIT_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" "$@"; }
  api -o /dev/null -w "delete demo@example.test mail -> HTTP %{http_code}\n" -X DELETE -G --data-urlencode "query=to:demo@example.test" "https://$H/api/v1/search"
  api -G --data-urlencode "query=to:demo@example.test" "https://$H/api/v1/search" | python3 -c "import sys,json; print(\"messages left:\", json.load(sys.stdin)[\"messages_count\"])"
'
delete demo@example.test mail -> HTTP 200
messages left: 0

Step 8: HTML Check and Link Check

Open a message and choose Link Check, then Check message links. Mailpit tests every link in the message and shows the answer for each. Links to private, loopback or reserved addresses are never fetched: they show 451 Blocked private/reserved address, as the metadata service link in this test message does:

The Mailpit Link Check tab: two public links answered 200 OK and a link to 169.254.169.254 was blocked with status 451

HTML Check scores the message's HTML and CSS against what real email clients support, with a warning for each feature that some clients do not render:

The Mailpit HTML Check tab showing 92.5% client support for the message, the tested platforms and the warnings by HTML element and CSS property

The same checks are in the API (GET /api/v1/message/<ID>/link-check and .../html-check). Because they make the instance fetch URLs, nginx only runs them for a request that shows it comes from Mailpit's own pages: a browser sends that by itself, and a script adds an Origin header with the instance's own address. Without it, or from another web site, the answer is 403:

sudo bash -c '
  F=/var/lib/cloudimg/mailpit-credentials.txt
  P=$(grep "^MAILPIT_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^MAILPIT_URL=" $F | sed -E "s#^MAILPIT_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" "$@"; }
  python3 - <<PY
import smtplib
from email.message import EmailMessage
m = EmailMessage()
m["From"] = "app@example.test"; m["To"] = "links@example.test"; m["Subject"] = "Links to check"
m.set_content("Docs: https://www.cloudimg.co.uk/\nMetadata: http://169.254.169.254/latest/meta-data/\n")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
PY
  sleep 1
  ID=$(api -G --data-urlencode "query=to:links@example.test" "https://$H/api/v1/search" | python3 -c "import sys,json; print(json.load(sys.stdin)[\"messages\"][0][\"ID\"])")
  api "https://$H/api/v1/message/$ID/link-check" -H "Origin: https://$H" | python3 -c "
import sys, json
for l in json.load(sys.stdin)[\"Links\"]:
    print(l[\"URL\"], \"->\", l[\"StatusCode\"], l[\"Status\"])"
  api -o /dev/null -w "link check, no Origin -> HTTP %{http_code}\n" "https://$H/api/v1/message/$ID/link-check"
  api -o /dev/null -w "link check, other web site -> HTTP %{http_code}\n" -H "Origin: https://evil.example" "https://$H/api/v1/message/$ID/link-check"
  api -o /dev/null -X DELETE -G --data-urlencode "query=to:links@example.test" "https://$H/api/v1/search"
'
https://www.cloudimg.co.uk/ -> 200 OK
http://169.254.169.254/latest/meta-data/ -> 451 Blocked private/reserved address
link check, no Origin -> HTTP 403
link check, other web site -> HTTP 403

Step 9: Accept mail from your application servers

Out of the box only programs on the instance itself can send mail to Mailpit. Mailpit accepts mail for any address from anyone who can connect, so open SMTP to the servers that should send test mail, and only to them. It takes two changes.

  1. Make Mailpit's SMTP server listen on all interfaces. Local settings go in /etc/mailpit/mailpit.local.env, which is read after the shipped /etc/mailpit/mailpit.env (so its values win and survive image updates). Then restart Mailpit and check the listener:
# then open TCP 1025 to <your-mgmt-cidr> only (step 2)
echo 'MP_SMTP_BIND_ADDR=0.0.0.0:1025' | sudo tee /etc/mailpit/mailpit.local.env
sudo systemctl restart mailpit
sudo ss -lntp | grep ':1025 '
  1. Allow TCP 1025 in the instance's security group from your application servers' CIDR only (here <your-mgmt-cidr>, for example 10.20.0.0/16), never from 0.0.0.0/0:
aws ec2 authorize-security-group-ingress --group-id <your-security-group> \
  --protocol tcp --port 1025 --cidr <your-mgmt-cidr>

Then set your application's SMTP settings to the instance's private IP or DNS name, port 1025, no authentication and no TLS. Mail from those servers now appears in the web UI.

To go back to local only, remove the override, restart Mailpit and delete the security group rule:

sudo rm -f /etc/mailpit/mailpit.local.env
sudo systemctl restart mailpit
# and remove the security group rule for <your-mgmt-cidr>

The same file takes any other setting from Mailpit's runtime options, for example MP_SMTP_ALLOWED_RECIPIENTS to accept mail only for your test domains, or MP_SMTP_AUTH_ACCEPT_ANY=true if your application insists on sending an SMTP login. Keep POP3 and the web server on 127.0.0.1: nginx already serves the web UI and API with HTTPS and the login.

Step 10: Limits and the mail store

The limits Mailpit is running with:

grep -E '^MP_' /etc/mailpit/mailpit.env
grep -E '^Memory(High|Max)=' /etc/systemd/system/mailpit.service
MP_UI_BIND_ADDR=127.0.0.1:8025
MP_SMTP_BIND_ADDR=127.0.0.1:1025
MP_POP3_BIND_ADDR=127.0.0.1:1110
MP_POP3_AUTH_FILE=/etc/mailpit/pop3-auth
MP_SMTP_MAX_RECIPIENTS=100
MP_MAX_MESSAGE_SIZE=10
MP_MAX_MESSAGES=500
MP_MAX_AGE=3d
MemoryHigh=40%
MemoryMax=50%
Limit Value What happens
Message size 10 MiB A larger message is refused with 552, whether or not the client declares its size
Recipients per message 100 Recipients past the 100th are refused with 452
Messages kept 500 The oldest messages are removed to make room
Message age 3 days Older messages are removed
Mail store 1 GiB When full, new mail gets 451 (try again later); nothing else on the instance runs out of disk
Memory 50% of the instance Mailpit is held to it by systemd

The mail store is an ext4 filesystem in /var/lib/mailpit-store.img (created on this instance's first boot, with its space reserved up front), mounted at /var/lib/mailpit with nodev,nosuid,noexec. Mailpit refuses to start unless it is mounted, so mail never lands on the root disk:

findmnt -no FSTYPE,OPTIONS,TARGET /var/lib/mailpit
df -h --output=size,target /var/lib/mailpit
sudo ls -ls --block-size=M /var/lib/mailpit-store.img | awk '{print $1, $2, $6, $NF}'
ext4   rw,nosuid,nodev,noexec,relatime /var/lib/mailpit
 Size Mounted on
 974M /var/lib/mailpit
1025M -rw------- 1024M /var/lib/mailpit-store.img

To change a limit, put the setting in /etc/mailpit/mailpit.local.env (Step 9), for example MP_MAX_MESSAGES=2000, and restart Mailpit. To grow the store (here to 4 GiB; make sure the root volume has the room, and grow the EBS volume first if needed), stop Mailpit and the store, resize the image and its filesystem, and start them again:

# keep a copy of any mail you need first: sudo cp -a /var/lib/mailpit <backup-dir>
sudo systemctl stop mailpit mailpit-store
sudo fallocate -l 4GiB /var/lib/mailpit-store.img
sudo e2fsck -f -p /var/lib/mailpit-store.img
sudo resize2fs /var/lib/mailpit-store.img
sudo sed -i 's/^STORE_SIZE_MIB=.*/STORE_SIZE_MIB=4096/' /etc/mailpit/store.conf
sudo systemctl start mailpit-store mailpit
df -h /var/lib/mailpit

Step 11: Check the security posture from the command line

Without a login everything is refused, including the REST API, deletes, the live feed and Mailpit's own status endpoints; port 80 redirects to HTTPS; the health check stays open for load balancers; and a request for a host name the instance does not serve is dropped without an answer:

H=$(sudo grep '^MAILPIT_URL=' /var/lib/cloudimg/mailpit-credentials.txt | sed -E 's#^MAILPIT_URL=https://([^/]+)/.*#\1#')
R="--resolve $H:443:127.0.0.1 --resolve $H:80:127.0.0.1"
curl -s -o /dev/null -w 'http://  -> %{http_code} %{redirect_url}\n' $R "http://$H/"
curl -sk -o /dev/null -w 'web UI, no login -> HTTP %{http_code}\n' $R "https://$H/"
curl -sk -o /dev/null -w 'REST API, no login -> HTTP %{http_code}\n' $R "https://$H/api/v1/messages"
curl -sk -o /dev/null -w 'delete, no login -> HTTP %{http_code}\n' $R -X DELETE "https://$H/api/v1/messages"
curl -sk -o /dev/null -w 'readyz, no login -> HTTP %{http_code}\n' $R "https://$H/readyz"
curl -sk --http1.1 -o /dev/null -w 'live feed WebSocket, no login -> HTTP %{http_code}\n' $R -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' "https://$H/api/events"
curl -sk -o /dev/null -w 'health -> HTTP %{http_code}\n' $R "https://$H/health"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}\n' --resolve "evil.example:443:127.0.0.1" "https://evil.example/" || true
http://  -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
web UI, no login -> HTTP 401
REST API, no login -> HTTP 401
delete, no login -> HTTP 401
readyz, no login -> HTTP 401
live feed WebSocket, no login -> HTTP 401
health -> HTTP 200
unknown host -> 000

000 means nginx closed the connection without answering, which is how it treats any host name it does not serve.

With the password, Mailpit is served and works from its own pages. A delete, or a live feed connection, whose browser Origin is another web site is refused (HTTP 403), so another page cannot use your signed in browser to read the live feed or delete captured mail:

sudo bash -c '
  F=/var/lib/cloudimg/mailpit-credentials.txt
  P=$(grep "^MAILPIT_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^MAILPIT_URL=" $F | sed -E "s#^MAILPIT_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null "$@"; }
  ws() { api --http1.1 -m 3 -w "$1 -> HTTP %{http_code}\n" -H "Origin: $2" -H "Connection: Upgrade" -H "Upgrade: websocket" \
    -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" "https://$H/api/events"; }
  api -w "with login -> HTTP %{http_code}\n" "https://$H/"
  api -w "delete, other origin -> HTTP %{http_code}\n" -X DELETE -H "Origin: https://evil.example" -H "Content-Type: application/json" --data "{\"IDs\":[\"none\"]}" "https://$H/api/v1/messages"
  ws "live feed WebSocket, other origin" "https://evil.example"
  ws "live feed WebSocket, same origin" "https://$H" || true
'
with login -> HTTP 200
delete, other origin -> HTTP 403
live feed WebSocket, other origin -> HTTP 403
live feed WebSocket, same origin -> HTTP 101

nginx and Mailpit's POP3 server keep only bcrypt hashes of the password; the plain text is in the root only credentials file:

sudo sed -E 's/\$2y\$12\$.*/$2y$12$<bcrypt-hash>/' /etc/nginx/.mailpit-htpasswd /etc/mailpit/pop3-auth
sudo stat -c '%a %U:%G %n' /etc/nginx/.mailpit-htpasswd /etc/mailpit/pop3-auth /var/lib/cloudimg/mailpit-credentials.txt
admin:$2y$12$<bcrypt-hash>
admin:$2y$12$<bcrypt-hash>
640 root:www-data /etc/nginx/.mailpit-htpasswd
640 root:mailpit /etc/mailpit/pop3-auth
600 root:root /var/lib/cloudimg/mailpit-credentials.txt

The certificate was generated on this instance and names its own addresses:

H=$(sudo grep '^MAILPIT_URL=' /var/lib/cloudimg/mailpit-credentials.txt | sed -E 's#^MAILPIT_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name: 
    DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

Mailpit and nginx cannot reach the instance metadata service; Mailpit runs as its own user with no capabilities and can write only to its mail store; it runs with remote CSS and fonts blocked and the release check off, and no relay, forwarding or webhook is configured:

for u in mailpit nginx; do echo "$u IPAddressDeny: $(systemctl show $u -p IPAddressDeny --value | tr ' ' '\n' | sort | xargs)"; done
systemctl show mailpit -p User -p ProtectSystem -p ReadWritePaths -p CapabilityBoundingSet -p NoNewPrivileges
sudo tr '\0' ' ' < /proc/$(systemctl show -p MainPID --value mailpit)/cmdline; echo
sudo cat /proc/$(systemctl show -p MainPID --value mailpit)/environ | tr '\0' '\n' | grep -cE '^MP_(SMTP_RELAY|SMTP_FORWARD|WEBHOOK|ENABLE_SPAMASSASSIN|ENABLE_PROMETHEUS|ALLOW_INTERNAL)' || true
mailpit IPAddressDeny: 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
nginx IPAddressDeny: 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
CapabilityBoundingSet=
User=mailpit
ReadWritePaths=/var/lib/mailpit
ProtectSystem=strict
NoNewPrivileges=yes
/opt/mailpit/mailpit --database /var/lib/mailpit/mailpit.db --block-remote-css-and-fonts --disable-version-check
0

The last line is the number of relay, forwarding, webhook, SpamAssassin, Prometheus and internal address settings in Mailpit's environment: none.

Step 12: Your own DNS name and certificate

Point a DNS record at the instance, then set the name Mailpit is served as. This mints a new self signed certificate for the name, updates nginx, the cross site protection and the URL in the credentials file, and keeps the name across reboots:

sudo mailpit-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running mailpit-set-hostname again replaces it with a self signed one):

sudo install -m 0644 <your-domain>.crt /etc/ssl/mailpit/mailpit.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/mailpit/mailpit.key
sudo nginx -t && sudo systemctl reload nginx

Step 13: Change the password or add users

The web login is an nginx Basic Auth file and the POP3 login is Mailpit's own file in the same format. To set a new password for admin, hand it to htpasswd on standard input for both files (printf is a shell builtin, so the password never appears in the process list; start the lines with a space so most shells keep them out of your history), then reload nginx and restart Mailpit:

 printf '%s\n' '<new-password>' | sudo htpasswd -i -B -C 12 /etc/nginx/.mailpit-htpasswd admin
 printf '%s\n' '<new-password>' | sudo htpasswd -i -B -C 12 /etc/mailpit/pop3-auth admin
sudo systemctl reload nginx
sudo systemctl restart mailpit

Running sudo htpasswd -B -C 12 /etc/nginx/.mailpit-htpasswd <user> without -i prompts for the password instead. Use it with another user name to give a colleague or a CI system its own web and API login, and sudo htpasswd -D /etc/nginx/.mailpit-htpasswd <user> to remove one. Then update or delete MAILPIT_PASSWORD in /var/lib/cloudimg/mailpit-credentials.txt so it does not hold an old password.

nginx checks the login and does not pass it on to Mailpit.

Step 14: Optional features (off by default)

These Mailpit features send data off the instance, so the image ships with all of them off. Switch one on only when you need it, in /etc/mailpit/mailpit.local.env (Step 9), then sudo systemctl restart mailpit. The runtime options page lists every setting.

  • Release a message to a real mail server. MP_SMTP_RELAY_CONFIG names a YAML file with your relay's host, port, TLS and login, and allowed-recipients limits who can receive released mail. The web UI then shows a Release button on each message. See SMTP relay.
  • Webhook on new mail. MP_WEBHOOK_URL makes Mailpit post a summary of every new message to a URL you choose.
  • SpamAssassin scores, Prometheus metrics and forwarding to another address are documented on the same page.

For example, to release mail through your company's relay to your own domain only (create /etc/mailpit/relay.yaml with your relay's details first, mode 0640, owner root:mailpit):

echo 'MP_SMTP_RELAY_CONFIG=/etc/mailpit/relay.yaml' | sudo tee -a /etc/mailpit/mailpit.local.env
# relay.yaml: host: smtp.<your-domain>, port: 587, starttls: true, auth: plain, username/password, allowed-recipients: '@<your-domain>$'
sudo systemctl restart mailpit

Mailpit stays blocked from the instance metadata service whatever you switch on.

Step 15: Server components

Component Path
Mailpit binary (web UI built in) /opt/mailpit/mailpit
Configuration /etc/mailpit/mailpit.env (shipped), /etc/mailpit/mailpit.local.env (yours)
Mail store /var/lib/mailpit-store.img, mounted at /var/lib/mailpit (mailpit-store.service, size in /etc/mailpit/store.conf)
Mailpit service mailpit.service (user mailpit; web 127.0.0.1:8025, SMTP 127.0.0.1:1025, POP3 127.0.0.1:1110)
POP3 login file /etc/mailpit/pop3-auth (bcrypt)
Build record, licence, govulncheck report /usr/share/doc/cloudimg-mailpit/
nginx site /etc/nginx/sites-available/cloudimg-mailpit
Basic Auth file /etc/nginx/.mailpit-htpasswd (bcrypt)
Certificate and key /etc/ssl/mailpit/
First boot script /usr/local/sbin/mailpit-firstboot.sh
Host name helper /usr/local/sbin/mailpit-set-hostname
Self test /usr/local/sbin/mailpit-selftest
Credentials /var/lib/cloudimg/mailpit-credentials.txt (mode 0600)
/opt/mailpit/mailpit version
command -v go || echo "no Go toolchain"
nginx -v 2>&1
sed -n '1,3p' /usr/share/doc/cloudimg-mailpit/BUILD-INFO.txt
/opt/mailpit/mailpit v1.32.0 compiled with go1.27.3 on linux/amd64
no Go toolchain
nginx version: nginx/1.24.0 (Ubuntu)
mailpit           1.32.0 (tag v1.32.0, commit ce798cfd781ef07b321427fc5082b3eebdcae460), MIT - the OFFICIAL release binary
                  https://github.com/axllent/mailpit/releases/download/v1.32.0/mailpit-linux-amd64.tar.gz
                  tarball sha256 4b5993c978e8604c70d43927e2ad103b311bba3b1822fa0cf1d4ee4ec890a93c (= GitHub's asset digest); binary sha256 262cfa28fae4554be198d1fea5ef83c0f8299317876d658623711c629a8edec8

/usr/share/doc/cloudimg-mailpit/BUILD-INFO.txt records the release archive and binary checksums, the Go version and modules the release was built with, and the govulncheck result; govulncheck-mailpit-binary.txt holds the full report.

Step 16: Security notes and recommendations

  • Allow ports 22, 80 and 443 in the security group from your own networks only, and TCP 1025 (if you open SMTP, Step 9) from your application servers only. Mailpit accepts mail for any address and is meant for test traffic: never point real users' mail at it or list it as an MX record.
  • Keep the per instance password secret, and rotate it if it may have been shared (Step 13). Anyone with it can read every captured message, which can include password reset links and other tokens your applications send in test.
  • Use your own DNS name and a certificate your clients trust (Step 12).
  • Mailpit's SMTP has no login and no TLS in this configuration. Keep it on trusted networks.
  • govulncheck lists one advisory for a module Mailpit's build includes, GO-2026-5932 (the golang.org/x/crypto/openpgp package is unmaintained; there is no fixed version). Mailpit never uses that package, so no vulnerable code is reachable. cloudimg rechecks this at each Mailpit release.
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.

Support and licensing

Mailpit is open source under the MIT licence (Ralph Slooten); the licence text is in /usr/share/doc/cloudimg-mailpit/. Its built in web UI includes open source packages under the MIT, Apache 2.0, BSD, MPL 2.0, ISC, Blue Oak and PSF licences, listed in the app under About. There is no per user or per CPU licence fee. The AMI price covers cloudimg's build, hardening, first boot setup and support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA