Architecture: Stalwart Mail Server
## Deployment: Stalwart Mail Server
Stalwart Mail Server — Rust single-binary mail server handling SMTP relay for transactional email (password resets, notifications).
### Diagram
```mermaid
graph TB
subgraph Internet
Recipient[Recipient Mail Servers]
Bounce[Bounce Messages]
end
subgraph Hetzner["Hetzner Edge VPS"]
Caddy[Caddy :443]
subgraph Stalwart["Stalwart Mail Server"]
SMTP_In[SMTP :25]
Submission[Submission :587]
IMAP[IMAPS :993]
Admin[Admin UI :8080]
DKIM[DKIM Signing]
end
end
subgraph k3s["Home k3s Cluster"]
Keycloak[Keycloak Realms]
end
Keycloak -->|sends via port 587| Submission
Submission --> DKIM
DKIM -->|signed email| Recipient
Bounce -->|port 25| SMTP_In
Caddy -->|reverse proxy| Admin
```
### Components
| Component | Purpose | Notes |
|-----------|---------|-------|
| Stalwart SMTP (port 25) | Inbound relay for bounces | Receives NDRs and feedback loops |
| Stalwart Submission (port 587) | Authenticated outbound sending | Keycloak connects here per-realm |
| Stalwart IMAPS (port 993) | Optional mailbox access | For checking bounce mailboxes |
| Stalwart Admin UI (port 8080) | Web management interface | Behind Caddy reverse proxy |
| DKIM Signing | Cryptographic email authentication | Per-domain key pairs |
| Keycloak | Identity provider sending transactional email | Each realm has own from-address |
### Key Decisions
- **Stalwart over alternatives**: Single binary, ~50MB RAM; no Java/Elasticsearch dependencies
- **NOT on k3s**: Requires port 25 + PTR/rDNS on the sending IP — must run on Hetzner VPS
- **SaltStack managed**: Declarative state in `salt/states/stalwart/`; pillar-driven config
- **Per-realm SMTP config**: Each Keycloak realm sends from its own domain (e.g., noreply@landscaping-assistant.app)
- **DKIM per domain**: Keys generated by Stalwart, public key extracted for DNS records
### Related
- `arch-edge-vps` — host VPS where Stalwart runs
- `arch-dns` — DNS records (SPF/DKIM/DMARC) for mail deliverability
- Project: `stalwart-email`
## Integration with Keycloak
Keycloak realms connect via SMTP submission (port 587):
- Config lives in `~/secrets/pal-e-services/k3s.tfvars` under `keycloak_realms.{realm}.smtp`
- Terraform `dynamic "smtp_server"` block in `terraform/keycloak.tf` (lines 47-63)
- Each realm gets its own from-address (e.g., noreply@landscaping-assistant.app)
## Per-Domain Setup Pattern
For each new sending domain:
1. Create domain + account in Stalwart admin UI
2. Extract DKIM public key
3. Add SPF/DKIM/DMARC DNS records
4. Add `smtp = {...}` block to realm in k3s.tfvars
5. Apply terraform to configure Keycloak
## Key Files
- `salt/states/stalwart/init.sls` — install + configure state
- `salt/pillar/stalwart.sls` — domains, accounts, DKIM keys
- `salt/states/top.sls` — assigns stalwart state to edge-proxy
- `terraform/keycloak.tf` — dynamic smtp_server block