Streaming & Messaging AWS

smtp4dev 3.15.0 on AWS User Guide

| Product: smtp4dev 3.15.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the smtp4dev 3.15.0 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.

smtp4dev is an open source fake SMTP server for development and testing. Point the applications you are building or testing at it instead of a real mail server: it accepts every message, keeps it, and shows it in a web inbox where you can read the rendered HTML, the plain text and the raw MIME source, check headers and attachments, and see HTML validation and email client compatibility warnings. It never delivers anything to a real mailbox, so password resets, order confirmations and notification digests can be exercised safely against realistic data. A REST API exposes the same messages to your test suites.

The cloudimg image adds:

  • SMTP that is not an open mail sink. smtp4dev listens for SMTP on port 2525 and requires SMTP authentication with the login generated for your instance: a sender with no credentials or the wrong ones is refused. STARTTLS is offered with your instance's own certificate, plain text passwords (PLAIN and LOGIN) are only accepted after STARTTLS, and anonymous and OAuth logins are not offered. Port 2525 also avoids the restriction EC2 places on port 25.
  • No relaying, no IMAP, no POP3. smtp4dev can forward captured mail to a real server and can serve it over IMAP and POP3. On this image relaying is not configured and the settings are locked so the web UI and API cannot configure it, and IMAP and POP3 are off. (Upstream's IMAP server accepts any user name and password unless authentication is required, which would expose every captured message.)
  • HTTPS from the first boot. nginx serves the web inbox on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • A login unique to your instance. The user admin and a password generated on first boot protect the web inbox, the REST API, the live update channel and the API documentation, and authenticate SMTP. The password is written to /root/smtp4dev-credentials.txt (root only); nginx keeps a bcrypt hash of it and smtp4dev checks it again behind nginx. No password, settings file or key is baked into the image, and neither nginx nor smtp4dev starts until first boot has created them.
  • Loopback only and locked down. The smtp4dev web app listens on 127.0.0.1:5000 behind nginx and runs as its own unprivileged user with no capabilities, in a systemd sandbox where its settings and certificate are read only. smtp4dev and nginx cannot reach the EC2 instance metadata service.
  • Hardened defaults. The AMI requires IMDSv2; requests for a host name the instance does not serve are dropped; another web site cannot drive the inbox with your browser's saved login; nginx passes smtp4dev the real client address, not one a client claims.
  • A dedicated data volume. Captured messages live in SQLite at /var/lib/smtp4dev on their own 20 GiB gp3 EBS volume, which you can resize independently.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on (they also patch the ASP.NET Core runtime smtp4dev runs on).

What is on the image: smtp4dev 3.15.0 (the official release build) at /opt/smtp4dev on the Ubuntu ASP.NET Core 10 runtime, nginx, swaks (a command line SMTP client), 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.
  • A security group that allows TCP 22 (SSH), TCP 443 (the web inbox) and TCP 80 (redirects to HTTPS) from your own networks, and TCP 2525 (SMTP) from the networks or security groups of the applications that will send mail to smtp4dev. Keep all four closed to the rest of the internet.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick smtp4dev 3.15.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. The AMI includes the 20 GiB data volume for captured mail; increase its size on the launch screen if you keep a lot of mail.

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=smtp4dev}]'

Step 2: Connect to your instance

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

Step 3: Check the services

First boot takes a few seconds. Then the three units are active:

systemctl is-active smtp4dev nginx smtp4dev-firstboot
active
active
active

smtp4dev (a .NET process) serves the web app on loopback only and SMTP on 2525; nginx serves ports 80 and 443. Nothing listens on 25, 143 (IMAP) or 110 (POP3):

