Admin Login
HomeGuidesAPI ReferenceChangelogBlockdaemon Main Docs
Log In
Guides

GCP GKE Installation

Install the Institutional Vault on Google Kubernetes Engine using Workload Identity and GCP Secret Manager.

This guide covers installing one Institutional Vault instance on Google Kubernetes Engine (GKE). Complete Cluster Prerequisites first.

Prerequisites

Before starting, confirm you have:

  • A GKE cluster (Standard or Autopilot) with Workload Identity enabled
  • An Artifact Registry repository with the Blockdaemon images mirrored (see Image setup below)
  • GCP Secret Manager secrets provisioned for this instance
  • A Cloud DNS managed zone (or another DNS provider) for the instance domain
  • Helm 3.12+, kubectl, and gcloud CLI configured
  • The mpc chart version and image versions provided by your Blockdaemon account team

Step 1: Mirror images to Artifact Registry

PROJECT=<your-gcp-project>
REGION=us-central1
DST=${REGION}-docker.pkg.dev/${PROJECT}/mpa-wallet

# Image tags from your Blockdaemon delivery (examples).
# Confirm current tags in the Institutional Vault changelog:
# https://vault.docs.blockdaemon.com/changelog
IV_VERSION=v3.6.0              # mothership, wallet-frontend, evm-tracker, nats
POLICY_NODE_VERSION=v9.10.0
CONFIGMAP_INIT_VERSION=v0.2.0

# Create the repository (once)
gcloud artifacts repositories create mpa-wallet \
  --repository-format=docker \
  --location=$REGION \
  --project=$PROJECT

# Authenticate to the Blockdaemon source registry and to Artifact Registry
docker login iv.sepior.net
gcloud auth configure-docker ${REGION}-docker.pkg.dev

mirror() {
  local image=$1 version=$2
  docker pull iv.sepior.net/${image}:${version}
  docker tag  iv.sepior.net/${image}:${version} ${DST}/${image}:${version}
  docker push ${DST}/${image}:${version}
}

# Institutional Vault release tag
for image in mothership wallet-frontend evm-tracker nats; do
  mirror "$image" "$IV_VERSION"
done

# Independently versioned images
mirror policy-node "$POLICY_NODE_VERSION"
mirror configmap-init "$CONFIGMAP_INIT_VERSION"

Set global.imageRegistry: <region>-docker.pkg.dev/<project>/mpa-wallet in your values file. Use the same tags in the versions block (see Step 4).

Step 2: Configure Workload Identity

GKE Workload Identity lets Kubernetes ServiceAccounts impersonate Google Service Accounts (GSAs) without key files. Pods authenticate to GCP APIs (including Secret Manager) using short-lived tokens issued by the GKE metadata server.

Create Google Service Accounts

PROJECT=<your-gcp-project>
NAMESPACE=<tenant-namespace>
ENV=<env>   # e.g. testnet

# Create one GSA per component group (or a single shared GSA for simplicity)
for component in wallet policy-node nats; do
  gcloud iam service-accounts create mpa-${component}-${ENV} \
    --display-name "Vault ${component} (${ENV})" \
    --project $PROJECT
done

Grant Secret Manager access

for component in wallet policy-node nats; do
  gcloud projects add-iam-policy-binding $PROJECT \
    --member="serviceAccount:mpa-${component}-${ENV}@${PROJECT}.iam.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"
done
📘

Note:

For tighter scoping, grant access at the individual secret resource level instead of the project level:

gcloud secrets add-iam-policy-binding <secret-name> \
  --member="serviceAccount:mpa-wallet-${ENV}@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor" \
  --project $PROJECT

Bind Kubernetes ServiceAccounts to GSAs

CLUSTER_NAME=<your-gke-cluster>
CLUSTER_LOCATION=<region-or-zone>

for component in wallet policy-node nats; do
  KSA_NAME=$component   # chart creates SAs named 'wallet', 'policy-node', 'nats'
  GSA_EMAIL="mpa-${component}-${ENV}@${PROJECT}.iam.gserviceaccount.com"

  gcloud iam service-accounts add-iam-policy-binding $GSA_EMAIL \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:${PROJECT}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]" \
    --project $PROJECT
done

Step 3: Populate GCP Secret Manager

The wallet reads its runtime configuration from Secret Manager at pod startup via configmap-init. Secret names use the GCP naming convention and must match the {{ gcpSecret "name" }} or {{ gcpSecret "name#field" }} references in your config templates.

