# 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. Both stages use the same
Terraform-rendered values files; Stage 1 changes only the four temporary hub service toggles.

If you used the [AWS](/deploy/provision-aws) or [GCP](/deploy/provision-gcp) example Terraform,
`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"
```

The chart pins a tested image set. Keep those defaults for normal released deployments. Per-service
overrides are available for image sets explicitly qualified with Sodot; if you use them, keep all
affected service tags on the qualified compatible set.

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

Install with the release name **`crypto-vault`**. The example Terraform pre-wires the cross-cluster
services and 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 startup

Run on each cluster and confirm the Key Store and Policy Server workloads are created, their init
containers complete, and the application containers start. On GCP, also confirm the Cloud SQL Auth
Proxy sidecars start:

```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 may take several minutes to reach Ready on first start. AWS must launch the Nitro
Enclave, while GCP must schedule the pod onto a Confidential node and initialize Cloud KMS access.
Do not wait for every Stage 1 pod to become Ready before continuing. Policy Server workloads can wait
for hub NATS while Stage 1 deliberately keeps NATS disabled. Stage 2 provides that dependency and lets
the full topology converge.
:::

As the workloads start, each node generates its own identity keypair and logs 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 and Policy Server private endpoints
(`remoteMpcKeyStores[]` / `remotePolicyServers[]`) and the NATS/relay addresses. AWS creates the
PrivateLink endpoints before Helm, and GCP creates the Private Service Connect endpoints before Helm.
The hub therefore converges without a post-install address patch.
:::

### 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.

On GCP, also wait for the GKE Ingress address and Google-managed certificate:

```bash
kubectl --kubeconfig out/cluster-1.kubeconfig -n crypto-vault \
  get ingress,managedcertificate
```

The certificate becomes active after the hostname resolves to the reserved ingress IP.

## Next step

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