Files
dezky/infrastructure/production/host/stalwart/config.toml
T
Ronni Baslund 149eb0b020
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
fix(infra): Stalwart installer — repo rename + exact asset; flag 0.16 config break
- install.sh: default repo stalwartlabs/mail-server -> stalwartlabs/stalwart
  (renamed), and select the exact /stalwart-<target>.tar.gz asset excluding the
  foundationdb build (head -n1 could grab the wrong one).
- config.toml: $env{...} -> %{env:...}% (correct Stalwart macro syntax).

KNOWN ISSUE: Stalwart v0.16 removed TOML config (single config.json datastore +
everything else in the DB via CLI/UI), so this config.toml does not load on
0.16.8 ("Failed to parse data store settings"). Needs either a pinned pre-0.16
version or a migration to the v0.16 config model. Binary is installed; the
service is stopped pending that decision.
2026-06-08 20:51:56 +02:00

103 lines
4.3 KiB
TOML

# 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"