# Install with Helm

The vault installs in two stages. Stage 1 brings up the Key Store and Policy Server on every cluster.
Stage 2 activates the Orchestrator, message bus, relay, and web app on the hub. The Orchestrator
propagates cluster identity across all nodes during Stage 2 startup.

If you used the [example Terraform](/deploy/provision-infra), `terraform apply` already wrote a complete,
ready-to-use values file and kubeconfig per cluster to `./out/` - you do not hand-author values. If you
brought your own infrastructure, author one `values.yaml` per cluster from the
[Values reference](/deploy/helm/values-reference) instead, and substitute your own kubeconfig contexts
in the commands below.

## Before you start

* `terraform apply` finished and `./out/` contains `cluster-{0,1,2}.values.yaml` and
  `cluster-{0,1,2}.kubeconfig`. The shard mapping is **`cluster-0` = spoke-0, `cluster-1` = hub,
  `cluster-2` = spoke-1**.
* Your backup public key is set under `mpcKeyStores[].backupEncryptorPublicKey` (Terraform wires this
  from `mpc_key_stores`; key creation and import are blocked until it is present).
* Your Setup Admin emails are set under `googleAuth.adminEmails` (Terraform wires this from
  `google_admin_emails`).
* If `chart_ha = false`, also append `-f out/extra.values.yaml` to **every** command below.

## Log in to the chart registry

```bash
helm registry login repo.sodot.dev -u <your-username>
```

The install commands reference the chart straight from the OCI registry. Browse the available versions
at [`sodot-helm-charts/crypto-vault`](https://repo.sodot.dev/ui/repos/tree/General/sodot-helm-charts/crypto-vault)
and pick the **latest**. If you used the example Terraform, use the exact chart ref and version it
pinned - `terraform output helm_handoff` echoes it under `chart`. Set it once (replace `X.Y.Z` with the
version you picked):

```bash
CHART="oci://repo.sodot.dev/sodot-helm-charts/crypto-vault --version X.Y.Z"
```

Image tags for every service are pinned by the chart version - do not override them.

## Stage 1: Key Store and Policy Server on all three clusters

Install with the release name **`crypto-vault`** (Terraform pre-wired the cross-cluster
TargetGroupBindings and the database bootstrap to that name). The spoke values already enable only Key
Store + Policy Server; the hub values describe its full final state, so Stage 1 switches the hub-only
services off at install time:

```bash
# Spokes - their values already enable only KS + POL.
helm install crypto-vault $CHART -n crypto-vault \
  --kubeconfig out/cluster-0.kubeconfig -f out/cluster-0.values.yaml
helm install crypto-vault $CHART -n crypto-vault \
  --kubeconfig out/cluster-2.kubeconfig -f out/cluster-2.values.yaml

# Hub - install KS + POL only for now (Stage 2 turns the rest on).
helm install crypto-vault $CHART -n crypto-vault \
  --kubeconfig out/cluster-1.kubeconfig -f out/cluster-1.values.yaml \
  --set services.orchestrator.enabled=false --set services.nats.enabled=false \
  --set services.relay.enabled=false --set services.vaultUi.enabled=false
```

### Verify Stage 1 health

Run on each cluster and wait for the Key Store and Policy Server pods to reach `Running` with all
containers Ready:

```bash
kubectl --kubeconfig out/cluster-0.kubeconfig -n crypto-vault get pods
kubectl --kubeconfig out/cluster-1.kubeconfig -n crypto-vault get pods
kubectl --kubeconfig out/cluster-2.kubeconfig -n crypto-vault get pods
```

:::note
Key Store pods run inside a Nitro enclave and take several minutes to reach Ready on first start. They
also dial the hub's NATS over PrivateLink, so they keep retrying until Stage 2 brings NATS up - that is
expected, not a failure.
:::

At this stage each node has generated its own identity keypair and logged its `own_pubkey` to stdout.
No cross-cluster trust exists yet; the Orchestrator establishes that in Stage 2.

## Stage 2: Activate Orchestrator, NATS, relay, and UI on the hub

Re-run the hub install as an upgrade, dropping the Stage-1 `--set` flags so the hub-only services come
up:

```bash
helm upgrade crypto-vault $CHART -n crypto-vault \
  --kubeconfig out/cluster-1.kubeconfig -f out/cluster-1.values.yaml
```

When the Orchestrator pod becomes Ready it connects to each Key Store and Policy Server instance,
collects their identity public keys, and pushes the full ordered six-pubkey set back to every node. Each
node then logs three values:

* `own_pubkey`: the node's own identity public key
* `cluster_pubkeys`: the ordered list of all six identity public keys
* `cluster_pubkeys_fingerprint`: a hash of the ordered six-pubkey set

:::note\[No post-install patching]
The hub's values file already contains the spokes' Key Store / Policy Server PrivateLink endpoint DNS
(`remoteMpcKeyStores[]` / `remotePolicyServers[]`) and the NATS/relay NLB addresses, because Terraform
created those NLBs before Helm. So the hub converges on its first attempt - there is no post-install
patch step, and the partial-set startup conflict that older flows hit cannot happen here.
:::

### Verify Stage 2 health

```bash
kubectl --kubeconfig out/cluster-1.kubeconfig -n crypto-vault get pods
# All pods (orchestrator, relay, nats, vault-ui, key-store, policy-server) should be Running/Ready

curl -sS https://<orchestrator-host>/health
```

`GET /health` on the orchestrator returns `200` once it is up and connected to the Key Store and Policy
Server instances. The vault still **rejects signing and policy operations** until the
[setup ceremony](/deploy/setup-ceremony) completes — that gate is enforced internally, not surfaced as a
health status code.

## Next step

Proceed to the [Secure setup ceremony](/deploy/setup-ceremony) to verify cluster trust and enroll Setup
Admin passkeys.
