# Institutional Vault Installation Guide

This guide describes how to install and configure the Institutional Vault in a cloud-agnostic way. It covers the installation flow, components and how they connect, broker configuration, secrets management, and the configuration options for the wallet service and policy nodes.

## Prerequisites
- A Kubernetes cluster (any supported distribution).
- Container images for: wallet service, policy-node (×3), NATS; optionally cold-wallet and Postgres if not using an existing database.
- Storage: PersistentVolumes (or equivalent) for Postgres and, if used, cold-wallet nodes.
- Secrets: An external secrets manager is required for production.
- Confidential Computing is required for Policy Nodes in production to protect MPC key share material while in-use.

## Deployment Components
1. **Namespace and RBAC** – Create the target namespace and any service accounts/roles used by the components.
2. **Secrets and ConfigMaps** – Create Kubernetes Secrets (or provision secrets via your chosen secret store) and ConfigMaps. Do not store secrets in ConfigMaps.
3. **Database (PostgreSQL)** – Deploy **PostgreSQL 14 or 15** and run init scripts to create databases: `wallet`, `orchestrator`, `approval`, `mpa0`, `mpa1`, `mpa2`. Ensure TLS is configured for production.
4. **Message broker (NATS)** – Deploy the broker and configure accounts/users for: policy-node0, policy-node1, policy-node2, facade. The wallet service connects as `facade`; each policy node has its own user (e.g. `node0`, `node1`, `node2`).
5. **Policy nodes** – Deploy the three policy nodes. They require broker and database connectivity.
6. **Wallet service** – Deploy the wallet after the broker and (if using the same cluster) policy nodes and database are ready. The wallet connects to the broker as `facade` and to the MPA routes (route0, route1, route2) via the broker. WalletConnect runs in-process and does not require a separate NATS connection.
7. **Cold wallet** – Optional. Requires Redis and persistent storage for two cold-wallet nodes and a proxy.
8. **Ingress / Gateway** – Expose the wallet API via your chosen gateway or ingress.

## Component Wiring

High-level architecture:

```
Data Layer

Wallet Namespace

Clients

HTTPS 443

HTTPS 443

HTTPS 443

HTTP 80

TCP 8080

TCP 5432

NATS TCP 4222

NATS TCP 4222

NATS TCP 4222

NATS TCP 4222

NATS TCP 4222

TCP 5432

TCP 5432

TCP 5432

Wallet UI
Client

Wallet API
Client

Approver

Reverse Proxy

Wallet Frontend

Wallet Service

EVM Tracker

NATS Message Broker

Policy Node 0

Policy Node 1

Policy Node 2

wallet,
approval,
orchestrator

mpa0

mpa1

mpa2
```

- **Wallet service** – API and business logic, including integrated WalletConnect support. Talks to the broker as `facade`, to Postgres (wallet, approval, orchestrator DBs), and to policy nodes only indirectly via the broker (route0, route1, route2). Ports: 8080 (API/health), 9876 (approval), 8085 (webhook), 9090 (metrics).
- **Policy nodes** – Three MPC participants. They connect to the **broker** and the **database** only. They do not call the wallet; the wallet sends intents over the broker and receives responses over the broker. When `HealthCheckPort` is set, expose **`GET /live`** (liveness; HTTP 200 while the process is running) and **`GET /ready`** (readiness; HTTP 200 only after NATS connect and subscribe, **503** before then). Optional **`GET /metrics`** on the same port.
- **NATS** – Message broker. Client port (e.g. 4222), monitoring (e.g. 8222), optional WebSocket (e.g. 8443). Users: node0, node1, node2, facade.
- **PostgreSQL** – **Version 14 or 15** recommended. The wallet uses multiple DBs (wallet, approval, orchestrator); policy nodes use a DB per tenant (e.g. `local`) or shared DB with schema separation.
- **Cold wallet** – Optional; uses Redis and persistent volumes; integrates with the wallet for cold-storage flows.
- **Gateway** – Optional; routes external traffic (e.g. Gateway API HTTPRoute or Ingress) to the wallet.

