# Helm values reference

All configurable values for the `crypto-vault` Helm chart, grouped by concern. Values marked **required** have no safe default and the chart will not produce a working installation without them.

:::note
Image tags for all services are pinned by the chart version. Do not override them. Per-service image tag overrides are not supported.
:::

:::note\[Using the example Terraform?]
`terraform apply` writes a complete values file per cluster to `./out/cluster-{0,1,2}.values.yaml`, with everything below already filled in (database, KMS, IRSA, shard identities, cross-cluster addresses). This page is for understanding those values, customizing them, or authoring your own when you [bring your own infrastructure](/deploy/provision-infra).
:::

## Service toggles

The chart enables all six components by default. The two-stage install controls them per cluster: the example Terraform's spoke values keep only Key Store + Policy Server (the four hub-only services are set to `false` there), and the hub comes up in two stages - Stage 1 installs it with the hub-only services turned off (`--set ...enabled=false`), then Stage 2 leaves them at their default. See [Install with Helm](/deploy/helm/install).

| Value | Type | Default | Description |
|---|---|---|---|
| `cloudProvider` | string | `aws` | Cloud provider. Only `aws` is fully supported. |
| `services.keyStore.enabled` | bool | `true` | Enable the Key Store on this cluster. |
| `services.policyServer.enabled` | bool | `true` | Enable the Policy Server on this cluster. |
| `services.orchestrator.enabled` | bool | `true` | Enable the Orchestrator. Hub only - set `false` on spokes and in Stage 1. |
| `services.relay.enabled` | bool | `true` | Enable the relay. Hub only - set `false` on spokes and in Stage 1. |
| `services.nats.enabled` | bool | `true` | Enable NATS. Hub only - set `false` on spokes and in Stage 1. |
| `services.vaultUi.enabled` | bool | `true` | Enable the web UI. Hub only - set `false` on spokes and in Stage 1. |

## MPC shard identity (required, per shard, sensitive)

Each cluster's shard is configured here, from its database connection to its key-handling material. The sensitive values (such as the imported-key encryptor private key) must never be committed to version control; supply them from a secrets manager or encrypted values file. The shard's own identity keypair is generated on first boot, not supplied here.

| Value | Description |
|---|---|
| `mpcKeyStores[].mpcShardIndex` | **Required, unique per cluster.** 0-based shard index (`0`/`1`/`2`). Sets the Key Store's self-name `mpc-ks-<index>`, which the Orchestrator matches to build its cluster-identity set. If omitted it falls back to the entry's position in the list - so with one shard per cluster every cluster collapses to `mpc-ks-0`, the shards collide, and the mesh never forms. |
| `mpcKeyStores[].name` | Kubernetes resource/label name only, e.g. `mpc-key-store-0`. Does **not** set the shard's MPC identity - use `mpcShardIndex` for that. |
| `mpcKeyStores[].backupEncryptorPublicKey` | X25519 HPKE public key (raw 32-byte, lowercase-hex, 64 chars) wired into the shard's backup encryptor. Key creation and import are blocked until this is set. See [generating the backup key pair](/deploy/operations/backup-dr#generating-the-backup-key-pair). |
| `mpcKeyStores[].importedKeyEncryptorPem` | RSA private key for unwrapping imported keys. |
| `mpcKeyStores[].importedKeyEncryptorPemSecret.existingSecret` / `.key` | Optional GitOps alternative to the inline `importedKeyEncryptorPem`: name of an existing Secret holding the RSA key (data key defaults to `imported-key-pem`). Substituted into the Key Store config at pod startup so the PEM never lands in Helm values. |
| `mpcKeyStores[].dbHost` / `.dbPort` / `.dbName` | Connection details for this shard's Key Store database. |
| `remoteMpcKeyStores[].url` | Hub only. Endpoint URL of a remote shard's Key Store, reachable over PrivateLink (no Deployment rendered; the Orchestrator dials this URL). |
| `remoteMpcKeyStores[].mpcShardIndex` | Hub only. The remote shard's own 0-based index. The Orchestrator names its gateway entry `mpc-ks-<index>` to match that shard's Key Store self-name. Set it whenever a remote shard's index differs from its position in this list (e.g. the default hub=shard 1 topology, whose remotes are shards 0 and 2); if omitted it falls back to list position and mis-names the higher shard. |

