# Stalwart Mail Server — Dezky PRODUCTION (bare-metal host, outside k3s) # # Topology (see host/README.md): # - Mail protocol ports bind directly on the host's public IP. # - Web/JMAP is served plaintext on 127-reachable :8080 and fronted by # Traefik (k3s) for mail.dezky.eu:443. Stalwart does NOT bind 80/443 — # those belong to Traefik. # - TLS for the mail-protocol ports uses a cert ISSUED BY cert-manager # (mail.dezky.eu) and copied here by stalwart/cert-sync.sh. Stalwart runs # no ACME of its own (80/443 are Traefik's). # - Storage is RocksDB on local disk — intentionally independent of the # in-cluster Postgres so mail keeps flowing regardless of cluster state. # # Reference: https://stalw.art/docs [server] hostname = "mail.dezky.eu" # MUST match the IP's PTR/rDNS record # ── Listeners ────────────────────────────────────────────────────────────── # Mail protocols on the public IP; management/JMAP on internal 8080 only # (firewall blocks 8080 from the world, allows the k3s pod CIDR + Traefik). [server.listener] "smtp" = { bind = "[::]:25", protocol = "smtp" } "submission" = { bind = "[::]:587", protocol = "smtp", tls.implicit = false } "submissions" = { bind = "[::]:465", protocol = "smtp", tls.implicit = true } "imap" = { bind = "[::]:143", protocol = "imap", tls.implicit = false } "imaps" = { bind = "[::]:993", protocol = "imap", tls.implicit = true } "sieve" = { bind = "[::]:4190", protocol = "managesieve" } # Internal HTTP: JMAP + WebAdmin + management API. Traefik terminates TLS for # the public hostname and proxies here; platform-api (pod) calls it directly. "http" = { bind = "0.0.0.0:8080", protocol = "http" } # ── Storage — RocksDB on local disk (host-isolated from the cluster) ──────── [store."rocksdb"] type = "rocksdb" path = "/opt/stalwart/data" compression = "lz4" [storage] data = "rocksdb" fts = "rocksdb" blob = "rocksdb" lookup = "rocksdb" directory = "internal" [directory."internal"] type = "internal" store = "rocksdb" # ── TLS — cert issued by cert-manager, synced here by cert-sync.sh ────────── # Until the first sync runs, install.sh drops a self-signed bootstrap cert so # the TLS listeners can start. cert-sync replaces it with the real LE cert. [certificate."default"] cert = "%{file:/opt/stalwart/etc/tls/cert.pem}%" private-key = "%{file:/opt/stalwart/etc/tls/key.pem}%" default = true # ── Authentication ───────────────────────────────────────────────────────── # Fallback admin is what platform-api uses for Basic auth on the JMAP # management API (STALWART_ADMIN_USER/PASSWORD on the platform-api side). [authentication] fallback-admin.user = "admin" fallback-admin.secret = "$env{STALWART_ADMIN_PASSWORD}" # ── Resolver ─────────────────────────────────────────────────────────────── # DNSSEC-aware system resolver. Mail deliverability depends on clean DNS. [resolver] type = "system" preserve-intermediates = true concurrency = 4 # ── Spam filtering — built-in filter ON in production ────────────────────── [spam-filter] enable = true # ── Logging — journald captures stdout ───────────────────────────────────── [tracer."stdout"] type = "stdout" level = "info" ansi = false enable = true # ── Audit webhook → platform-api (via the public api ingress) ────────────── # Stalwart on the host reaches platform-api through Traefik on the public # hostname; HMAC-signed so a public endpoint is safe. [webhook."audit-ingest"] url = "https://api.dezky.eu/ingest/stalwart/webhook" signature-key = "$env{STALWART_WEBHOOK_SECRET}" events = [ "auth.success", "auth.failure", "auth.banned", "account.created", "account.deleted", "account.password-changed", "message.rejected", "policy.rejection", "dkim.failure", "dmarc.failure", "spam.detected", ] throttle = "1s"