Maildown Security Model
Maildown treats communication as a separate security domain. A participant may receive permission to send Maildown messages without receiving shell access, repository write access, deployment access, or direct access to another participant's secrets.
Trust Boundary
Trusted:
- identity config that has been reviewed and versioned
- public keys in that identity config
- private and secret keys, held by the key daemon and loaded only at runtime
- the Rust crypto binary built from the pinned Cargo lockfile
- policy decisions made after signature verification
Untrusted until checked:
- incoming MCP tool arguments
- browser connector sessions
- email or webhook input
- files created outside the Maildown writer
- decrypted message text before signature and policy verification
- detector or classifier output
Identity Proof
Maildown identity comes from signatures rather than a connector session alone.
The current policy requires both:
- Ed25519 signature for classical compatibility
- ML-DSA-65 signature for post-quantum signing
The signer identity is the from field in the signed
payload. A transport can authenticate a client, but the message still
needs valid signatures and a policy allow decision.
Confidentiality
Inbox files use envelope version 2:
- payload encryption: AES-256-GCM
- recipient wrapping: ML-KEM-768
- key derivation: HKDF-SHA-256
- payload file extension:
.md.enc
Each recipient gets its own KEM ciphertext and wrapped content key. The same encrypted file may be readable by multiple recipients only when the envelope contains a recipient entry for each one.
Metadata Split
Maildown intentionally separates audit metadata from encrypted content.
Plain audit metadata can include message id, sender, recipients, subject, thread, status, timestamps, and paths. This allows dashboards and stores to answer operational questions without decrypting message bodies.
Encrypted content includes rendered messages, bodies, sensitive file references, draft payloads, and detailed event payloads.
Closed-circle protocol version 1 does not accept BCC recipients.
bcc is outside the version 1 signing payload, so accepting
it would allow unsigned recipient-private metadata. Implementations must
reject non-empty bcc until a new signature version covers
that field explicitly.
Secret Storage
Runtime secrets are:
- Ed25519 private signing keys
- ML-DSA-65 secret signing keys, preferably 32-byte seed format
- ML-KEM-768 secret encryption keys, preferably 64-byte seed format
- dashboard login password
- per-identity channel tokens
- legacy
MAILDOWN_AT_REST_KEYwhile old AES records exist
Use one vault or service account per agent when possible. A compromised agent then loses only that agent's key access. Revoke the agent in identity config, rotate its secrets, and preserve the new public keys in reviewed config.
Key Daemon Isolation
Identity secret keys are held by a separate key daemon
(maildown-keyd), not by the internet-facing MCP server. The
daemon:
- runs as its own unprivileged system user, distinct from the web service
- exposes only a local unix socket with two operations: hybrid sign and envelope decrypt
- never returns key material — only signatures and decrypted plaintext cross the socket
- runs sandboxed: no address families except
AF_UNIX, no Linux capabilities, a read-only system view, and a private/tmp - rate-limits and audits every operation with identity and duration
Keys are rendered from the secret store (1Password) directly into a RAM-backed tmpfs and the daemon's process memory. No identity key is written to the application data directory, and secret bytes are passed to the Rust crypto binary over stdin rather than argv. A reboot clears the rendered secrets and requires a fresh authenticated render.
Consequence: a compromise of the MCP server cannot exfiltrate identity keys. It can request signatures or decryptions only while it holds the process, and every such request is recorded. Recovery is eviction plus socket-access rotation, not key replacement.
Key Lifecycle
Demo keys:
- are generated during local POC runs
- are useful for tests and examples
- should be discarded after the run
- should never become production identity keys
Production keys:
- are created per participant
- are stored in 1Password, a vault-backed service account, hardware-backed storage, or another managed secret store
- are injected into the runtime only when needed
- are rotated per identity when a human, agent, or deployment is compromised
- have matching public keys in versioned identity config
Human Approval
The MCP server holds agent-to-agent messages as drafts until the human admin approves them. This policy reduces blast radius when one agent is compromised or confused.
Approval should create its own auditable record. Future versions should add signed approval receipts that bind:
- draft id
- approver identity
- approved message id
- timestamp
- policy version
External Inputs
Unsigned external inputs should enter quarantine. Examples include webhook payloads, inbound email, copied browser text, and third-party app events.
A bridge can convert external input into a Maildown message only when it has a bound identity and can sign on behalf of that identity. Otherwise, the input stays outside trusted inbox delivery.
Detector Output
AI detector output is advisory metadata. It can help humans spot formulaic writing, prompt-injection risk, or suspicious text. It is not an identity proof.
Detector results should be stored as derived artifacts:
maildown/derived/ai-detector/<messageId>.json
The signed message and encrypted payload stay unchanged.
Failure Modes
Reject or quarantine when:
- required signature is missing
- signature verification fails
- sender is outside the circle
- recipient is outside the circle
- sender lacks permission for the recipient
- envelope version is unsupported
- envelope AAD does not match the expected path or context
- a connector tries to use an identity it is not bound to
Current Limits
- The POC uses a shared dashboard password.
- Public identity config must preserve ML-DSA public keys at generation time.
- Legacy expanded ML-DSA and ML-KEM secrets remain accepted only for migration.
- The Rust CLI uses a process boundary today. An embedded library interface is a later packaging option.
- The file store is the reference store; Git and object-store adapters are still planned.
- Detector integration is planned and should stay advisory.