The secret names must match the {{ gcpSecret "name" }} or {{ gcpSecret "name#field" }} references in your config templates. Typical secrets for one instance include:

Secret nameContents
<ns>-wallet-db-infoDatabase connection string
<ns>-policy-node{0,1,2}-encryptor-master-passwordEncryptor master password per node
<ns>-policy-node{0,1,2}-broker-infoNATS broker credentials (JSON: {"PASSWORD":"..."})
<ns>-nats-infoNATS operator/system account credentials
<ns>-facade-oidc-secretOIDC client secret for IdP integration
🚧

Caution:

EncryptorMasterPassword must never change after the initial deployment. Back it up offline immediately after generation.

Create secrets with the gcloud CLI:

echo -n "<generated-password>" | gcloud secrets create \
  "${NAMESPACE}-policy-node0-encryptor-master-password" \
  --data-file=- \
  --project $PROJECT

Step 4: Create your values file

Start from the gke.yaml example in the chart repository and fill in all REPLACE placeholders:

# my-instance-values.yaml

global:
  imageRegistry: <region>-docker.pkg.dev/<project>/mpa-wallet
  environment: <env>
  domain: <base-domain>     # e.g. wallet.example.com
  gcp:
    projectId: <project>
  env:
    - name: GOOGLE_CLOUD_PROJECT
      value: "<project>"
    - name: SECRET_STORE
      value: gcp

secrets:
  provider: gcp

versions:
  wallet: v3.6.0              # mothership / wallet-frontend / nats / evm-tracker
  mpa: v9.10.0                # policy-node
  # configmap-init uses CONFIGMAP_INIT_VERSION (e.g. v0.2.0) from the mirror step

wallet:
  serviceAccount:
    create: true
    annotations:
      iam.gke.io/gcp-service-account: mpa-wallet-<env>@<project>.iam.gserviceaccount.com
  config:
    approvalHostUrl: ""
  ingress:
    enabled: true
    className: nginx   # or gce to use GKE's built-in HTTP(S) LB
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
      nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
      nginx.ingress.kubernetes.io/ssl-redirect: "true"
    hosts:
      - host: api.<env>.<base-domain>
        paths:
          - path: /approval
            pathType: Prefix
            portName: approval
          - path: /event-streaming
            pathType: Prefix
            portName: webhook
          - path: /
            pathType: Prefix
            portName: http
    tls:
      - hosts: [api.<env>.<base-domain>]
        secretName: api.<env>.<base-domain>-tls

frontend:
  enabled: true
  apiPath: https://api.<env>.<base-domain>/
  serviceAccount:
    create: true
    annotations:
      iam.gke.io/gcp-service-account: mpa-wallet-<env>@<project>.iam.gserviceaccount.com
  ingress:
    enabled: true
    className: nginx
    host: <env>.<base-domain>
    tls:
      enabled: true

policyNodes:
  enabled: true
  nodeCount: 3
  serviceAccount:
    create: true
    annotations:
      iam.gke.io/gcp-service-account: mpa-policy-node-<env>@<project>.iam.gserviceaccount.com
  config:
    configFile: "/config/policy-node.conf"
    nodes:
      - templateData:
          policy-node.conf: |
            [Identity]
            NodeId = "route0"
            PlayerIndex = 0
            [Secrets]
            EncryptorMasterPassword = "{{ gcpSecret `<ns>-policy-node0-encryptor-master-password` | tomlEscape }}"
            [Broker]
            Username = "node0"
            Password = "{{ gcpSecret `<ns>-policy-node0-broker-info#PASSWORD` | tomlEscape }}"
            URL = "nats://mpc-nats.<namespace>.svc.cluster.local:4222"
            [Service]
            HealthCheckPort = 8080
      # Repeat for node1 (PlayerIndex 1, NodeId route1, Username node1) and
      # node2 (PlayerIndex 2, NodeId route2, Username node2), referencing
      # policy-node1 / policy-node2 secret names.

nats:
  replicaCount: 3
  persistence:
    storageClass: premium-rwo    # SSD PD; use standard-rwo for lower cost
    size: 5Gi
  serviceAccount:
    create: true
    annotations:
      iam.gke.io/gcp-service-account: mpa-nats-<env>@<project>.iam.gserviceaccount.com

configmapInit:
  enabled: true
  env:
    - name: GOOGLE_CLOUD_PROJECT
      value: "<project>"

dbSetup:
  enabled: true
  network: <cloud-sql-ip-or-fqdn>