sudo ss -tlnp | awk 'NR==1 || /dotnet|nginx|sshd/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 127.0.0.1:5000 users:(("dotnet"))
LISTEN *:2525 users:(("dotnet"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
cat /opt/smtp4dev/.cloudimg-version
dotnet --list-runtimes | grep AspNetCore
findmnt -no TARGET,SOURCE,FSTYPE,SIZE /var/lib/smtp4dev
3.15.0
Microsoft.AspNetCore.App 10.0.12 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
/var/lib/smtp4dev /dev/nvme1n1 ext4   19.5G

Step 4: Get your URL, SMTP host and password

sudo cat /root/smtp4dev-credentials.txt

The file holds the address smtp4dev is served as (your instance's public DNS name by default), the SMTP host and port, the user and the password generated on this instance:

SMTP4DEV_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
SMTP4DEV_USERNAME=admin
SMTP4DEV_PASSWORD=<24 letters and digits>
SMTP4DEV_SMTP_HOST=ec2-<public-ip-dashed>.<region>.compute.amazonaws.com
SMTP4DEV_SMTP_PORT=2525

The same user and password sign in to the web inbox and authenticate SMTP.

Step 5: Send your first message

swaks is installed for testing. This sends a message to smtp4dev over STARTTLS with SMTP authentication; the password is read from the credentials file into an environment variable that swaks reads, so it never appears on a command line:

export SWAKS_OPT_auth_password="$(sudo sed -n 's/^SMTP4DEV_PASSWORD=//p' /root/smtp4dev-credentials.txt)"
swaks --server 127.0.0.1:2525 --tls --auth LOGIN --auth-user admin \
  --from app@example.com --to jane.doe@example.com \
  --header 'Subject: Hello from swaks' --body 'My first captured message' | grep -E '^ ?(<~|~>) +(AUTH|235|250 Mail)'
unset SWAKS_OPT_auth_password
 ~> AUTH LOGIN
<~  235 Authenticated OK
<~  250 Mail accepted

smtp4dev accepted the message and kept it. It was not delivered to jane.doe@example.com.

Step 6: Read captured mail in your browser

Open SMTP4DEV_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or install your own certificate (Step 12). Your browser then asks for a user name and password: enter admin and the password from Step 4.

smtp4dev opens on the inbox, with every captured message listed by time, sender, recipient and subject. New messages appear as they arrive. Select one to see it rendered as a mail client would show it, with the From, To and Subject at the top. HTML is sanitised for display by default, so a hostile message cannot run script in your browser.

The smtp4dev inbox with four captured messages and an order confirmation email rendered on the right

The Source tab shows the exact MIME your application produced, Headers lists every header, Parts breaks out the alternative bodies and attachments, and Analysis reports HTML validation problems and features that common email clients do not support.

The Source tab of a captured message showing the raw HTML body with line numbers

The Sessions tab keeps the full SMTP conversation of each connection, which is useful when a client fails to send. Here the client asked for STARTTLS, saw the extra login methods offered after it, and authenticated before sending:

The Sessions tab showing an SMTP conversation with STARTTLS, the AUTH methods offered before and after it, and a successful login

Step 7: Read captured mail through the API

Every view in the web inbox is backed by a JSON REST API under /api/, protected by the same login, which is handy for asserting on mail in automated tests. This helper reads the password from the credentials file and hands it to curl on stdin, so it never appears on a command line:

H=$(sudo sed -n 's/^SMTP4DEV_SMTP_HOST=//p' /root/smtp4dev-credentials.txt)
s4d_api() { sudo sed -n 's/^SMTP4DEV_PASSWORD=\(.*\)$/user = "admin:\1"/p' /root/smtp4dev-credentials.txt | curl -sk -K - --resolve "$H:443:127.0.0.1" "https://$H/api/$1"; }
s4d_api 'Messages?pageSize=5' | jq -r '.results[] | [.from, (.to | join(",")), .subject] | @tsv'
app@example.com jane.doe@example.com    Hello from swaks

/api/Messages/<id> returns a message's details (headers, parts, attachments), /api/Messages/<id>/source its raw MIME, and DELETE /api/Messages/* empties the inbox between test runs. The full API description is at https://<your host>/api/index.html once you are signed in. From another machine, use curl -u admin https://<your host>/api/Messages (curl then prompts for the password).

Step 8: Point your application at smtp4dev

Use these settings in the application under test:

Setting Value
SMTP host SMTP4DEV_SMTP_HOST from Step 4, or the instance's private IP from inside your VPC
Port 2525
Security STARTTLS (the certificate is self signed: turn certificate verification off for this server, or install your own certificate, Step 12)
Authentication user admin, password SMTP4DEV_PASSWORD (PLAIN or LOGIN after STARTTLS, or CRAM-MD5)

For example, in Python:

import smtplib, ssl
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
with smtplib.SMTP("<smtp4dev-host>", 2525) as s:
    s.starttls(context=ctx)
    s.login("admin", "<SMTP4DEV_PASSWORD>")
    s.sendmail("app@example.com", ["jane.doe@example.com"], "Subject: Test\r\n\r\nHello")

and in Node.js with nodemailer:

const transport = nodemailer.createTransport({
  host: "<smtp4dev-host>", port: 2525, secure: false, requireTLS: true,
  tls: { rejectUnauthorized: false },
  auth: { user: "admin", pass: process.env.SMTP4DEV_PASSWORD },
});

Keep the password in your application's secret store, not in source control. If you allowed 2525 only from your application's security group, nothing else can reach the port.

Step 9: Security checks

A sender that does not log in is refused before it can hand over a message:

OUT=$(swaks --server 127.0.0.1:2525 --from app@example.com --to jane.doe@example.com --quit-after RCPT || true)
grep -E '^<\*\*|^ ?<- +250 AUTH' <<<"$OUT"
<-  250 AUTH CRAM-MD5
<** 530 Authentication is required

(Before STARTTLS only CRAM-MD5, which never sends the password, is offered.) Port 80 sends browsers to HTTPS, the health endpoint answers without a password for load balancer checks, the inbox and the API do not, and a host name the instance does not serve gets no answer (000):

H=$(sudo sed -n 's/^SMTP4DEV_SMTP_HOST=//p' /root/smtp4dev-credentials.txt)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'web inbox without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'API without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/Messages"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
ok
web inbox without a password: 401
API without a password: 401
unknown host name: 000

The settings, the password hash and the TLS key are readable only by root and the service that needs them, and the credentials file only by root. The certificate (used for HTTPS and for SMTP STARTTLS) was generated on this instance and names its own addresses:

sudo stat -c '%a %U:%G %n' /etc/smtp4dev/appsettings.json /etc/nginx/smtp4dev.htpasswd /etc/ssl/smtp4dev/smtp4dev.key /root/smtp4dev-credentials.txt
echo | openssl s_client -connect 127.0.0.1:2525 -starttls smtp 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
640 root:smtp4dev /etc/smtp4dev/appsettings.json
640 root:www-data /etc/nginx/smtp4dev.htpasswd
640 root:smtp4dev /etc/ssl/smtp4dev/smtp4dev.key
600 root:root /root/smtp4dev-credentials.txt
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

Browsers resend a saved login on requests that other sites trigger, so nginx refuses any change request or WebSocket whose Origin is another site (HTTP 403).

Step 10: Locked settings and no relaying

smtp4dev reads its settings from /etc/smtp4dev/appsettings.json, which the service can read but not change. Relaying is not configured, IMAP and POP3 are off, SMTP authentication is required, and the settings are locked, so the Settings dialog in the web inbox shows them read only and the API refuses changes:

H=$(sudo sed -n 's/^SMTP4DEV_SMTP_HOST=//p' /root/smtp4dev-credentials.txt)
sudo python3 -c 'import json; d=json.load(open("/etc/smtp4dev/appsettings.json")); s=d["ServerOptions"]; print({k: s[k] for k in ("Port","ImapPort","Pop3Port","AuthenticationRequired","SmtpAllowAnyCredentials","TlsMode","LockSettings")}); print("Relay server:", repr(d["RelayOptions"]["SmtpServer"]))'
sudo sed -n 's/^SMTP4DEV_PASSWORD=\(.*\)$/user = "admin:\1"/p' /root/smtp4dev-credentials.txt | curl -sk -K - --resolve "$H:443:127.0.0.1" -X POST -H "Origin: https://$H" -H 'Content-Type: application/json' -d '{"relaySmtpServer":"smtp.example.com"}' -w ' (HTTP %{http_code})\n' "https://$H/api/Server"
{'Port': 2525, 'ImapPort': None, 'Pop3Port': None, 'AuthenticationRequired': True, 'SmtpAllowAnyCredentials': False, 'TlsMode': 'StartTls', 'LockSettings': True}
Relay server: ''
Settings are locked (HTTP 401)

The smtp4dev Settings dialog with every setting shown locked, authentication required for SMTP and for the web inbox

smtp4dev keeps the newest 100 messages and 100 SMTP sessions and accepts messages of up to 25 MiB. To change these or other options, edit /etc/smtp4dev/appsettings.json as root and run sudo systemctl restart smtp4dev. The option names are documented in /opt/smtp4dev/appsettings.json. smtp4dev-set-password and smtp4dev-set-hostname rewrite the file with the cloudimg defaults, so re-apply your edits after running them.

Step 11: What smtp4dev can reach

smtp4dev runs as the unprivileged smtp4dev user with no capabilities and a read only view of the system. The kernel refuses its connections, and those of nginx, to the EC2 instance metadata service:

systemctl show smtp4dev -p User -p ProtectSystem -p NoNewPrivileges -p CapabilityBoundingSet -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
CapabilityBoundingSet=
User=smtp4dev
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

Step 12: Your own DNS name and certificate

Point a DNS record at the instance, then set the name smtp4dev is served as. This mints a new self signed certificate for the name (used for HTTPS and for SMTP STARTTLS), updates nginx, the SMTP greeting and the credentials file, restarts smtp4dev and keeps the name across reboots:

sudo smtp4dev-set-hostname <your-domain>

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

sudo install -o root -g root -m 0644 <your-domain>.crt /etc/ssl/smtp4dev/smtp4dev.crt
sudo install -o root -g smtp4dev -m 0640 <your-domain>.key /etc/ssl/smtp4dev/smtp4dev.key
sudo nginx -t && sudo systemctl reload nginx && sudo systemctl restart smtp4dev

Step 13: Change the password

smtp4dev-set-password sets a new password for admin (web inbox, API and SMTP), records it in the credentials file, reloads nginx and restarts smtp4dev. Run it without arguments for a new random password, or pass your own (16 to 64 letters and digits) on stdin. Update the applications that send mail with the new password.

sudo smtp4dev-set-password
printf '%s\n' '<your-new-password>' | sudo smtp4dev-set-password --stdin

Step 14: Data, backups and upgrades

Captured mail is in the SQLite database /var/lib/smtp4dev/database.db on the separate data volume, so you can resize or snapshot that volume on its own. smtp4dev is a test tool and keeps only the newest messages; snapshot the volume if you want to keep a copy. The image ships the version named in the listing; to move to a newer release, launch the newer version from the listing when cloudimg publishes it.

Step 15: Server components

Component Path
smtp4dev release /opt/smtp4dev
Settings (read only to the service) /etc/smtp4dev/appsettings.json (root:smtp4dev, 0640)
Message store (data volume) /var/lib/smtp4dev
Service unit and metadata drop-in /etc/systemd/system/smtp4dev.service, /etc/systemd/system/smtp4dev.service.d/
nginx site /etc/nginx/sites-available/cloudimg-smtp4dev
Password hash (bcrypt) /etc/nginx/smtp4dev.htpasswd (root:www-data, 0640)
Certificate and key /etc/ssl/smtp4dev/
First boot script /usr/local/sbin/smtp4dev-firstboot.sh
Host name and password helpers /usr/local/sbin/smtp4dev-set-hostname, /usr/local/sbin/smtp4dev-set-password
Credentials /root/smtp4dev-credentials.txt (mode 0600)

Step 16: Security recommendations

  • Allow 2525 only from the networks or security groups of the applications under test, and 22, 80 and 443 only from your own networks.
  • Treat captured mail as sensitive: it is whatever your applications sent, including password reset links and personal data from test fixtures. Empty the inbox between test runs (DELETE /api/Messages/*).
  • Keep the password secret, and rotate it if it may have been shared (Step 13).
  • Use your own DNS name and a certificate your clients trust (Step 12).
  • 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

smtp4dev is open source under the BSD 3-Clause licence (a copy is at /usr/share/doc/smtp4dev/LICENSE); there is no per user or per CPU licence fee. The AMI price covers cloudimg's 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