## Secrets Management
Wallet and policy-node services load local config files at startup; any secret values referenced in those files must be interpolated by fetching them from external secret managers at runtime (not stored in plaintext in config files or ConfigMaps). Both wallet and policy nodes support direct SDK-based secret retrieval (for example via AWS Secrets Manager or Azure Key Vault client integrations) so secrets are resolved from the manager instead of being hardcoded in deployment manifests.

### Secret interpolation syntax
The config templates in this stack use Go-template style interpolation (`{{ ... }}`).

| Interpolation type | Syntax | Behavior |
| --- | --- | --- |
| Environment variable (for development only) | `{{ env`VAR_NAME` }}` (example: `{{ env`BD_API_KEY` }}`) | Reads `VAR_NAME` from process environment; errors if missing. |
| AWS Secrets Manager lookup | `{{ awsSecret`secret-name` }}` or `{{ awsSecret`secret-name#FIELD` }}` | Reads secret by name; when `#FIELD` is provided, treats the secret value as JSON and returns that key. |
| Azure Key Vault lookup | `{{ azureSecret`secret-name` }}` or `{{ azureSecret`secret-name#FIELD` }}` | Same pattern as AWS; optional `#FIELD` extracts a JSON field. |
| Attested enclave secret lookup (MPA policy-node) | `{{ enclaveSecret`secret-name` }}` | Retrieves the secret and decrypts through enclave/attestation flow before injecting into config.

In this repository:
- The policy-node config expansion supports `env`, `awsSecret`, `azureSecret`, and `enclaveSecret`.
- The wallet service config expansion supports `env`, `azureSecret` and `awsSecret`.

### Security tiers (reference)
- **Tier 0 (dev/example)** – Kubernetes Secrets with local config interpolation (including env var expansion); values provided by overlay or manual creation. Not safe against cluster-admin access.
- **Tier 1 (production)** – Applications must fetch secrets directly from an external secret manager (for example AWS Secrets Manager, Azure Key Vault, or Vault) at runtime using provider SDKs or approved secret-client libraries. Do not rely on mounted Kubernetes Secrets for sensitive material, because cluster-admin users can observe Kubernetes Secrets once they are materialized in the cluster.
- **Tier 2 (high assurance)** – Secret release gated by attestation. Policy nodes may run in TEEs and pull secrets after attestation.

### Secret categories and usage
| Service | Secret Name | Purpose |
| --- | --- | --- |
| `wallet` | `WALLET_DB_PASSWORD` | Wallet database password for main wallet DB connection ( **optional** when `use_iam: true` with AWS IAM DB authentication). |
| `wallet` | `APPROVAL_DB_PASSWORD` | Approval service database password ( **optional** when `use_iam: true` with AWS IAM DB authentication). |
| `wallet` | `ORCHESTRATOR_DB_PASSWORD` | Orchestrator/MPA database password used by wallet adapter ( **optional** when `use_iam: true` with AWS IAM DB authentication). |
| `wallet` | `FACADE_BROKER_PASSWORD` | NATS password for wallet `facade` user. |
| `wallet` | `BD_API_KEY` | API key used by Blockdaemon service integrations (auth_token/utxos/ubiquity/staking APIs). |
| `wallet` | `WEBHOOK_TOKEN` | Webhook shared secret/token for callback verification. |
| `wallet` | `BOOTSTRAP_KEY` | Bootstrap key material used for local/bootstrap API auth flow. |
| `wallet` | `CHAINALYSIS_TOKEN` | Chainalysis credential ( **optional**, only if compliance integration is enabled). |
| `wallet` | `BOSS_API_KEY` | Staking service token ( **optional**, only if staking integration is enabled). |
| `wallet` | `WEBHOOK_SECRET` | Alternate webhook secret key name ( **optional**, only in deployments using this key instead of `WEBHOOK_TOKEN`). |
| `wallet` | `CANTON_DEVNET_CLIENT_SECRET` | Canton OIDC client secret ( **optional**, only if Canton integration is enabled). |
| `policy-node` | `DB_PASSWORD` | Policy-node Postgres password for default and tenant DB DSNs. |
| `policy-node` | `BROKER_PASSWORD` | NATS password for per-node broker users (`node0`/`node1`/`node2`). |
| `policy-node` | `ENCRYPTOR_MASTER_PASSWORD` | Master password used to derive encryption keys for sensitive policy-node data. |
| `policy-node` | `BROKER_INTERMEDIATE_CA` | CA bundle used to validate broker certificate ( **optional**, required when broker TLS validation is enabled). |

