Postfix Relay to AWS SES: Step-by-Step SMTP Setup
Route outbound Postfix mail through AWS SES over authenticated TLS. Edit main.cf, hash the SASL credentials, postmap, restart, and verify the relay with a test message.
By SESMetric Editorial
A Linux box that already runs Postfix can hand off every outbound message to AWS SES with about a dozen lines of config. You keep the local queue, the sendmail interface, and your existing cron jobs; SES handles authentication, TLS, and the reputation work that gets mail past Gmail and Outlook.
This guide walks through a complete Postfix relay AWS SES configuration on a stock Debian or Ubuntu host. Every step is copy-pasteable and verified against Postfix 3.x.
Before you start
You need three things on the SES side and two on the host side.
- An SES identity (domain or address) that is verified in the same region you plan to relay through. The examples below use
us-east-1; substitute your region in therelayhostline if you are elsewhere. - A set of SMTP credentials generated from the SES console under Account dashboard → Create SMTP credentials. This is a separate IAM user with an access-key-derived password; your regular AWS access keys will not authenticate against the SMTP endpoint.
- Production access on the SES account if you intend to send to addresses you do not own. Sandbox accounts can only relay to verified recipients and will reject everything else with a
554(covered in the troubleshooting section).
On the host you need root (or sudo), the postfix and libsasl2-modules packages, and the system CA bundle. On Debian and Ubuntu the bundle ships with ca-certificates; on RHEL-family distros it is in ca-certificates as well.
sudo apt-get update
sudo apt-get install -y postfix libsasl2-modules ca-certificates mailutils
Pick Internet Site when the installer prompts for a configuration profile and set the system mail name to your domain. The rest of this guide rewrites the relevant parts of main.cf anyway.
Edit main.cf for SES relay
Open /etc/postfix/main.cf and add (or replace) the relay block. Leave the rest of the file alone — the defaults are fine for a smarthost setup.
# Hand every outbound message to SES over submission with STARTTLS.
relayhost = [email-smtp.us-east-1.amazonaws.com]:587
# SASL: authenticate to SES using credentials stored in a hashed map.
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous
# TLS: require an encrypted session and verify the SES certificate
# against the system CA bundle.
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# Stop Postfix from rewriting the envelope sender into something SES
# does not recognise; keep the address your application sets.
smtp_generic_maps =
A few notes on the choices:
- The square brackets around the hostname tell Postfix not to do an MX lookup — SES does not publish MX records for its SMTP endpoint, so an MX query would fail.
- Port 587 is the submission port and is the only one that supports STARTTLS plus SASL on SES. Port 25 is rate-limited and unauthenticated; do not use it.
smtp_tls_security_level = encryptis mandatory: SES rejects the SASL handshake before TLS is negotiated, so anything weaker thanencryptwill fail.- On RHEL/CentOS/Amazon Linux replace the CA path with
/etc/pki/tls/certs/ca-bundle.crt.
Store the SASL credentials
Postfix looks up the credentials by the same hostname it dials, so the left-hand side of the map must match relayhost exactly, brackets and port included.
Create /etc/postfix/sasl_passwd:
[email-smtp.us-east-1.amazonaws.com]:587 AKIA...SMTP_USERNAME:SMTP_PASSWORD
Lock the file down before hashing it — the password is effectively a long-lived API key.
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd.db
postmap writes the .db Berkeley DB next to the source file. Postfix only reads the .db; the plaintext file stays around so you can update it and re-run postmap later. Keep both at mode 600.
If you ever rotate the SES SMTP credentials, edit sasl_passwd, re-run postmap, and reload Postfix — there is no need to restart.
Restart and verify the daemon
sudo systemctl restart postfix
sudo systemctl status postfix --no-pager
The status output should show active (running) and a recent timestamp. If Postfix refuses to start, journalctl -u postfix -n 50 will show the parser error — almost always a typo in main.cf or a missing .db.
A quick sanity check on the active config:
sudo postconf -n | grep -E 'relayhost|sasl|tls_security_level|tls_CAfile'
The output should mirror what you wrote into main.cf. If anything is missing, Postfix is reading a different file than you think — check with postconf | grep config_directory.
Send a test message
Use mailx (from mailutils) for the first send. It exercises the local sendmail interface, which is what your applications and cron jobs will use.
echo "Relay test from $(hostname) at $(date)" | \
mail -s "Postfix relay AWS SES smoke test" you@example.com
Or hit sendmail directly to be explicit about the envelope sender:
sendmail -f sender@yourdomain.com you@example.com <<'EOF'
Subject: Postfix relay AWS SES smoke test
From: sender@yourdomain.com
Relay test body.
EOF
Tail the log immediately:
sudo tail -f /var/log/mail.log
A successful relay produces three lines that look roughly like this:
postfix/pickup[1234]: A1B2C3D4: uid=0 from=<sender@yourdomain.com>
postfix/cleanup[1235]: A1B2C3D4: message-id=<...>
postfix/smtp[1236]: A1B2C3D4: to=<you@example.com>, relay=email-smtp.us-east-1.amazonaws.com[1.2.3.4]:587, delay=0.7, status=sent (250 Ok 0100018f...)
The pieces that matter:
relay=email-smtp.us-east-1.amazonaws.comconfirms Postfix went to SES, not direct-to-MX.status=sent (250 Ok ...)is SES acknowledging the message. The hex token at the end is the SES message ID — keep it for support tickets.delay=0.7is end-to-end seconds. Anything under 2s on a warm connection is normal.
If you see status=deferred or status=bounced instead, jump to the next section.
Troubleshooting common errors
TLS handshake fails
Symptom in /var/log/mail.log:
postfix/smtp[1236]: SSL_connect error to email-smtp.us-east-1.amazonaws.com[...]:587: -1
postfix/smtp[1236]: warning: TLS library problem: ... certificate verify failed
Causes and fixes:
- Wrong CA path. Confirm
smtp_tls_CAfilepoints at a file that exists and contains the Amazon Trust Services roots. On Debian/Ubuntu runsudo update-ca-certificates; on RHEL runsudo update-ca-trust. - System clock skew. A clock more than a few minutes off will fail cert validation. Run
timedatectl statusand startchronydorsystemd-timesyncdif it is not active. - Outbound 587 blocked. Test reachability with
nc -vz email-smtp.us-east-1.amazonaws.com 587. Some cloud providers block submission ports by default — open it in the security group or host firewall.
SASL authentication fails
Symptom:
postfix/smtp[...]: warning: SASL authentication failure: No worthy mechs found
postfix/smtp[...]: SASL authentication failed; cannot authenticate to server email-smtp...: no mechanism available
Fix in this order:
- Install
libsasl2-modules(Debian/Ubuntu) orcyrus-sasl-plain(RHEL). Postfix ships without the PLAIN/LOGIN mechanisms by default. - Re-run
sudo postmap /etc/postfix/sasl_passwdafter any edit. Postfix only reads the.db. - Make sure the left-hand side of
sasl_passwdis byte-identical to therelayhostvalue, brackets and port included.
A different SASL message — 535 Authentication Credentials Invalid — means the credentials themselves are wrong. Regenerate the SMTP user in the SES console; the username and password come straight from the Show user SMTP credentials modal and cannot be derived from an existing IAM access key without re-running the helper.
554 Message rejected from SES sandbox
postfix/smtp[...]: ... status=bounced (host email-smtp...: 554 Message rejected:
Email address is not verified. The following identities failed the check in
region US-EAST-1: you@example.com)
Your account is still in the SES sandbox. Either verify the recipient under Verified identities for testing, or request production access from Account dashboard → Request production access. Approval usually takes under a day.
Rate-limit deferrals
postfix/smtp[...]: ... status=deferred (host email-smtp...: 454 Throttling failure:
Maximum sending rate exceeded)
SES enforces both a per-second send rate and a 24-hour quota. Postfix will retry automatically, but if you see this often, slow the queue down with two main.cf settings:
smtp_destination_concurrency_limit = 2
smtp_destination_rate_delay = 1s
That caps you at two concurrent connections to SES and inserts a one-second gap between messages per connection — well under any production-tier limit. Long term, request a quota increase in the SES console.
Mail leaves the host but never arrives
Look at the SES side: open SES → Sending statistics and Suppressed destinations. A message accepted with 250 Ok and then never delivered usually shows up there as a bounce or a complaint, not in /var/log/mail.log. Configure an SNS-backed bounce notification on the verified identity so you do not have to refresh the console.
Next steps
Once the relay is steady, tighten the rest of the stack: publish SPF, DKIM, and DMARC for the sending domain, and route bounce and complaint notifications into a webhook your application listens on. A clean relay plus authenticated headers plus active feedback handling is what keeps the SES reputation that you have just plugged into above zero.