# Security model

How the vault keeps key material secret from everyone, including its own operators.

## Zero key exposure

Keys are never revealed to any system component, any operator, or any application. This holds across every key type the vault manages.

| Key type | How it stays secret |
|---|---|
| HMAC secrets (exchange API keys) | You paste the raw secret in your browser. The browser shards it using the Sodot HMAC key-sharing SDK before any network call. Encrypted shares go directly to each Key Store node. The secret never passes through the Orchestrator. |
| System-generated keys (Ed25519, RSA 4096) | Generated by a distributed key generation (DKG) protocol across the three Key Store nodes. Full key material never exists in one place, not during generation and not afterward. |

## The trust boundary

The vault does not rely on trusting any single component. The trust boundary is built from two independent checks:

**2-of-3 Policy Server consensus.** Each of the three Policy Server nodes independently evaluates your policies and signs its verdict. The Orchestrator cannot fake a policy approval: it can only collect what the nodes produce.

**Independent verification at each Key Store.** Before participating in any MPC signing, each Key Store node checks that 2-of-3 Policy Server signatures are present and valid. A compromised Orchestrator, or even a compromised single Policy Server, cannot drive a Key Store to sign.

Together, this means: no single compromise anywhere in the system can produce an unauthorized signature.

![Trust boundary: Orchestrator as untrusted router, 2-of-3 Policy Server consensus, and independent Key Store verification](/img/diagrams/trust-boundary.svg)

```mmd
flowchart LR
    subgraph Untrusted["Untrusted Zone"]
        ORC["Orchestrator\n(routes requests;\ntreated as adversarial)"]
    end

    subgraph TrustBoundary["Trust Boundary"]
        subgraph Consensus["2-of-3 Policy Server Consensus"]
            POL_A["Policy Server A\n(signs verdict)"]
            POL_B["Policy Server B\n(signs verdict)"]
            POL_C["Policy Server C\n(signs verdict)"]
        end

        subgraph KeyVerification["Independent Key Store Verification"]
            KEY_A["Key Store A\n(verifies 2-of-3 pol sigs)"]
            KEY_B["Key Store B\n(verifies 2-of-3 pol sigs)"]
            KEY_C["Key Store C\n(verifies 2-of-3 pol sigs)"]
        end
    end

    ORC -->|"fan out: evaluate policy"| POL_A
    ORC -->|"fan out: evaluate policy"| POL_B
    ORC -->|"fan out: evaluate policy"| POL_C

    POL_A -->|"signed verdict"| ORC
    POL_B -->|"signed verdict"| ORC
    POL_C -->|"signed verdict"| ORC

    ORC -->|"2-of-3 pol signatures"| KEY_A
    ORC -->|"2-of-3 pol signatures"| KEY_B
    ORC -->|"2-of-3 pol signatures"| KEY_C

    KEY_A -->|"MPC threshold sign"| KEY_B
    KEY_B -->|"MPC threshold sign"| KEY_C

    KEY_C -->|"signature"| ORC
```

*The Orchestrator sits outside the trust boundary. Policy Servers and Key Stores verify independently; neither trusts the Orchestrator.*

## Request authentication

**Human users** authenticate in two layers. First, an SSO session (Google) establishes identity. Second, every state-changing operation requires a WebAuthn passkey assertion. Passkeys are hardware-backed and phishing-resistant by design. The vault enforces that every human user maintains at least one enrolled passkey at all times.

**Machine Users and Agents** sign every mutating request with an Ed25519 private key. The signature covers a canonical transcript:

* HTTP method, URI path, and query string
* Request body
* Authorization header value
* Timestamp and nonce

The Key Store and Policy Servers verify this signature. A request replayed after the 5-minute timestamp window is rejected. A request with a reused nonce is rejected.

Read-only endpoints (such as `GET /v1/whoami` and `GET /v1/audit-logs`) require only the `Authorization` header and no signed assertion.

See the [Developer Guide](/develop/overview) for the full request-signing algorithm.

## Replay protection

Every mutating API request includes two anti-replay fields:

* `X-Request-Timestamp`: a timestamp in milliseconds, must be within 5 minutes of server time.
* `X-Request-Nonce`: 8 random bytes, single-use.

Both fields are included in the Ed25519 transcript so they cannot be stripped or reused.

## What each layer defends against

| Threat | Mitigation |
|---|---|
| Compromised Orchestrator | Policy Servers and Key Stores verify independently. A rogue Orchestrator cannot forge policy approvals or drive signing. |
| Compromised single MPC node | 2-of-3 threshold: one key share is cryptographically useless. |
| Compromised single cloud provider | Nodes are deployable across different cloud providers, eliminating single-vendor risk. |
| Replay attack | 5-minute timestamp window plus single-use nonce. |
| Phishing | WebAuthn passkeys are bound to the origin and hardware-backed. |
| Insider threat | Zero key exposure: keys are never accessible to developers or infrastructure operators. |

## Backup and recovery

An authorized admin can export an encrypted recovery kit from the web app (or via `GET /v1/keys-backup`). The kit contains all key shares, encrypted with an RSA-OAEP-SHA-256 + ChaCha20-Poly1305 hybrid scheme. Each Key Store node uses a distinct backup key; a single shared backup key is prohibited.

Backups are designed for immutable WORM object storage (AWS S3 Object Lock or GCP Bucket Lock). Re-importing a backup is an infrastructure-level operation using the corresponding offline private key.

New key creation is blocked unless a backup public key is configured at deployment, ensuring you always have a recovery path before you accumulate keys.

See the [Backup and recovery](/guide/backup/export-recovery-kit) guide for the full procedure.

## Audit trail

Every operation produces a signed, append-only audit log entry. The log records who acted, on which resource, with what outcome. It is queryable from the web app and exportable as CSV or JSON for SIEM ingestion.