## Wallet Service Configuration
The wallet is configured via a single YAML file (e.g. mounted from a ConfigMap). Sensitive values must come from Secrets, not from this file.

The following YAML is an annotated example showing the available configuration keys and typical option values.

### `wallet.yaml`

```yaml
# CORS allow-origin. Typical: "*" for dev, or exact frontend origin in production.
access_control_allow_origin: "http://localhost:3000"

# Bootstrap key source.
# Supported `type` values in wallet config:
# - local (read key from file path)
# - secretsmanager (AWS Secrets Manager)
# - keyvault (Azure Key Vault)
bootstrap_key:
  type: "local"
  key: "/secrets/api-key"

# Compliance provider credentials. Keep secret values in external secret manager.
compliance:
  chainalysis:
    password: "{{ env `CHAINALYSIS_TOKEN` }}"
    # Provider username/account identifier.
    username: new_user_02

# Cold wallet signer set.
# Options: comma-separated signer indexes, e.g. "0,1", "0,2", "1,2".
cold_wallet:
  signer_set: "0,1"

# Wallet DB DSN.
# Options:
# - sslmode=disable (local dev)
# - sslmode=require (production)
database:
  url: postgresql://postgres:{{ env `WALLET_DB_PASSWORD` }}@host.docker.internal:5432/wallet?sslmode=disable

# Environment label / tenant namespace.
# Typical: local, dev, staging, production, or per-tenant subdomain.
environment: "local"

# Feature flags.
# Toggle true/false based on your deployment capabilities.
features:
  block_explorer: true
  cold_storage: true
  dev_tools: true
  stake_eth: true
  txn_risk_assessment: true

# Gateway and webhook behavior.
gateway:
  # Webhook listener port.
  webhook_port: 8085
  # Shared secret/token for webhook verification.
  webhook_secret: "{{ env `WEBHOOK_SECRET`}}"
  # Public host URL used in callback generation.
  webhook_url: "http://localhost"
  # Internal buffering/cache knobs.
  buffer_flush_interval: 1s
  cache_flush_interval: 10m
  cache_expiry: 12h

# Logging verbosity.
# Typical: 0 (quiet) to higher values (more verbose), based on runtime implementation.
log_level: 1

# Deployment mode label.
# Typical values: on-prem or cloud-managed flavors depending on environment.
mode: "on-prem"

# MPA/orchestrator integration.
mpa_adapter_config:
  # Blockdaemon auth token.
  auth_token: "{{ env `BD_API_KEY` }}"
  # Approval DB DSN.
  approval_db_url: "postgresql://postgres:{{ env `APPROVAL_DB_PASSWORD` }}@host.docker.internal:5432/approval?sslmode=disable"
  approval_service:
    # Mobile approval bundle/application id.
    bundle_id: "com.blockdaemon.Advanced-MPC-Approver"
    # Minimum supported approver app version.
    min_version: "v1.0.8"
    # Public host and port for approval callbacks.
    host_url: "http://localhost"
    port: "9876"
    # Optional: SMTP relay for email notifications when an operation requires approver action.
    # Omit smtp_relay entirely to disable email notifications.
    # smtp_relay:
    #   host: smtp.internal.example.com
    #   port: 587
    #   from_email: wallet@example.com
    #   username: "{{ env `SMTP_USERNAME` }}"
    #   password: "{{ env `SMTP_PASSWORD` }}"
    #   tls_skip_verify: false  # Set true only for self-signed certs in non-production environments.
  # Supported asset families.
  asset_families:
    - EVM
    - solana
    - bitcoin
    - TVM
    - substrate
  # Supported blockchain definitions.
  # Repeat block entries per chain/network you enable.
  blockchains:
    - protocol_name: "base"
      network_name: "sepolia"
      family_name: "EVM"
      native_symbol: "BASE-ETH"
      decimals: 18
      mainnet_coin_type: 8453
      test_network: true
      native_driver_url: "https://svc.blockdaemon.com/base/testnet/native/http-rpc"
      # Optional: credential for this chain's native_driver_url only. When omitted, the global
      # auth_token above is used. Plain string = Bearer auth; or HTTP Basic, for example:
      # native_driver_auth_token: "{{ env `BASE_SEPOLIA_NATIVE_DRIVER_KEY` }}"
      # native_driver_auth_token: "Authorization: Basic dXNlcjpwYXNz"
      family_properties:
        evm_properties:
          chain_id: 84532
  # Orchestrator DB DSN.
  db_url: "postgresql://postgres:{{ env `ORCHESTRATOR_DB_PASSWORD` }}@host.docker.internal:5432/orchestrator?sslmode=disable"
  # NATS connection for wallet facade user.
  nats:
    broker_url: "nats://host.docker.internal:4222"
    username: "facade"
    password: "{{ env `FACADE_BROKER_PASSWORD` }}"
    # Optional TLS CA bundle for NATS server validation.
    ca_bundle_file: ""
    # Optional client cert/key for mTLS to broker.
    client_certificate_file: ""
    client_certificate_key_file: ""
  # Route IDs must match policy-node [Identity] NodeId (for example route0, route1, route2).
  nodes:
    - route_id: "route0"
    - route_id: "route1"
    - route_id: "route2"
  # True for testnets/sandboxes, false for production mainnet-only behavior.
  should_use_test_networks: true
  staking:
    service_name: "BlockdaemonStaking"
    protocol_name: "ethereum"
    native_asset_symbol: "ETH"
    auth_token: "{{ env `BOSS_API_KEY` }}"
    client_id: "3bcf0d33-cb49"
    is_test_network: true
    url: "https://svc.blockdaemon.com"
  # Global MPA timeout.
  timeout: "5m"
  utxos:
    url: "https://svc.blockdaemon.com"
    auth_token: "{{ env `BD_API_KEY` }}"

# OIDC configuration.
oidc_static_config:
  issuer: "https://auth.example-wallet.local/oauth2/default"
  audience: "api://wallet-example"
  # SPA/web frontend OIDC client id.
  client_id: "wallet-spa-client-id-example"
  # Native/mobile Approver app OIDC client id.
  # Must match policy-node TOML OIDCConfig.ClientID for approver JWT validation.
  client_id_native: "wallet-approver-native-client-id-example"

# Wallet HTTP port.
port: 80

# Optional region label.
region: ""

# Use IAM DB authentication instead of static DB passwords (AWS deployments).
# When true, wallet DB connections use IAM auth token flow (for example RDS IAM auth).
use_iam: false

# Telemetry.
telemetry:
  enable_prom: true
  enable_otlp: true
  service_name: "wallet"
  # OTLP collector endpoint.
  otel_endpoint: "localhost:4318"
  # Sample ratio: 0..1
  otel_sample_ratio: 1

# Ubiquity API integration.
ubiquity:
  url: "https://svc.blockdaemon.com"
  api_key: "{{ env `BD_API_KEY` }}"

# WalletConnect integration (runs in-process in the wallet service).
walletconnect:
  project_id: "927b518384169c157828af8b77181ad3"

# Staking API settings.
staking_api:
  api_key: "{{ env `BD_API_KEY` }}"
  base_url: "https://svc.blockdaemon.com/boss"

# Staking reporting API settings.
staking_report_api:
  api_key: "{{ env `BD_API_KEY` }}"
  base_url: "https://svc.blockdaemon.com/boss/reporting/staking/v1"
  cache_expiration: "10m"

# Mailer sender identity.
mailer:
  from_email: "wallet-noreply@localhost"

# Service version label.
version: "2.0"

# Optional Canton integration.
# Uncomment and set values when Canton support is enabled.
#canton_config:
#  # OAuth2 client-credentials token endpoint for Canton participant and validator APIs.
#  token_url: "https://<oidc_issuer>/realms/canton-devnet/protocol/openid-connect/token"
#  # OAuth2 client ID for the client-credentials grant (machine-to-machine identity). The token subject
#  # for this client must hold IdentityProviderAdmin or ParticipantAdmin on the participant node to create external parties.
#  client_id: "A54918EC-DD51-4B5A-BED5-C0653C3C48FA"
#  # Secret for the OAuth client above. Use secret interpolation, not plaintext.
#  client_secret: "{{ env `CANTON_DEVNET_CLIENT_SECRET` }}"
#  # OAuth audience required by the Canton IdP. Must match the token aud claim.
#  audience: "<audience>"
#  # Canton network label (e.g. devnet, testnet, mainnet). Must match the registered chain.
#  network: "devnet"
#  # gRPC Ledger API endpoint (used by the wallet Canton driver and MPA).
#  ledger_api_url: "https://<canton_grpc_endpoint>:443"
#  # HTTP JSON Ledger API base URL (not gRPC). Required for the embedded Canton Contract Management UI.
#  json_ledger_api_url: "https://<canton_json_ledger_endpoint>/api/json-api"
#  # HTTP Validator API base URL (scan proxy, wallet registration).
#  validator_api_url: "https://<canton_wallet_endpoint>/api/validator"
#  # When true, the wallet re-runs address generation for existing Canton accounts after startup. Dev and recovery convenience after a ledger reset; does not reset sync cursors.
#  regenerate_addresses: true
#  # Optional. Required for outgoing Canton DA Utility Token transfers.
#  # Host root only (no path). Vault discovers instruments at {base}/api/utilities/v0/contract/instrument-configuration/all.
#  token_registry_urls:
#    - "https://<token_registry_host>"
```

