← Back to Maildown Security

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_KEY while 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.