## Policy Server identity (required, per shard)

| Value | Description |
|---|---|
| `policyServers[].name` | **Required, unique per cluster** on a multi-cluster deployment. The shard's MPC identity - the reader's `cluster_identity_name` and the Orchestrator's `policy_gw` reader entry (approvals are verified by this name). Keep the literal `reader` substring, e.g. `policy-server-reader-0`. Distinct from `writerName`/`readerName`; if omitted, every single-shard cluster falls back to `policy-server-reader-0` and the cross-cluster approval mesh breaks. |
| `policyServers[].writerName` / `.readerName` | Kubernetes resource names for the writer and reader halves. |
| `policyServers[].db.host` / `.port` / `.name` | Connection details for this shard's Policy Server database. |
| `remotePolicyServers[]` | Hub only. Reader/writer URL entries for the remote shard Policy Servers (the Orchestrator dials these over PrivateLink). |
| `policyServer.approvalThreshold` | The number of human Setup Admins who must approve a change to the initial Administrative Policy (the admin enrollment quorum). This is a human-approval count, not the Policy Server (MPC) signature threshold. Only a value of `1` is currently supported. |
| `policyServers[].externalRuleServerSigningKey` | **Required at install** - inline here, or via `externalRuleServerSigningKeySecret.existingSecret` below. The render fails when both are empty and the Policy Server refuses to boot without it, **even when no ERS is configured**. The private key the Policy Server uses to sign its requests to an [External Rule Server](/guide/signing-policy/external-rule-server); the matching public key is published at `GET /system-info`. Nothing is auto-generated. |
| `policyServers[].externalRuleServerSigningKeySecret.existingSecret` / `.key` | Source the ERS signing key from an existing Secret (PKCS#8 EC PEM) instead of inlining it above - the GitOps path. Data key defaults to `signing-key`; substituted into the config at pod startup so the key never lands in Helm values. |

## Bootstrap and Setup Admins

| Value | Type | Default | Description |
|---|---|---|---|
| `googleAuth.adminEmails` | list | (required) | Email addresses of the Google Setup Admins. These accounts are prompted to enroll a passkey on first sign-in. |
| `googleAuth.clientId` | string | (required) | Google OAuth client ID. Hub cluster only. |
| `ui.clientSecretRef` | string | `""` | Name of an existing Secret (release namespace) holding the UI's Google OAuth **client secret** under key `CLIENT_SECRET`, mounted into the UI pod - the secret half of the pair whose public half is `googleAuth.clientId`. Required for Google login when bringing your own infrastructure; the example Terraform wires it automatically. |
| `policyServer.initialAdmins` | list | (required) | Non-Google **human** Setup Admins seeded at bootstrap (Google admins are derived from `googleAuth.adminEmails` and appended automatically). Only `type: human` is accepted - the render fails on any other type. Machine users and agents are **not** seeded here; they are created at runtime via the API. Each entry requires `cloudProvider`, `id` (UUID), `name`, `publicKey`, `email` (or `emailEnv`), and `type: human`. |

## Database

| Value | Description |
|---|---|
| `postgresql.credentials.username` | Application PostgreSQL role name. |
| `postgresql.credentials.password` | Application role password. Supply from a secrets manager. |
| `postgresql.credentials.existingSecret` / `.usernameKey` / `.passwordKey` | GitOps / External Secrets Operator alternative: name of an existing Secret (release namespace) holding the DB username and password. When set, the inline `password` is ignored and never embedded in rendered Secrets; credentials are read via `secretKeyRef` at pod startup. `usernameKey` / `passwordKey` name the keys within that Secret (default `username` / `password`). |
| `postgresql.sslMode` | Set to `require` when RDS enforces TLS; leave empty otherwise. |
| `orchestrator.db.host` / `.port` / `.name` | Orchestrator database connection. Hub cluster only. |

## Storage encryptor (required for production)

| Value | Default | Description |
|---|---|---|
| `keyStore.storageEncryptor.type` | `aws_kms` | Encryptor type. Use `aws_kms` (the supported production type). |
| `keyStore.storageEncryptor.awsKms.keyId` | (required when `type=aws_kms`) | KMS key ARN. |
| `keyStore.storageEncryptor.awsKms.region` | (required when `type=aws_kms`) | AWS region of the KMS key. |

## AWS Nitro Enclave configuration

| Value | Default | Description |
|---|---|---|
| `aws.region` | (required) | AWS region for the credential-fetcher. |
| `aws.nitroEnclave.enclave.cpuCount` | `"2"` | vCPUs allocated to the enclave. |
| `aws.nitroEnclave.enclave.memoryMiB` | `"2048"` | Memory allocated to the enclave in MiB (must be a multiple of 1024). |
| `aws.nitroEnclave.enclave.debug` | `"false"` | Must remain `"false"` in production. Debug mode zeroes PCR attestation values. |
| `aws.nitroEnclave.hugepages.size` | `"1Gi"` | Must match the Nitro node allocator page size. |
| `aws.nitroEnclave.nodeSelector` | `aws-nitro-enclaves-k8s-dp: enabled` | Selects Nitro-capable nodes for Key Store workloads. |
| `mpcKeyStoreServiceAccount.aws.roleArn` | (required) | IRSA role ARN granting `kms:Decrypt` and `kms:GenerateDataKey`. |
| `aws.nitroEnclave.registrySecret.name` / `.userKey` / `.passwordKey` | `artifactory-credentials` / `username` / `password` | Secret with **plaintext** username/password keys that the Key Store runner and the fetch-EIF init container use to `oras login` and pull the enclave image / measured EIF. This is **distinct** from the `artifactory-dockerconfig` dockerconfigjson imagePullSecret the kubelet uses for the sidecar images - provision both. |

## Cross-cluster connectivity

| Value | Description |
|---|---|
| `nats.hub.url` | Spoke clusters: NATS endpoint URL on the hub, e.g. `nats://nats.hub.<private-hostname>:4222`. |
| `nats.internalLoadBalancer.enabled` | Hub: expose NATS via an internal NLB for PrivateLink. |
| `nats.internalLoadBalancer.annotations` | Cloud-specific Service annotations that make the NATS LB internal-only and the correct cross-account primitive. **Required when `enabled: true`** - without them AWS provisions an internet-facing LB that exposes NATS:4222 publicly. AWS: `aws-load-balancer-scheme: internal` + `aws-load-balancer-nlb-target-type: ip` + `aws-load-balancer-type: external`; GKE/Azure equivalents. |
| `nats.streamReplication.credentialsSecret` | Name of a Secret holding a NATS `.creds` file for a JetStream-admin account (mounted at `/creds`; data key from `nats.streamReplication.credentialsKey`, default `nats.creds`). Set this when NATS operator-JWT auth is enabled so the post-install stream-replication Job can authenticate; empty uses the default unauthenticated connection. |
| `nats.config.merge` | Strategic-merge overlay onto the NATS subchart config. Selects alternate cross-cluster/security topologies: leaf-node (`leafnodes.remotes`) and operator-JWT auth + TLS (`operator`/`resolver`). See `bootstrap/nats-leaf-topology.example.yaml` and `nats-jwt-auth.example.yaml`. Not shown in the chart's generated values (it's a subchart passthrough). |
| `relay.address` | Client-side relay endpoint. Hub: `http://relay-router:80`. Spokes: PrivateLink endpoint URL. |
| `relay.apiKey` | Relay API key (client side). |
| `relay.relay.apiKey` | Server-side relay key on the hub, passed through to the relay subchart's `RELAY_API_KEYS`. Must be the bare `relay.apiKey` client key plus the `,AdminKey` role suffix, e.g. `<key>,AdminKey`. **Required** when the hub deploys the relay subchart (Stage 2) - the render fails without it. |
| `relay.apiKeySecret.existingSecret` / `.key` | Source the client relay API key from an existing Secret instead of inline `relay.apiKey` (GitOps). Data key defaults to `relay-api-key`; substituted at pod startup so nothing secret lands in the rendered config. |
| `orchestrator.internalLoadBalancer.enabled` / `.annotations` | Hub. Expose the Orchestrator API on a cloud-agnostic internal load-balancer Service instead of, or alongside, ingress - the chart-native "expose the Services yourself" path when the Terraform front-door NLB is disabled. `.annotations` carry the cloud-specific internal-LB knobs (as for `nats.internalLoadBalancer.annotations`). Off by default. |
| `policyServers[].internalLoadBalancer.enabled` / `.annotations` / `.reader.annotations` / `.writer.annotations` | Spoke. Expose this shard's Policy Server reader+writer via internal LBs for the hub Orchestrator to dial over PrivateLink; per-role annotations merge over the shared base. Off by default (the example Terraform uses TF-managed NLBs instead). |
| `networkPolicies.enabled` | Render Kubernetes NetworkPolicies (deny-all baseline plus Pod-selector and `extraIngressCidrs` ingress). Default `true`; set `false` only for single-cluster Pod-only topologies or a CNI that does not enforce NetworkPolicies. |
| `networkPolicies.extraIngressCidrs` | Additional CIDRs admitted by NetworkPolicies. Must include your VPC CIDR to allow cross-cluster and load-balancer traffic. |