## Policy Node Configuration
Each policy node is configured by a single TOML file (e.g. `policy-node.conf`). The three nodes differ only by identity and broker username; the rest of the structure is the same. Sensitive values (database password, broker password, encryptor master password) must come from secrets (env or secret-manager interpolation).

### Confidential Computing and Attestation
For production deployments, secret protection should include Confidential Computing and remote attestation for policy-node workloads. This ensures secret confidentiality while the workload is running (in-use), not only at rest or in transit. For the security architecture (Nitro/Azure TEE isolation, KMS attestation, and MPC share protection), see [MPC and TEE Security Architecture](https://vault.docs.blockdaemon.com/docs/mpc-tee-security-architecture).

- Run policy-node workloads in a confidential execution environment (TEE-backed runtime).
- Require remote attestation before releasing sensitive material such as `ENCRYPTOR_MASTER_PASSWORD`, broker credentials, database passwords.
- Keep secrets in an external secrets manager and release them to workloads only after attestation policy checks pass.
- Use short-lived credentials where possible and rotate static secrets in the service secret table regularly.

### Example policy node TOML
Below is an example for **policy node 0** (`route0`). For node 1 and 2, change `NodeId`, `PlayerIndex`, and `Broker.Username` to `route1`/`route2`, `1`/`2`, and `node1`/`node2`. Passwords and encryptor material must come from your secret store (env vars or secret-manager interpolation); do not commit real passwords.

### `policy-node.conf`

```toml
# OIDC config list. You can define one or more OIDC providers.
[[OIDCConfig]]
# OIDC issuer URL.
Issuer = "https://auth.example-wallet.local/oauth2/default"
# OIDC client id expected in JWT audience/client claims.
ClientID = "wallet-approver-native-client-id-example"
# Optional: allow tokens without email_verified=true.
AllowUnverifiedEmail = false
# Optional: static public keys. If omitted, keys are fetched from the IdP JWKS endpoint.
[[OIDCConfig.PublicKeys]]
# Key ID from JWT header.
KID = "kid-1"
# Base64-encoded DER public key bytes (non-secret public key material).
Key = "MIIBojANBgkqhkiG9w0BAQEFAA...u6AfzAgMBAAE="

# Identity: NodeId must match wallet mpa.nodes[].route_id and broker routing (route0, route1, route2).
[Identity]
NodeId = "route0"
# MPC player index for this node.
PlayerIndex = 0

# Global encryptor password for multi-tenant mode (required unless set per-tenant).
[Secrets]
EncryptorMasterPassword = "{{ env `ENCRYPTOR_MASTER_PASSWORD` }}"

# Global/default database used when tenant-specific database override is not set.
[Database]
# Database driver name.
DriverName = "pgx"
# DSN/connection string.
DataSourceName = "host=postgres sslmode=require user=postgres password={{ env `DB_PASSWORD` }} dbname=local"

# Broker settings.
[Broker]
# Broker endpoint URL.
URL = "nats://nats:4222"
# Username/password auth (optional depending on broker mode).
Username = "node0"
Password = "{{ env `BROKER_PASSWORD` }}"
# Optional token auth (NATS).
Token = ""
# Optional (NATS): allow untrusted TLS certificates (development only).
AllowUntrustedCertificate = false
# Optional broker public key pinning (base64-encoded key bytes).
PublicKey = ""
# Optional client mTLS cert/key file paths.
ClientCertificateFile = ""
ClientCertificateKeyFile = ""
# Optional CA bundle path for broker TLS verification.
CABundleFile = ""

# Service settings.
[Service]
# If > 0, HTTP on this port: GET /live (liveness), GET /ready (readiness after NATS subscribe), GET /metrics (optional). GET /health is deprecated (same as /live).
HealthCheckPort = 8080
# CloudWatch logging toggle.
CloudwatchLogging = false

# Tenant-specific overrides. You can define multiple tenant blocks.
[Tenant.default]
# Optional tenant ERS RSA public key (base64-encoded DER/SPKI).
ERSPublicKey = "MIIBojANBgkqhkiG9w0BAQEFAA...u6AfzAgMBAAE="
# Optional tenant-specific override for EncryptorMasterPassword.
EncryptorMasterPassword = "{{ env `ENCRYPTOR_MASTER_PASSWORD` }}"

# Optional tenant-specific DB override (same keys as [Database]).
[Tenant.default.Database]
DriverName = "pgx"
DataSourceName = "host=postgres sslmode=require user=postgres password={{ env `DB_PASSWORD` }} dbname=local"
```

## Broker Configuration
The message broker (NATS) can be secured with password authentication and/or TLS.

### Password authentication
- The broker is configured with one user per client: e.g. `node0`, `node1`, `node2` for policy nodes, `facade` for the wallet.
- Each component’s config supplies the same username and the password (from a secret). No certificates are required for this mode.
- In NATS server config, `accounts.default.users[].password` should be a bcrypt hash (not raw plaintext). Keep raw passwords only in client-side secret values.

Recommended setup sequence (config-focused):
1. Generate one raw password per NATS user (`node0`, `node1`, `node2`, `facade`).
2. Configure each client service with its username and raw password.
3. Configure NATS `accounts.default.users[].password` with bcrypt hashes of those same passwords.
4. Generate broker server TLS cert/key and CA chain.
5. If using mTLS, generate per-service client cert/key material.
6. Mount cert/key/CA files into pods and point service config fields to file paths (`ClientCertificateFile`, `ClientCertificateKeyFile`, `CABundleFile`).

### Certificates
- **Server TLS**: Broker can expose a TLS listener (e.g. `nats://` with TLS or a separate port). In that case, clients need to trust the broker’s certificate (or the CA that signed it).
- **Client certificates (mTLS)**: Policy nodes and wallet can present a client certificate to the broker. In the policy-node config this is done via:
  - `ClientCertificateFile` – path to the client certificate (PEM).
  - `ClientCertificateKeyFile` – path to the client private key (PEM).
- **CA bundle**: To verify the broker’s server certificate, set:
  - `CABundleFile` – path to a PEM file containing the CA (or CA chain) that signed the broker’s certificate.

In Kubernetes, TLS material is typically mounted from Secrets (e.g. `kubernetes.io/tls` or opaque Secrets) into the pod; config then references file paths under the mount (e.g. `/certs/tls.crt`, `/certs/tls.key`, `/certs/ca.crt`). On VMs, use equivalent paths to where you deploy certs and keys.

### `nats-server.conf`

```json
{
  // NATS client port used by wallet and policy nodes.
  "port": 4222,

// Monitoring/health endpoint (e.g. GET /healthz).
  "http_port": 8222,

// Optional websocket listener for broker websocket clients.
  "websocket": {
    // Websocket port.
    "port": 8443,
    // Set true for plaintext websocket in local/dev.
    // Set false in production and configure websocket TLS.
    "no_tls": true
  },

// TLS for NATS client listener (production recommended).
  // Enable this block and provide broker certificate material in production.
  "tls": {
    // Broker/server certificate PEM.
    "cert_file": "/etc/nats/server-cert.pem",
    // Broker/server private key PEM.
    "key_file": "/etc/nats/server-key.pem",
    // CA bundle PEM used to validate client certificates and trust chain.
    "ca_file": "/etc/nats/intermediate-ca.pem",
    // Require and verify client certificates.
    "verify": true
  },

// Debug tracing. Keep false in production unless troubleshooting.
  "debug": false,
  "trace": false,

// User accounts and credentials.
  "accounts": {
    "default": {
      "users": [\
        // Use bcrypt password hashes in NATS server config.\
        { "user": "node0", "password": "{{ env `NODE0_BROKER_PASSWORD_HASH` }}" },\
        { "user": "node1", "password": "{{ env `NODE1_BROKER_PASSWORD_HASH` }}" },\
        { "user": "node2", "password": "{{ env `NODE2_BROKER_PASSWORD_HASH` }}" },\
        { "user": "facade", "password": "{{ env `FACADE_BROKER_PASSWORD_HASH` }}" }\
      ]
    },
    // Internal NATS system account.
    "SYS": {}
  },

// Authorization options.
  // Option A (default): static username/password in accounts.default.users (shown above).
  // Option B (advanced): auth_callout for externalized auth/token validation.
  "authorization": {
    "auth_callout": {
      // Public key / issuer material used by auth callout verifier.
      // Must match the public key derived from the auth-callout signer seed.
      "issuer": "{{ env `BROKER_PUBLIC_KEY` }}",
      // Users that are allowed to use auth callout in the target account.
      // Must use NATS usernames (not service IDs).
      "auth_users": ["node0", "node1", "node2", "facade"],
      // Account where auth_callout should apply.
      "account": "default"
    }
  },

"system_account": "SYS"
}
```

## Verification
- **Broker**: HTTP health on the monitoring port (e.g. 8222) at `/healthz`.
- **Wallet**: HTTP 200 on `/health` (e.g. port 8080).
- **Policy nodes** (when `HealthCheckPort` is set): HTTP 200 on **`/live`**; HTTP 200 on **`/ready`** only after NATS connect and subscribe (503 before then). `/health` is deprecated (same behavior as `/live`).
- **Postgres**: `pg_isready` (or equivalent) for the postgres user; engine **14 or 15**.

## Summary
- Install in order: namespace/RBAC → secrets/config → database → broker → policy nodes → wallet → cold wallet (optional) → ingress/gateway.
- Policy nodes require broker + database connectivity (+ inter-node MPC).
- Use an external secrets manager in production (for example AWS Secrets Manager, Azure Key Vault, or Vault); never put secrets in ConfigMaps or in repo.
- Broker: password auth and/or TLS/mTLS; policy nodes support CA bundle and optional public key pinning.
- Wallet config: one YAML; MPA section wires route0/route1/route2 via broker only.
- Policy node config: one TOML per node; identity (`NodeId` = wallet `route_id`, `PlayerIndex`), broker URL/user/password, DB (PostgreSQL 14 or 15), tenant ERS public key (plaintext in config), and encryptor password from secrets; probe `/live` and `/ready` on `HealthCheckPort`.
