Inbox
articleJune 16, 20267 min read

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 the relayhost line 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 = encrypt is mandatory: SES rejects the SASL handshake before TLS is negotiated, so anything weaker than encrypt will 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.com confirms 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.7 is 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_CAfile points at a file that exists and contains the Amazon Trust Services roots. On Debian/Ubuntu run sudo update-ca-certificates; on RHEL run sudo update-ca-trust.
  • System clock skew. A clock more than a few minutes off will fail cert validation. Run timedatectl status and start chronyd or systemd-timesyncd if 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:

  1. Install libsasl2-modules (Debian/Ubuntu) or cyrus-sasl-plain (RHEL). Postfix ships without the PLAIN/LOGIN mechanisms by default.
  2. Re-run sudo postmap /etc/postfix/sasl_passwd after any edit. Postfix only reads the .db.
  3. Make sure the left-hand side of sasl_passwd is byte-identical to the relayhost value, 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.

Tagssmtppostfixaws-seslinux