Cloud-agnostic Install Guide

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

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

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) {{ envVAR_NAME }} (example: {{ envBD_API_KEY }}) Reads VAR_NAME from process environment; errors if missing.
AWS Secrets Manager lookup {{ awsSecretsecret-name }} or {{ awsSecretsecret-name#FIELD }} Reads secret by name; when #FIELD is provided, treats the secret value as JSON and returns that key.
Azure Key Vault lookup {{ azureSecretsecret-name }} or {{ azureSecretsecret-name#FIELD }} Same pattern as AWS; optional #FIELD extracts a JSON field.
Attested enclave secret lookup (MPA policy-node) {{ enclaveSecretsecret-name }} Retrieves the secret and decrypts through enclave/attestation flow before injecting into config.

In this repository:

Security tiers (reference)

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

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

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

# 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

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

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

{
  // 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

Summary