Files
LambdaMail/.env.production.example
T
Lucas RayanandClaude Opus 5 de16efe874 #27: never publish a DKIM record with an empty key, and say why one is missing
A fresh deployment published "v=DKIM1; k=rsa; p=" for its own domain. An empty
p= is not an absent key, it is a revoked one (RFC 6376 section 3.6.1): every
verifier reading it treats all signatures from that selector as invalid. On a
domain whose DMARC policy is p=quarantine with strict alignment - which this
reconciler generates - that quarantines all of its own outbound mail. Worse
than publishing nothing.

The chain: DkimRepository.Insert is an INSERT ... SELECT conditional on the
domain existing in the domains table, so on an empty database it inserts no
rows and returns no error. FindPublicKey then finds nothing, ensure() returns
an empty string with no error, and the reconciler faithfully published the
empty value. Every field along that path was behaving as written; nothing
checked that the result meant anything.

Two changes:

- The record builder omits a DKIM record when its key is empty, so the
  revocation cannot be published regardless of why the key is missing.
- Provisioning logs when it produced no key, naming the likely cause. The
  insert being conditional on an onboarded domain is exactly the case a new
  operator hits, and "no key, domain probably not onboarded" is the difference
  between a diagnosis and unsigned mail with no explanation. It stays
  non-fatal: erroring here would abort the whole reconcile and leave a new
  install with no A, MX or SPF records either.

Found while migrating to a new host: the fresh database had no domain row, so
the first reconcile revoked the domain's DKIM. The two published records have
been deleted from the zone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 09:14:54 -03:00

105 lines
4.8 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
# except POSTGRES_PASSWORD and REDIS_PASSWORD, which go inside connection URIs
# and must be URL-safe - see the note above them.
# --- 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 ----------------------------------------------------------------
# These two are substituted into connection URIs - postgres://user:PASS@db/... and
# redis://:PASS@redis:6379 - so they must contain no character that means
# something in a URL. Use hex:
#
# openssl rand -hex 24
#
# NOT `openssl rand -base64`, whose alphabet includes "/" and "+". A "/" ends
# the authority component, so the host is parsed as part of the password and
# every connection fails authentication - which surfaces as the migrations
# container exiting 1 while Postgres itself reports healthy.
POSTGRES_DB=lambdamail
POSTGRES_USER=lambdamail
POSTGRES_PASSWORD=CHANGE-generate-with-openssl-rand-hex-24
REDIS_PASSWORD=CHANGE-generate-with-openssl-rand-hex-24
# 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=