SpfInclude derived the mechanism by reducing the relay hostname to its organisational domain. That is unsound, and Brevo is the case that proves it: the relay is smtp-relay.brevo.com, so the reduction yields "include:brevo.com" - but brevo.com publishes the SPF of Brevo's own corporate mail (Google Workspace and Zendesk). The sending ranges live at spf.brevo.com. Publishing the wrong include is worse than publishing none. It authorises Google to send for the domain while leaving the relay unauthorised, so every message the relay forwards fails SPF - and against the "-all" policy this reconciler generates, is rejected rather than merely marked. RELAY_SPF_INCLUDE now carries whatever the provider documents. A bare domain gets the "include:" prefix; any other SPF term (ip4:, a:, mx:) passes through untouched, since not every provider publishes an include. The old reduction stays as a fallback for relays whose hostname does happen to match, because publishing something plausible beats publishing nothing at all. Caught before configuring a relay for real, by checking what brevo.com and spf.brevo.com actually serve rather than trusting the heuristic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
4.1 KiB
Bash
93 lines
4.1 KiB
Bash
# LambdaMail - production environment
|
|
#
|
|
# Copy this into Coolify's environment editor, or to .env on the host, and
|
|
# replace every value marked CHANGE. The stack refuses to start without the
|
|
# required ones rather than falling back to a default, because a default
|
|
# secret in a published repository is not a secret.
|
|
#
|
|
# Generate each secret with: openssl rand -base64 36
|
|
|
|
# --- Container images -------------------------------------------------------
|
|
# Published by .github/workflows/release.yml for linux/amd64 and linux/arm64.
|
|
# Pin IMAGE_TAG to a released version or a sha- tag to roll back.
|
|
IMAGE_NAMESPACE=ghcr.io/lucasrguerra/lambdamail
|
|
IMAGE_TAG=latest
|
|
|
|
# --- Identity ---------------------------------------------------------------
|
|
# MAIL_DOMAIN is the domain you send and receive as.
|
|
# PRIMARY_MAIL_HOST is the host the MX record points at; its PTR must match.
|
|
MAIL_DOMAIN=example.com # CHANGE
|
|
PRIMARY_MAIL_HOST=mail.example.com # CHANGE
|
|
WEBMAIL_HOST=mail.example.com # CHANGE - where the web UI is served
|
|
ADMIN_HOST= # optional separate host for /admin
|
|
PUBLIC_IPV4=203.0.113.10 # CHANGE - the VPS public IPv4
|
|
# Leave empty unless IPv6 has a matching PTR and is covered by SPF. Half
|
|
# configured IPv6 is a common cause of rejection (PLAN.md premises).
|
|
PUBLIC_IPV6=
|
|
|
|
# --- Secrets ----------------------------------------------------------------
|
|
POSTGRES_DB=lambdamail
|
|
POSTGRES_USER=lambdamail
|
|
POSTGRES_PASSWORD=CHANGE-generate-with-openssl-rand
|
|
REDIS_PASSWORD=CHANGE-generate-with-openssl-rand
|
|
# Signs webmail and admin sessions. Minimum 32 characters; the auth service
|
|
# refuses to start below that.
|
|
JWT_SECRET=CHANGE-generate-with-openssl-rand-at-least-32-chars
|
|
# Encrypts DKIM private keys, TOTP secrets and provider tokens at rest.
|
|
# LOSING THIS MAKES EVERY STORED SECRET UNRECOVERABLE - back it up separately
|
|
# from the database (PLAN.md risk R9).
|
|
LAMBDAMAIL_MASTER_KEY=CHANGE-generate-with-openssl-rand-at-least-32-chars
|
|
|
|
# --- DNS automation ---------------------------------------------------------
|
|
# Token scope: Zone.DNS:Edit plus Zone.Zone:Read on the zone. Leave empty to
|
|
# manage the 13 records by hand; the service then logs that sync is off.
|
|
CLOUDFLARE_API_TOKEN=
|
|
DNS_SYNC_INTERVAL_SECONDS=21600
|
|
|
|
# --- TLS --------------------------------------------------------------------
|
|
# traefik: read the certificate Coolify's proxy already manages (default).
|
|
# acme: obtain it here via DNS-01. Required for DANE, and only then.
|
|
TLS_MODE=traefik
|
|
COOLIFY_PROXY_DIR=/data/coolify/proxy
|
|
COOLIFY_PROXY_NETWORK=coolify
|
|
TRAEFIK_ACME_FILENAME=acme.json
|
|
CERT_POLL_INTERVAL_SECONDS=60
|
|
PROTOCOLS_SPOOL_DIR=/var/lambdamail/spool
|
|
# Used only when TLS_MODE=acme.
|
|
ACME_EMAIL=
|
|
ACME_DIRECTORY_URL=
|
|
|
|
# --- Transport security -----------------------------------------------------
|
|
# Start at testing. Promote to enforce only once TLS-RPT reports come back
|
|
# clean: enforce with a broken certificate silently blackholes inbound mail.
|
|
MTA_STS_MODE=testing
|
|
OUTBOUND_MTA_STS_ENABLED=true
|
|
# DANE stays off under TLS_MODE=traefik. The key changes at every renewal and
|
|
# the published TLSA record stops matching, which permanently rejects mail at
|
|
# every validating MTA (PLAN.md section 5.1).
|
|
OUTBOUND_DANE_ENABLED=false
|
|
DNSSEC_RESOLVER=1.1.1.1:53
|
|
MAX_MESSAGE_BYTES=52428800
|
|
ARC_SEAL_ENABLED=false
|
|
|
|
# --- Smarthost --------------------------------------------------------------
|
|
# Most VPS providers block outbound port 25. If the preflight reports it
|
|
# blocked, set a relay here; it is added to the published SPF automatically.
|
|
RELAY_HOST=
|
|
RELAY_PORT=587
|
|
RELAY_USER=
|
|
RELAY_PASS=
|
|
# The SPF term that authorises the relay, exactly as your provider documents
|
|
# it. Set this: it cannot be derived from RELAY_HOST, and getting it wrong is
|
|
# worse than leaving it out. Brevo's relay is smtp-relay.brevo.com, but
|
|
# brevo.com publishes the SPF of their corporate mail - the sending ranges are
|
|
# at spf.brevo.com, so the value here is "include:spf.brevo.com".
|
|
RELAY_SPF_INCLUDE=
|
|
|
|
# --- Storage ----------------------------------------------------------------
|
|
STORAGE_DRIVER=local
|
|
S3_ENDPOINT=
|
|
S3_BUCKET=
|
|
S3_ACCESS_KEY=
|
|
S3_SECRET_KEY=
|