## Ingress and TLS (hub cluster only)

| Value | Description |
|---|---|
| `orchestrator.ingress.enabled` / `.host` / `.className` | Orchestrator Ingress resource. Set `className` to `alb` or `nginx`. |
| `orchestrator.ingress.tls` | TLS configuration for the Orchestrator Ingress. |
| `orchestrator.ingress.annotations` | Annotations for the Orchestrator Ingress (e.g. `cert-manager.io/cluster-issuer`, NGINX/ALB controller config). Controller/cert annotations are effectively required on the `alb`/`nginx` path. |
| `ui.ingress.enabled` / `.host` / `.className` | UI Ingress resource. |
| `ui.ingress.tls` | TLS configuration for the UI Ingress. |
| `ui.ingress.annotations` | Annotations for the UI Ingress (e.g. `cert-manager.io/cluster-issuer`, controller config). |
| `ui.loadBalancer.enabled` / `.host` / `.certificateArn` / `.scheme` | Alternative to `ui.ingress`: expose the UI via a chart-managed AWS NLB (Service type LoadBalancer) serving HTTPS:443. `.host` becomes the UI public origin driving `REDIRECT_URI` and the WebAuthn origin (must match the cert); `.certificateArn` is the ACM cert ARN and is **required when enabled** (the render fails otherwise); `.scheme` is `internal` (default) or `internet-facing`. Off by default; the example Terraform provisions its own front-door NLB and leaves this off. |

