How To: Use the Shared Non-Production Mailpit Instance¶
Overview¶
One shared, authenticated Mailpit instance catches mail for every SyRF non-production environment: staging, the S30 rehearsal environment (when it exists) and every PR preview. Production always sends through AWS SES — Mailpit is never in the production path (SyRF CLAUDE.md, global constraint G4).
- Web UI / API: https://mailpit.staging.syrf.org.uk (HTTP basic auth, credentials below)
- SMTP:
mailpit.mailpit.svc.cluster.local:1025, from inside the cluster only, and only from namespaces the NetworkPolicy allows (syrf-staging, every preview namespace and, while the S30 rehearsal exists,syrf-rehearsal) - Owner:
plugins/local/mailpit(chart:charts/mailpit), deployed under thepluginsAppProject alongside RabbitMQ, ESO, etc.
Logging in¶
The UI/API login is a single shared operator credential (mailpit-admin), generated by ESO
and stored in the mailpit namespace's mailpit-smtp-credentials Secret under the
MP_UI_AUTH key (format username:password). Fetch it with:
(Read-only kubectl get — never edit this Secret by hand; ESO owns it.)
Per-environment tags¶
Every SMTP credential's username is a static, human-readable environment-class name, and
Mailpit is configured with --tags-username (MP_TAGS_USERNAME=1), so every message sent
with that credential is automatically tagged with the username. Filter the UI (or the
/api/v1/search endpoint) with tag:<name>:
| Environment class | SMTP username | Filter |
|---|---|---|
| Staging | staging |
tag:staging |
| PR previews (shared) | previews-shared |
tag:previews-shared |
S30 rehearsal (isolated user, syrf-rehearsal) |
rehearsal |
tag:rehearsal |
Credentials for apps to consume¶
No SyRF app sends through Mailpit yet — the SMTP transport code is a separate, parallel
change (camaradesuk/syrf#3595). Its config contract is already fixed, so this section documents
exactly what that PR's syrf-staging values wiring should use.
The API/Identity charts' emailTransport.smtp block splits config into plain (non-secret)
Helm values and one Secret:
emailTransport:
provider: smtp
smtp:
host: mailpit.mailpit.svc.cluster.local # plain value — NOT secret-sourced
port: "1025" # plain value
username: staging # plain value — the tag (see above)
tlsMode: None # plain value — Mailpit has no SMTP TLS cert
secretName: mailpit-smtp-credentials # Secret keys: password, fromAddress
timeoutSeconds: "30"
Only password and fromAddress come from a Kubernetes Secret — host/port/username/
tlsMode are ordinary values set directly in the consuming service's staging values file.
host is mailpit.mailpit.svc.cluster.local, not mailpit-smtp.... — the chart exposes
one Service (mailpit) with both the http and smtp ports; don't assume a separate
mailpit-smtp Service exists.
The mailpit-smtp-credentials Secret's source of truth is the mailpit namespace
(ESO-generated, refreshPolicy: CreatedOnce so it never rotates once created). It is fanned
out to other namespaces — never re-generated there, which would silently split the value — via
a ClusterSecretStore (mailpit-smtp-credentials-store, ESO kubernetes provider, restricted
by spec.conditions to the same namespace classes the NetworkPolicy allows) that a
namespace-local ExternalSecret reads with secretStoreRef: {kind: ClusterSecretStore, name:
mailpit-smtp-credentials-store} and remoteRef.key: mailpit-smtp-credentials,
property: <envClass>Password.
syrf-stagingalready has this today:plugins/local/extra-secrets-staging/values.yaml(externalSecrets:entrymailpit-smtp-credentials) creates amailpit-smtp-credentialsSecret insyrf-stagingwith keyspasswordandfromAddress— ready for the SMTP-transport PR to mount viasecretName: mailpit-smtp-credentials, alongside the plain values above.- Rehearsal (S30 isolated synthetic auth rehearsal, namespace
syrf-rehearsal): uses an isolated SMTP user, not a newsmtpUsersentry. The sharedmailpit-smtp-credentialsSecret isrefreshPolicy: CreatedOnce, and ESO never re-syncs a CreatedOnce ExternalSecret after its first sync, so appending a user tosmtpUserswould never add its key. Insteadcharts/mailpitisolatedSmtpUsersgivesrehearsalits own CreatedOnce Secretmailpit-smtp-rehearsal(keypassword), a reader that may get only that Secret, amailpit-smtp-rehearsal-storeClusterSecretStore admitting onlysyrf-rehearsal, and an SMTP NetworkPolicy rule for that namespace. The Deployment appends the entry toMP_SMTP_AUTHat pod start (Kubernetes$(VAR)expansion), so staging/preview credentials are unchanged. The rehearsal's namespace-local copy is declared inplugins/local/extra-secrets-rehearsal(username: rehearsal, filtertag:rehearsal). Adding or removing an isolated user restarts the Mailpit pod once (env change); retained messages are on the PVC. - Previews: all previews share the one
previews-sharedcredential (a per-PR credential isn't worth the ESO/RBAC complexity for a test-mail catcher). Every namespace labelledsyrf.org.uk/environment: preview— every current and futurepr-{n}namespace — already gets its own local copy declaratively, via aClusterExternalSecret(mailpit-smtp-credentials-preview,plugins/local/mailpit/resources/preview-mailpit-smtp.yaml) pointed atmailpit-smtp-credentials-storewithproperty: previewssharedPassword. No per-PR wiring is needed — this matches howbff-redis-preview(plugins/local/valkey-nonprod) already fans the Redis preview credential out. Every preview is switched to SMTP:syrf/environments/preview/services/{api,identity}/values.yamlsetemailTransport.provider: smtpat the "one service, all previews" level (mirrors the staging entries exactly,username: previews-shared) — filter the UI withtag:previews-sharedfor preview mail.
Retention and storage¶
Messages are capped at 1000 / 7 days (MP_MAX_MESSAGES / MP_MAX_AGE) and stored on a 1Gi
PVC (disk-backed SQLite), not in pod memory — the cluster is memory-bound, so keeping message
data off the heap matters more than surviving a pod restart. A restart loses in-flight
messages; re-trigger the email if you need it again.
Changing SMTP/UI credentials¶
Never hand-edit mailpit-smtp-credentials. To force a rotation:
- Delete the Secret in the
mailpitnamespace (ESO recreates it from the Password generators with a fresh random value —CreatedOncemeans a manual delete is required to rotate). - Delete each namespace-local fan-out copy too (e.g.
mailpit-smtp-credentialsinsyrf-staging), or they'll keep the old password until their ownrefreshIntervalexpires — and even after it does re-sync, they'll be out of sync with Mailpit until step 3. - Restart the mailpit Deployment (
kubectl -n mailpit rollout restart deployment/mailpitvia GitOps — e.g. bump a pod annotation incharts/mailpit/values.yamland let ArgoCD sync it, matching thepodAnnotationspattern used elsewhere in this repo, such asplugins/helm/external-secrets-operator/values.yaml'srestart-timestamp).MP_SMTP_AUTH/MP_UI_AUTHare plain env vars viasecretKeyRef— they do not hot-reload, so the running pod keeps authenticating with the old password until it restarts.