ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
177
docs/spec/authoritative-email-server-spec.md
Normal file
177
docs/spec/authoritative-email-server-spec.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Authoritative Email Server Specification
|
||||
|
||||
## 1. Scope
|
||||
|
||||
Stalwart is the authoritative email server for `quorate.dev`. It keeps hosted delivery inside the cluster and submits external delivery through OCI Email Delivery.
|
||||
|
||||
This specification owns the Stalwart deployment, configuration, routing, storage, protocol, identity, and filtering boundaries.
|
||||
|
||||
## 2. Topology
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Ingress["Declared ingress surfaces"]
|
||||
Stalwart["Stalwart\n≥2 replicas"]
|
||||
Controller["Configuration controller\nexactly one owner"]
|
||||
PG["PostgreSQL\ndurable state"]
|
||||
S3["VersityGW\ndurable blobs"]
|
||||
Cache["DragonflyDB\nnon-durable state"]
|
||||
OCI["OCI Email Delivery"]
|
||||
Hosted["Hosted mailbox"]
|
||||
External["External recipient"]
|
||||
|
||||
Ingress --> Stalwart
|
||||
Controller --> Stalwart
|
||||
Stalwart --> PG
|
||||
Stalwart --> S3
|
||||
Stalwart --> Cache
|
||||
Stalwart --> Hosted
|
||||
Stalwart -->|"authenticated TLS"| OCI
|
||||
OCI -->|"best-effort TLS"| External
|
||||
```
|
||||
|
||||
## 3. Configuration Ownership
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Empty
|
||||
Empty --> RecoveryAccess: one Job starts
|
||||
RecoveryAccess --> RecoveryAccess: retry after interruption
|
||||
RecoveryAccess --> Reconciled: controller applies ordinary desired state
|
||||
Reconciled --> Ownerless: Job, controller, and recovery workload removed
|
||||
Ownerless --> Steady: replicas and controller start
|
||||
Steady --> Steady: continuous reconciliation
|
||||
```
|
||||
|
||||
| Owner | Authority |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Git | All readable, non-secret desired state |
|
||||
| Vault | Service credentials |
|
||||
| Bootstrap Job | At most one one-shot Job; owns only temporary recovery access and waits for controller readiness |
|
||||
| Controller | Sole owner of declared Stalwart configuration during initialization and steady operation, plus bounded certificate-ID derivation |
|
||||
| Terraform | Dependency and external-service state |
|
||||
| Flux | Bootstrap or normal workload state, never both |
|
||||
|
||||
The bootstrap Job contains no Stalwart desired-state plan and performs no configuration mutation. It provides the recovery-authenticated management endpoint while the controller applies the ordinary desired-state plan, then exits after the controller reports successful convergence. No Stalwart `Bootstrap` object is used. The recovery credential and recovery workload are absent before the steady controller starts, where the permanent controller identity is authenticated independently.
|
||||
|
||||
The deployment has the following minimum identities:
|
||||
|
||||
| Identity | Kind | Authority or use | Identity authority | Hosted mailbox |
|
||||
| ------------------------- | -------------------- | ------------------------------------------------------------------- | ----------------------------- | -------------- |
|
||||
| Recovery administrator | Temporary automation | Recovery-authenticated management during bootstrap only | — | None |
|
||||
| Configuration controller | Automation | Declarative configuration reconciliation | Zitadel | None |
|
||||
| Alert submitter | Automation | Authenticated cluster-local SMTP submission to the operator mailbox | Zitadel | None |
|
||||
| Authenticated client test | Automation | TCP/465 and TCP/993 protocol qualification as `[email protected]` | Zitadel | Required |
|
||||
| Human administrator | Human | Administration and operator mailbox membership | Zitadel | — |
|
||||
| `[email protected]` | Shared mailbox | Operational alerts and Android shared-folder access | Zitadel-authenticated members | Required |
|
||||
| `[email protected]` | Test mailbox | Authenticated client, inbound delivery, and routing acceptance | Authenticated client test | Required |
|
||||
|
||||
Zitadel provides the human-administrator role. The human administrator is a member of the operator shared mailbox.
|
||||
|
||||
Human identity remains Zitadel-owned. Mail clients that cannot use the Zitadel access token through `OAUTHBEARER` or `XOAUTH2` use a per-user Stalwart app password as a revocable protocol credential.
|
||||
|
||||
The controller:
|
||||
|
||||
| Field | Contract |
|
||||
| ------------------ | ------------------------------------------------------------------------ |
|
||||
| Ownership | PostgreSQL advisory lock; a competing owner performs no reconciliation |
|
||||
| Interval | 5 minutes |
|
||||
| Timeout | 2 minutes per reconciliation |
|
||||
| Retry | Exponential backoff from 5 seconds to 5 minutes |
|
||||
| Persistent failure | Unready after 5 consecutive failures while retries continue |
|
||||
| Desired input | Git state plus runtime-projected secret values |
|
||||
| Derived input | Server certificate ID resolved from its declared SAN |
|
||||
| Secret rotation | Vault-owned service credentials converge after rotation |
|
||||
| Pruning | Exhaustive declared configuration is pruned; accounts are upserted |
|
||||
| Status | Liveness, readiness, last success, persistent failure, and plan revision |
|
||||
|
||||
Manual API patches, generated snapshot adoption, and live `kubectl` mutation are not deployment or configuration mechanisms. Break-glass mutation is limited to failure response and teardown.
|
||||
|
||||
| App-password frontend | Contract |
|
||||
| --------------------- | --------------------------------------------------------------- |
|
||||
| Implementation | Custom UI backed by Zitadel login and Stalwart app-password API |
|
||||
| Scope | Signed-in user's app passwords |
|
||||
| Endpoint | `mail.quorate.dev/account/` |
|
||||
| Ingress | Gateway R and direct Stalwart frontend |
|
||||
| TLS | Gateway R certificate on both ingress paths |
|
||||
| Root redirect | HTTPS `/` → `/account/` |
|
||||
|
||||
## 4. Protocol Boundary
|
||||
|
||||
| Surface | Required listener availability | TLS | Authentication |
|
||||
| --------------------------------------------- | -------------------------------- | ----------------------------- | -------------------------------------- |
|
||||
| SMTP TCP/25 | Public-ingress target | Opportunistic STARTTLS | AUTH prohibited |
|
||||
| Submissions TCP/465 | Public-ingress target | Implicit TLS | Zitadel token or per-user app password |
|
||||
| Submission TCP/587 | Cluster-local | Cilium transparent encryption | Vault-backed automation authentication |
|
||||
| IMAPS TCP/993 | Public-ingress target | Implicit TLS | Zitadel token or per-user app password |
|
||||
| JMAP and user HTTPS TCP/443 | Public-ingress and direct target | Required | Zitadel-backed human authentication |
|
||||
| Administration API at `mail.quorate.internal` | Internal-ingress target | Required outside the cluster | Zitadel-backed human authentication |
|
||||
|
||||
## 5. Routing
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Copy["Accepted recipient copy"]
|
||||
Classify{"In Stalwart's\nhosted-domain set?"}
|
||||
Local["Local delivery\nPostgreSQL + VersityGW"]
|
||||
Explicit{"Explicit external\nrecipient or disclosure?"}
|
||||
Relay["OCI Email Delivery"]
|
||||
Reject["Reject"]
|
||||
|
||||
Copy --> Classify
|
||||
Classify -->|yes| Local
|
||||
Classify -->|no| Explicit
|
||||
Explicit -->|yes| Relay
|
||||
Explicit -->|no| Reject
|
||||
```
|
||||
|
||||
| Condition | Result |
|
||||
| -------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| Hosted account, alias, or mailbox | Deliver or queue entirely inside the cluster |
|
||||
| Unknown hosted recipient | Reject during SMTP without a later bounce |
|
||||
| External authenticated recipient | Submit through OCI Email Delivery |
|
||||
| Mixed recipients | Keep the hosted copy local; submit only the external copy |
|
||||
| Explicit external forwarding or report | Treat as an external disclosure and minimize quoted original content |
|
||||
| OCI failure | Keep hosted delivery available and queue external delivery |
|
||||
| Local-store failure | Queue locally or return `451`; never reclassify as external |
|
||||
| SMTP pressure | Apply bounded session and concurrency controls; return retryable `451` where SMTP permits it |
|
||||
| Routing loop | Never convert a hosted destination into external delivery |
|
||||
|
||||
Hosted-domain classification comes from Stalwart's authoritative domain set, not suffix matching or a duplicated routing list. Authenticated cluster-local submission to hosted recipients never contacts OCI.
|
||||
|
||||
## 6. Storage and Credentials
|
||||
|
||||
| Component | Role |
|
||||
| ----------- | -------------------------------------------------- |
|
||||
| PostgreSQL | Durable account, metadata, queue, and search state |
|
||||
| VersityGW | Durable message bodies and attachments |
|
||||
| DragonflyDB | Disposable Redis-compatible state |
|
||||
| Vault | Source authority for service credentials |
|
||||
|
||||
Stalwart reports SMTP success only after the required PostgreSQL state and VersityGW blob are durable. Dependency failure causes local queueing or a temporary failure without changing the recipient's routing class.
|
||||
|
||||
Coordinated PostgreSQL and VersityGW recovery is outside this specification.
|
||||
|
||||
## 7. Encryption and Filtering
|
||||
|
||||
The server-wide application-encryption capability is enabled.
|
||||
|
||||
| Identity | Application-layer encryption policy |
|
||||
| ---------------------------------------------------- | ------------------------------------------------ |
|
||||
| Recovery, controller, and alert-submitter identities | Not applicable; none has a mailbox |
|
||||
| `[email protected]` | Disabled |
|
||||
| External human account | Enabled by default; explicit per-account opt-out |
|
||||
|
||||
Human onboarding records the OpenPGP public key or S/MIME certificate, recovery implications, supported search boundary, spam-training consequences, and append-encryption choice. Delivery and append encryption are verified independently.
|
||||
|
||||
Encrypted and plaintext accounts coexist without changing each other's policy. Plaintext accounts retain body search. Encrypted accounts expose only supported header search. Plaintext spam training requires recorded consent.
|
||||
|
||||
Stalwart's built-in spam and phishing filter is the only v1 content filter. It runs before application-layer encryption for unauthenticated inbound mail. Authenticated alert submission bypasses inbound spam classification.
|
||||
|
||||
Resolver and filter-update concurrency, cache, and timeouts are bounded. Resolver failure does not reject all inbound mail. Filter-update failure keeps the last valid data or enters the declared bounded degraded behavior without opening general egress.
|
||||
|
||||
OpenPGP and S/MIME remain available when message content must stay opaque to OCI Email Delivery and subsequent relays.
|
||||
|
||||
## 8. Mail Identity
|
||||
|
||||
SPF, OCI DKIM, DMARC, the envelope sender, bounce handling, MTA-STS, and TLS reporting align with the `quorate.dev` authority.
|
||||
Loading…
Reference in a new issue