natsSetup:
  certProvider: none

Step 5: Create the namespace

kubectl create namespace <tenant-namespace>

Step 6: Install the chart

helm install mpc oci://iv.sepior.net/charts/mpc \
  --version <chart-version> \
  --namespace <tenant-namespace> \
  -f my-instance-values.yaml

Passing policy node configs as external files

The policy node config templates shown in Step 4 are embedded directly in the values file under templateData. If you prefer to keep per-node config files separate (for example, to manage them outside version control), use --set-file instead:

helm install mpc oci://iv.sepior.net/charts/mpc \
  --version <chart-version> \
  --namespace <tenant-namespace> \
  -f my-instance-values.yaml \
  --set-file "policyNodes.config.nodes[0].templateData.policy-node\.conf=./node0.conf" \
  --set-file "policyNodes.config.nodes[1].templateData.policy-node\.conf=./node1.conf" \
  --set-file "policyNodes.config.nodes[2].templateData.policy-node\.conf=./node2.conf"

Each file (node0.conf, node1.conf, node2.conf) contains the rendered TOML template for that node, with {{ gcpSecret "..." }} placeholders. Remove the templateData block from the values file when using this approach.

Step 7: Verify the deployment

kubectl get pods -n <tenant-namespace>

Check the wallet API health endpoint:

kubectl port-forward -n <tenant-namespace> svc/mpc-wallet 8080:80 &
curl http://localhost:8080/ready

Check that the certificate issued:

kubectl get certificate -n <tenant-namespace>

Troubleshooting

Pod stuck in Init:0/1 (configmap-init failing)
Check the init container logs:

kubectl logs -n <tenant-namespace> <pod-name> -c configmap-init

Common causes: GOOGLE_CLOUD_PROJECT environment variable missing, Workload Identity binding not complete, GSA lacks secretmanager.secretAccessor, or the secret name in the template doesn't exist in Secret Manager.

Workload Identity not working

Verify the KSA annotation:

kubectl describe serviceaccount wallet -n <tenant-namespace>
# Should show: iam.gke.io/gcp-service-account: mpa-wallet-<env>@<project>.iam.gserviceaccount.com

Verify the IAM binding:

gcloud iam service-accounts get-iam-policy \
  mpa-wallet-<env>@<project>.iam.gserviceaccount.com
# Should show workloadIdentityUser binding for the KSA

StorageClass premium-rwo not found

GKE Standard clusters include premium-rwo and standard-rwo by default via the PD CSI driver. On older clusters or Autopilot, check available storage classes:

kubectl get storageclass

Certificate not issuing with DNS-01

Verify cert-manager's ServiceAccount has the roles/dns.admin binding on the Cloud DNS project and that GOOGLE_CLOUD_PROJECT is correctly set in the mpc-cluster-prereqs ClusterIssuer configuration.

Network requirements

Internal cluster communication

SourceDestinationPortProtocolPurpose
walletnats4222TCPNATS client — MPC coordination
walletPostgreSQL5432TCPApplication database
frontendwallet80TCPInternal API proxy
nats podnats pod6222TCPNATS inter-pod cluster routing
db-setup jobPostgreSQL5432TCPSchema migrations
All podsCoreDNS53UDP/TCPDNS resolution

Policy nodes expose port 8080 for health checks.

Outbound to GCP services

DestinationPortPurposeRecommended endpoint
GCP Secret Manager (secretmanager.googleapis.com)443Secret resolution at pod startupPrivate Service Connect or public
GCP metadata server (metadata.google.internal)80Workload Identity token exchangeInternal (link-local, no internet egress)
Artifact Registry443Image pullPrivate Service Connect or public
Cloud DNS443DNS record management (external-dns)Public
Let's Encrypt (acme-v02.api.letsencrypt.org)443TLS certificate issuance (cert-manager)Public internet

Inbound

SourceDestinationPortProtocolPurpose
External clientsIngress → wallet443HTTPSWallet REST API
External clientsIngress → frontend443HTTPSWeb UI (if frontend.enabled: true)

NetworkPolicy

The chart includes optional egress-only NetworkPolicy resources, disabled by default. Enable with:

security:
  networkPolicies:
    enabled: true

When enabled, egress is allowed from all pods in the namespace to: CoreDNS (53 UDP/TCP), any HTTPS (443/TCP), PostgreSQL (5432/TCP), and NATS internally (4222, 8222, 8443/TCP). No ingress NetworkPolicy rules are rendered by the chart.


Did this page help you?