# Architecture & topology

The vault runs across three Kubernetes clusters, each in a separate cloud sub-account. Sub-account separation is the foundation of the distributed-trust model: compromising one cloud account exposes at most one MPC key share and one Policy Server, which is below the signing threshold. No shard ever shares compute or identity credentials with another.

## Cluster layout

![Three EKS clusters across three AWS sub-accounts: two spoke clusters connect to the hub cluster via PrivateLink](/img/diagrams/cluster-layout.svg)

```mmd
flowchart TB
    subgraph A1["AWS Sub-account 1: Spoke (EKS)"]
        direction TB
        KS1["Key Store\n(Nitro Enclave)"]
        PS1["Policy Server"]
        DB1[("RDS\nmpc_ks_db · policy_db_1")]
    end

    subgraph A2["AWS Sub-account 2: Hub (EKS)"]
        direction TB
        ORC["Orchestrator\n(API entry point)"]
        UI["Vault UI"]
        RELAY["Relay"]
        NATS["NATS JetStream\n(3-node)"]
        KS2["Key Store\n(Nitro Enclave)"]
        PS2["Policy Server"]
        DB2[("RDS\nmpc_ks_db · policy_db_2 · orchestrator_db")]
    end

    subgraph A3["AWS Sub-account 3: Spoke (EKS)"]
        direction TB
        KS3["Key Store\n(Nitro Enclave)"]
        PS3["Policy Server"]
        DB3[("RDS\nmpc_ks_db · policy_db_3")]
    end

    A1 <-->|"PrivateLink"| A2
    A3 <-->|"PrivateLink"| A2
```

*Two spoke clusters (sub-accounts 1 and 3) each run a Key Store shard, Policy Server, and RDS instance. The hub cluster (sub-account 2) adds the Orchestrator, Vault UI, Relay, and NATS. Spokes connect to the hub over PrivateLink.*

One cluster is the **hub** (it hosts the Orchestrator, UI, NATS, and relay). The other two are **spokes**. All three run a Key Store shard and a Policy Server.

![Deployment topology: three EKS clusters across three AWS sub-accounts connected via PrivateLink](/img/diagrams/deployment-topology.svg)

```mmd
flowchart TB
    Users["Corporate Users\n(VPN / zero-trust)"]

    subgraph Account2["AWS Sub-account 2: Hub Cluster (EKS)"]
        direction TB
        ORC2["Orchestrator\n(API entry point)"]
        UI2["Vault UI\n(Next.js)"]
        NATS2["NATS JetStream\n(3-node cluster)"]
        RELAY["Relay\n(MPC round router)"]
        KS2["Key Store\n(Nitro Enclave)"]
        POL2["Policy Server"]
        RDS2[("RDS Postgres\nmpc_ks_db\npolicy_db_2\norchestrator_db")]
        KMS2["KMS Key"]
    end

    subgraph Account1["AWS Sub-account 1: Spoke Cluster (EKS)"]
        direction TB
        KS1["Key Store\n(Nitro Enclave)"]
        POL1["Policy Server"]
        RDS1[("RDS Postgres\nmpc_ks_db\npolicy_db_1")]
        KMS1["KMS Key"]
    end

    subgraph Account3["AWS Sub-account 3: Spoke Cluster (EKS)"]
        direction TB
        KS3["Key Store\n(Nitro Enclave)"]
        POL3["Policy Server"]
        RDS3[("RDS Postgres\nmpc_ks_db\npolicy_db_3")]
        KMS3["KMS Key"]
    end

    Users -->|"HTTPS :443"| ORC2
    Users -->|"HTTPS :443"| UI2

    ORC2 -->|":3001"| POL1
    ORC2 -->|":3001"| POL2
    ORC2 -->|":3001"| POL3
    ORC2 -->|":8080"| KS1
    ORC2 -->|":8080"| KS2
    ORC2 -->|":8080"| KS3

    ORC2 <-->|":4222"| NATS2
    ORC2 -->|":80 create MPC room"| RELAY

    KS1 <-->|"PrivateLink :80"| RELAY
    KS3 <-->|"PrivateLink :80"| RELAY
    KS2 <--> RELAY

    POL1 <-->|"PrivateLink :4222"| NATS2
    POL3 <-->|"PrivateLink :4222"| NATS2
    POL2 <--> NATS2

    KS1 --- RDS1
    POL1 --- RDS1
    KS1 --- KMS1
    KS2 --- RDS2
    POL2 --- RDS2
    ORC2 --- RDS2
    KS2 --- KMS2
    KS3 --- RDS3
    POL3 --- RDS3
    KS3 --- KMS3
```

