Mail transport
The RapidMX server doesn't speak SMTP to the internet itself. It hands that job to a dedicated mail transfer agent and talks to it over a small internal API. There are two transports to choose from:
- Postfix with postfix-bridge, which the Helm chart and the single-server installer install for you. It receives mail on port 25 and delivers your outgoing mail directly to recipients' servers.
- Amazon SES with ses-bridge, which the AWS deployment uses. SES receives and sends the mail, which avoids running port 25 yourself.
This page explains how each one works, what it needs from you, and the settings around it: DKIM signing, TLS between mail servers, and the sender verification the server relies on.
How mail flows
Inbound, Postfix asks postfix-bridge whether it serves the recipient's domain and address before it accepts a message, so mail for unknown addresses is refused during the SMTP conversation. Accepted mail is handed to the server, which scans it with rspamd and ClamAV and files it.
Outbound, the server relays each message to Postfix (the chart's mail.relay.*, port 25 on the cluster network).
Postfix signs it with the domain's DKIM key and delivers it by MX lookup. A delivery that fails bounces back to the
sender's mailbox.
The hand-off API lives at /internal/mta on the server and is authenticated with a shared bearer secret,
mail:transport:ingest:secret (Helm global.mailIngestSecret, Compose MAIL_INGEST_SECRET). postfix-bridge must have
exactly the same value. The public Gateway always answers 404 for /internal, and the server refuses a request to it
that carries proxy headers, so it can't be reached from the internet.
Postfix and postfix-bridge
On Kubernetes and k3s
The chart installs postfix-bridge as its postfixBridge subchart and wires it up: the ingest secret, the address of
the server, and the DKIM volume. You set two things:
| Value | Default | What it is |
|---|---|---|
postfixBridge.hostname | mail.<global.domain> | Postfix's public host name: its HELO name, the name on its TLS certificate, and the name your MX records point at |
postfixBridge.domains | <global.domain> | Comma-separated domains Postfix knows when it starts |
Domains are the server's to manage. Add one in the admin console and, within about 10 seconds, Postfix accepts mail for
it, lets the server send as it and signs its mail with the DKIM key the console shows. Nothing restarts.
postfixBridge.domains is only the starting set. Removing a domain in the console stops it sending at once.
The postfix Service is a LoadBalancer on port 25. Postfix has to reach the internet on port 25 too, so check that
your provider allows outbound port 25 (see Before you deploy).
:::danger Keep the client address real
Port 25 is both your MX and the cluster's own outbound relay. Postfix tells the two apart by the client's address:
clients in postfix.internalNetworks (by default the private ranges, so the pod and node networks) may relay anywhere,
and everyone else may only send to domains the server accepts. That only holds if Postfix sees the real client
address, which is why the postfix Service uses externalTrafficPolicy: Local. With Cluster, k3s' ServiceLB shows
every internet client as an internal address, and Postfix becomes an open relay. Behind another load balancer,
check the address Postfix logs for an outside connection before you open port 25.
:::
TLS between mail servers
Postfix requires TLS in both directions: other servers must use TLS to deliver to it, and it only delivers to servers that offer TLS. Nothing arrives or leaves in the clear. The trade-off is that a server that can't do TLS can't exchange mail with yours. The only plaintext hop is Postfix to postfix-bridge, inside the cluster.
Postfix's certificate comes from, in order:
postfixBridge.tls.existingSecret: akubernetes.io/tlsSecret you name. The single-server installer sets it tomail.example.com-tls-cert, the certificate the chart already issues for the web app, so Let's Encrypt isn't asked for a duplicate.- cert-manager, through the chart's own Issuer (
postfixBridge.tls.certManager.enabled, on by default). - A self-signed certificate, with
postfixBridge.tls.certManager.enabled=false.
Postfix as a separate release
Set postfixBridge.create=false to install the postfix-bridge chart as a
release of its own. Nothing keeps separate releases in step, so give it:
ingestSecret: the exact value of the server'smail.ingestSecret, whichhelm get notestells you how to read;ingestBaseUrl: the server's Service,http://rapidmx-server-services/internal/mta;hostnameanddomains;dkim.storage.existingClaim: the server's DKIM claim,rapidmx-server-dkim-keys, so Postfix signs with the keys the server publishes in DNS;tls.existingSecret, ortls.certManager.enabled=false.
With Docker Compose
The production Compose setup runs
postfix-bridge's own Compose stack in the same project. Its settings are MAIL_HOSTNAME, MAIL_DOMAINS,
MTA_INGEST_BASE_URL and MTA_INGEST_SECRET. Two differences from Kubernetes:
MAIL_DOMAINSis the list of domains Postfix lets the server send from. Keep it in step with the admin console, and restart Postfix after changing it. Inbound acceptance follows the console without a restart.- The Compose stack's Postfix doesn't sign with the server's DKIM keys yet. It signs with a key it generates itself, which doesn't match the record the admin console asks you to publish. Use Kubernetes or the single-server installer if you need DKIM to verify.
DKIM
DKIM signatures let receiving servers check that mail claiming to be from your domain really came from you. RapidMX generates a key pair for each domain when you add it, shows the public key as a DNS record in the domain's checklist, and writes the private key to the DKIM volume, where Postfix reads it.
| Setting | Default | What it is |
|---|---|---|
mail.dkim.selector | mail | The selector, so the record is mail._domainkey.example.com |
mail.dkim.autogenerate | true | Generate keys |
mail.dkim.storage | ReadWriteOnce, 1Gi | The volume both the server and Postfix mount |
Because both pods mount the same ReadWriteOnce volume, the server and Postfix must run on the same node. The DKIM
keys can't be recreated: back them up, see Backup and restore.
Sender verification: mail.trustedAuthservId
Some features need to know that a message really came from the address in its From line: members-only distribution
lists, encryption key discovery, read receipts, calendar replies, message recalls and the e-mail challenges of
automatic signing certificates. The server learns that from the Authentication-Results header the inbound mail
system adds after checking DKIM, but only trusts the header carrying the right authserv-id.
- With the bundled Postfix, it is set for you, to
postfixBridge.hostname. - With any other inbound path (ses-bridge, your own MTA), set
mail.trustedAuthservId(server settingmail:security:trusted_authserv_id) to theauthserv-idyour MTA stamps.
While it's empty, the server logs a warning at startup and fails closed: no sender is verified, so messages to
members-only lists are dropped, list and forwarding copies are sent with a rewritten From, forwarded invitations are
refused, and the features above ignore incoming mail. Your MTA must also remove any Authentication-Results header a
message arrives with before adding its own, or a sender could forge one.
Amazon SES and ses-bridge
On AWS, the server sends through the SES API and receives through ses-bridge. The AWS guide sets this up end to end; this section explains the parts.
Outbound. With mail.transport.provider=ses and postfixBridge.create=false, the server hands each message to SES's
SendEmail API. Credentials come from the standard AWS credential chain: the node's IAM role, or an IAM role for the
pod's ServiceAccount (global.serviceAccount.annotations with eks.amazonaws.com/role-arn). There are no keys to
configure. The From address of every message must be on a verified SES identity, and the account must be out of the
SES sandbox to mail unverified recipients.
| Value | What it is |
|---|---|
mail.transport.ses.region | The region SES sends from. Empty: the SDK's own resolution. |
mail.transport.ses.configurationSet | An SES configuration set, for delivery, bounce and complaint event tracking |
Inbound. ses-bridge is a separate AWS CDK stack, deployed once per mail domain. SES receives the message, stores it
in S3 and invokes a Lambda function that checks each recipient against the server's /internal/mta/resolve and
delivers through /internal/mta/deliver. Unknown recipients are bounced after the message was accepted, because SES has
no way to refuse them during the SMTP conversation. The Lambda reaches the server through an internal load balancer
that mail.ingestService.enabled adds; restrict who can reach it with mail.ingestService.loadBalancerSourceRanges.
After each ses-bridge deploy, publish its three DKIM CNAME records, point the domain's MX record at SES, and activate
the receipt rule set. SES signs outgoing mail with its own (Easy DKIM) keys, so the DKIM row of the admin console's
checklist, which expects a RapidMX key, may stay red.
ses-bridge doesn't yet process SES bounce and complaint notifications, and it passes the ingest secret to the Lambda as a plain environment variable.
E-mail from the sign-in page
The auth-server sends sign-in codes and verification e-mail itself, over SMTP, with its own settings: the chart's
global.smtp values (the auth-server subchart can't read mail.*).
| Value | Default | With SES |
|---|---|---|
global.smtp.host | postfix | email-smtp.<region>.amazonaws.com |
global.smtp.port | 25 | 587 |
global.smtp.ignoreTLS | true (the hop to the bundled Postfix stays in the cluster) | false |
global.smtp.requireTLS | false | true |
global.smtp.from | noreply@<global.domain> | an address on a verified SES identity |
With SES, the auth-server also needs an SES SMTP user name and password, given as smtp_config__auth__user and
smtp_config__auth__pass from a Secret through authserver.service.extraEnv. The AWS installer does this from
RAPIDMX_SES_SMTP_USERNAME and RAPIDMX_SES_SMTP_PASSWORD.
The auth-server copies its SMTP settings into its database the first time it uses them; after that, its admin console's Messages page owns them, and changing the chart values doesn't reach a running deployment by itself.