feat(infra): production host bootstrap and bare-metal Stalwart scaffolding
Host provisioning for the single-server production target: SSH + firewall hardening (nftables allowlist), k3s node registration, bare-metal Stalwart install with systemd units and TLS cert-sync from the cluster secret, and Restic encrypted backup/restore (primary + DR) with timer units. Host-specific secrets live in config.env (gitignored); config.env.example is the template. Also gitignores MemPalace per-project files.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# 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"
|
||||
Reference in New Issue
Block a user