*Each cluster runs in a separate AWS sub-account. Spoke clusters connect to the hub via PrivateLink for NATS and relay traffic.*

## Services

### Key Store (every cluster)

The Key Store holds the MPC key shares. It runs inside an AWS Nitro Enclave, meaning its runtime memory is cryptographically isolated from the host operating system, from AWS infrastructure, and from your own platform team. Key shares are encrypted at rest with an AWS KMS key provisioned in the same sub-account.

The Key Store pod is a four-container unit: an enclave runner, a vsock bridge, a resource server that feeds configuration and AWS credentials into the enclave, and a credential-fetcher that refreshes the IRSA token every six hours.

The Key Store has no external HTTP surface. Only the Orchestrator communicates with it over internal REST.

### Policy Server (every cluster)

The Policy Server holds the Setup Admin set, their passkey public keys, user groups, and Signing Policy rules. Three instances run in lockstep across the three clusters; writes replicate atomically. Policy evaluation happens on reads (stateless fan-out from the Orchestrator).

The Policy Server has no external HTTP surface.

### Orchestrator (hub only)

The Orchestrator is the single externally reachable service. It accepts every API and UI request, fans out to all three Key Store and three Policy Server instances, and coordinates the MPC ceremony round-trip. It runs only on the hub cluster and is accessible only over your internal network (LAN, VPN, or zero-trust gateway). No internet-facing ingress is required.

### NATS (hub only)

A three-node JetStream cluster carries policy-server coordination and approval/completion events between the Orchestrator and the Policy Servers. The Orchestrator and Policy Server pods connect to the hub's NATS endpoint; Key Stores do not use NATS.

### Relay (hub only)

The relay routes the MPC round messages exchanged between Key Store nodes during a signing or key-generation ceremony. The Orchestrator creates the ceremony room on the relay; each Key Store joins it to run the protocol. Spoke Key Store pods connect to the relay over PrivateLink.

### Vault UI (hub only)

The Next.js web application. Reachable alongside the Orchestrator on the hub's internal ingress, it uses Google OAuth and WebAuthn passkeys for authentication.

## Data stores

| Store | Location | Created by |
|---|---|---|
| Key Store database (`mpc_ks_db`) | RDS, one instance per sub-account | Terraform (outside the Helm chart) |
| Policy Server database (`policy_db_N`) | RDS, one instance per sub-account | Terraform |
| Orchestrator database (`orchestrator_db`) | RDS, hub sub-account only | Terraform |
| KMS encryption key | One symmetric key per sub-account | Terraform |

Postgres is not part of the Helm chart. The chart accepts connection strings as values.

## Required network paths

| From | To | Port |
|---|---|---|
| Corporate users | Hub cluster ingress (Orchestrator + UI) | 443 (HTTPS, internal network only) |
| Orchestrator (hub) | Policy Server on all three clusters | 3001 |
| Orchestrator (hub) | Key Store on all three clusters | 8080 |
| Orchestrator (hub) | NATS and relay on hub | 4222 / 80 |
| Policy Server (all clusters) | NATS on hub | 4222 (PrivateLink from spokes) |
| Key Store (all clusters) | Relay on hub | 80 (PrivateLink from spokes) |
| All pods | Local sub-account RDS | 5432 |

The example Terraform wires cross-cluster connectivity with AWS PrivateLink (hub exposes NATS and relay via internal NLBs; spokes expose their Key Store and Policy Server). VPC peering is an alternative for simpler setups with non-overlapping CIDRs.

## Why Nitro only for the Key Store

Key Store pods hold MPC key shares, the highest-value target in the system. Nitro enclaves bound and attest the runtime memory, ensuring no process outside the enclave can observe key material. Policy Server, Orchestrator, NATS, and UI hold no key material; running them inside enclaves adds complexity without changing the threat model.