## Replicas and high availability

| Value | Default | Description |
|---|---|---|
| `keyStore.replicas` | `2` | Nitro enclave replicas for the Key Store (one unified enclave image per shard). Each replica requires a dedicated Nitro-capable node. |
| `orchestrator.replicas` | `2` | Active-active Orchestrator replicas. |
| `policyServer.replicas` | `2` | Policy Server reader and writer replicas. |
| `ui.replicas` | `2` | UI replicas. |
| `autoscaling.enabled` | `false` | Enable HPA for stateless services (Orchestrator, Policy Server, UI). Requires the metrics-server addon. |
| `autoscaling.minReplicas` / `.maxReplicas` | `2` / `6` | HPA replica bounds. |
| `podDisruptionBudgets.enabled` | `true` | PDB with `maxUnavailable: 1` for all roles. |
| `orchestrator.maxConnections` | `500` | Orchestrator request-concurrency cap - bounds concurrent in-flight signing ceremonies. Above it, requests get 503s / throughput plateaus. A compute/throughput lever alongside replicas and HPA (it does not size the Postgres pool). |
| `nats.streamReplication.enabled` | `true` | Post-install/upgrade Job on the hub that raises the JetStream stream replica factor to R3 for HA (the app creates streams at R1). No-op on spokes and single-node dev. |

## Node placement and scheduling

By default the stateless services carry a soft anti-affinity that spreads their replicas across availability zones and nodes, with no node-group constraint. To steer a service onto a dedicated node group (for example, nodes labeled `purpose=cv-general`), set any of the values below. `nodeAffinity` expresses a preferred or required node match and merges with the built-in anti-affinity. `nodeSelector` is a hard label match. `tolerations` let the pods schedule onto tainted nodes. All three default to no constraint, so existing installs are unaffected.

The Key Store is not listed here: it is pinned to Nitro-capable nodes by `aws.nitroEnclave.nodeSelector` (see above). The relay service exposes its own `nodeAffinity` and `tolerations` values (under `relay` and `relayRouter`), and NATS is scheduled through `nats.podTemplate` (topology spread and a merge patch).

| Value | Default | Description |
|---|---|---|
| `orchestrator.nodeAffinity` | `{}` | Node affinity for the Orchestrator pods, merged with the built-in anti-affinity. |
| `orchestrator.nodeSelector` | `{}` | Node label selector (hard constraint) for the Orchestrator pods. |
| `orchestrator.tolerations` | `[]` | Tolerations for the Orchestrator pods. |
| `policyServer.nodeAffinity` | `{}` | Node affinity for the Policy Server pods. Applies to both the reader and writer, on every shard. |
| `policyServer.nodeSelector` | `{}` | Node label selector (hard constraint) for the Policy Server pods. |
| `policyServer.tolerations` | `[]` | Tolerations for the Policy Server pods. |
| `ui.nodeAffinity` | `{}` | Node affinity for the UI pods. |
| `ui.nodeSelector` | `{}` | Node label selector (hard constraint) for the UI pods. |
| `ui.tolerations` | `[]` | Tolerations for the UI pods. |

## Web UI feature flags

The UI ships with every area enabled. Set a flag to `"false"` to hide that area from the web UI (rendered as `NEXT_PUBLIC_FEATURE_*` env in the UI ConfigMap). Values are strings.

| Value | Default | Description |
|---|---|---|
| `ui.nextPublicFeatureAdminPolicy` / `RiskPolicy` / `AddressBook` / `UserManagement` / `Keys` / `Settings` / `AuditLogs` / `Approval` / `Vaults` | `"true"` | Per-area UI feature toggles. Set `"false"` to hide the corresponding area. |
| `ui.nextPublicIsDemo` | `"false"` | Enables the UI's demo mode. Keep `"false"` for production. |

## Observability

OpenTelemetry (OTLP) export for the non-enclave services (Orchestrator and Policy Server reader/writer). Export is **off until `observability.otlp.endpoint` is set** - until then these services only log to stdout. This does not cover the Key Store, whose in-enclave telemetry ships through the separate `aws.nitroEnclave.otlpExporter` sidecar.

| Value | Default | Description |
|---|---|---|
| `observability.otlp.endpoint` | `""` | OTLP collector endpoint (must start with `http://` or `https://`). Setting it enables export. With `protocol: grpc` the URL is used verbatim (e.g. `http://collector:4317`); with `http/protobuf` the SDK appends `/v1/logs`. An `https://` scheme negotiates TLS. |
| `observability.otlp.protocol` | `grpc` | Transport: `grpc` or `http/protobuf`. |
| `observability.otlp.headersSecret.name` / `.key` | `""` | Existing Secret supplying collector auth headers (e.g. a bearer token), taken verbatim as a comma-separated `key=value` list. Requires an `https://` endpoint - the render fails otherwise. |
| `observability.otlp.logFilter` | `""` | Verbosity of the logs exported over OTLP (tracing `EnvFilter` syntax, e.g. `warn,policy_server=debug`), independent of `RUST_LOG` (which controls only stdout). Empty uses the default. |
| `observability.otlp.console` | `true` | Keep logging to stdout in addition to OTLP. Set `false` for OTLP-only. |
| `observability.otlp.resourceAttributes` | `""` | Extra OTel resource attributes, comma-separated `key=value` (e.g. `deployment.environment=prod`). |
| `observability.otlp.timeout` | `""` | Per-export timeout in milliseconds (quote it). Empty uses the SDK default. |
| `orchestrator.rustLog` / `keyStore.rustLog` / `policyServer.readerRustLog` / `policyServer.writerRustLog` | `"info"` | Console (stdout) log filter (`RUST_LOG`) per service, in tracing `EnvFilter` syntax. Independent of the OTLP sink filter (`observability.otlp.logFilter`); set `debug`/`trace` to troubleshoot. |
