Admin Login
HomeGuidesAPI ReferenceChangelogBlockdaemon Main Docs
Log In
Guides

Azure AKS Installation

Install the Institutional Vault on Azure Kubernetes Service using workload identity and Azure Key Vault.

This guide covers installing one Institutional Vault instance on Azure Kubernetes Service (AKS). Complete Cluster Prerequisites first.

Prerequisites

Before starting, confirm you have:

  • An AKS cluster with workload identity enabled
  • An Azure Container Registry (ACR) with the Blockdaemon images mirrored (see Image setup below)
  • An Azure Key Vault provisioned for this instance
  • DNS zone managed by Azure DNS (or another provider if not using external-dns)
  • Helm 3.12+ and kubectl configured for the cluster
  • The mpc chart version and image versions provided by your Blockdaemon account team

Step 1: Mirror images to ACR

The Blockdaemon images must be in a registry your cluster can pull from. Mirror them to your ACR before installing:

ACR=<your-acr>.azurecr.io/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

# Authenticate to the Blockdaemon source registry (credentials from your account team)
docker login iv.sepior.net
az acr login --name <your-acr>

mirror() {
  local image=$1 version=$2
  docker pull iv.sepior.net/${image}:${version}
  docker tag  iv.sepior.net/${image}:${version} ${ACR}/${image}:${version}
  docker push ${ACR}/${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: <your-acr>.azurecr.io/mpa-wallet in your values file so the chart constructs image references correctly. Use the same tags in the versions block (see Step 4).

Step 2: Create and configure the managed identity

The wallet pods authenticate to Azure Key Vault using a user-assigned managed identity federated with the Kubernetes ServiceAccounts.

RESOURCE_GROUP=<your-rg>
IDENTITY_NAME=mpa-<env>-<namespace>
AKS_NAME=<your-aks-cluster>
NAMESPACE=<tenant-namespace>   # Kubernetes namespace for this instance
KEY_VAULT_NAME=<your-kv>

# Create the managed identity
az identity create \
  --resource-group $RESOURCE_GROUP \
  --name $IDENTITY_NAME

CLIENT_ID=$(az identity show -g $RESOURCE_GROUP -n $IDENTITY_NAME --query clientId -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)

# Grant Key Vault access (Key Vault Secrets User role)
KV_ID=$(az keyvault show -n $KEY_VAULT_NAME --query id -o tsv)
az role assignment create \
  --assignee $CLIENT_ID \
  --role "Key Vault Secrets User" \
  --scope $KV_ID

# Federate the identity with the wallet and policy-node ServiceAccounts
OIDC_ISSUER=$(az aks show -g $RESOURCE_GROUP -n $AKS_NAME \
  --query "oidcIssuerProfile.issuerUrl" -o tsv)

for SA in wallet policy-node nats; do
  az identity federated-credential create \
    --name ${IDENTITY_NAME}-${SA} \
    --identity-name $IDENTITY_NAME \
    --resource-group $RESOURCE_GROUP \
    --issuer $OIDC_ISSUER \
    --subject "system:serviceaccount:${NAMESPACE}:${SA}" \
    --audience api://AzureADTokenExchange
done
📘

Note:

You can federate one identity to multiple ServiceAccounts, or create separate identities per component. The example above uses one shared identity for simplicity. The chart creates ServiceAccounts named wallet, policy-node, and nats by default when serviceAccount.create: true.

Step 3: Populate Azure Key Vault secrets

The wallet reads its runtime configuration from Key Vault at pod startup via configmap-init. Populate the secrets before deploying the chart.

The key names in Key Vault must match the {{ azureSecret "name" }} references in your config templates. Typical secrets for one instance include:

Secret name patternContents
<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 per node
<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. It is the root key for database encryption. Back it up to a separate vault or offline storage immediately after generation.

Step 4: Create your values file

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

# my-instance-values.yaml

global:
  imageRegistry: <your-acr>.azurecr.io/mpa-wallet
  environment: <env>          # e.g. testnet, mainnet, dev
  domain: <base-domain>       # e.g. wallet.example.com

azure:
  keyVaultUri: "https://<your-kv>.vault.azure.net/"
  clientId: <MANAGED_IDENTITY_CLIENT_ID>
  tenantId: <AZURE_TENANT_ID>

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:
      azure.workload.identity/client-id: <MANAGED_IDENTITY_CLIENT_ID>
      azure.workload.identity/tenant-id: <AZURE_TENANT_ID>
  podLabels:
    azure.workload.identity/use: "true"

policyNodes:
  nodeCount: 3
  serviceAccount:
    create: true
    annotations:
      azure.workload.identity/client-id: <MANAGED_IDENTITY_CLIENT_ID>
      azure.workload.identity/tenant-id: <AZURE_TENANT_ID>
  podLabels:
    azure.workload.identity/use: "true"

nats:
  replicaCount: 3
  persistence:
    storageClass: managed-csi
    size: 5Gi
  serviceAccount:
    create: true
    annotations:
      azure.workload.identity/client-id: <MANAGED_IDENTITY_CLIENT_ID>
      azure.workload.identity/tenant-id: <AZURE_TENANT_ID>
  podLabels:
    azure.workload.identity/use: "true"

ingress:
  enabled: true
  className: nginx   # or azure/application-gateway if using AGIC
  host: api.<env>.<base-domain>
  tls:
    enabled: true
    certManager:
      enabled: true
      clusterIssuer: letsencrypt-prod

frontend:
  enabled: true
  apiPath: https://api.<env>.<base-domain>/
  ingress:
    enabled: true
    className: nginx   # or azure/application-gateway if using AGIC
    host: <env>.<base-domain>
    tls:
      enabled: true

configmapInit:
  enabled: true

dbSetup:
  enabled: true
  network: <postgres-server-fqdn>

Step 5: Create the namespace

Create the namespace before running helm install. Do not let the chart manage it: if Helm owns the namespace, helm uninstall will delete it along with all PersistentVolumeClaims, destroying stateful data.

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

Helm validates values against values.schema.json before rendering. Correct any validation errors, then re-run.

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 {{ azureSecret "..." }} placeholders. Remove the templateData block from the values file when using this approach.

Step 7: Verify the deployment

Check that all pods reach Running or Completed state:

kubectl get pods -n <tenant-namespace>

Expected output (component set depends on your values):

NAME                              READY   STATUS      RESTARTS   AGE
mpc-wallet-<hash>                 1/1     Running     0          2m
mpc-policy-node-0-<hash>          1/1     Running     0          2m
mpc-policy-node-1-<hash>          1/1     Running     0          2m
mpc-policy-node-2-<hash>          1/1     Running     0          2m
mpc-nats-0                        1/1     Running     0          2m
mpc-nats-1                        1/1     Running     0          2m
mpc-nats-2                        1/1     Running     0          2m
mpc-db-setup-<hash>               0/1     Completed   0          2m

Check the wallet API health endpoint:

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

A 200 OK response confirms the wallet is running and database migrations have completed.

Check ingress TLS:

kubectl get certificate -n <tenant-namespace>

The certificate issued by cert-manager should show READY: True within a few minutes of install. DNS must resolve before the ACME challenge succeeds.

Troubleshooting

Pod stuck in Init:0/1

The configmap-init init container failed to resolve Key Vault secrets. Check its logs:

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

Common causes: missing federated credential, incorrect clientId or tenantId, Key Vault firewall blocking the pod's egress, secret name mismatch.

Certificate not issuing (READY: False)

Check cert-manager events:

kubectl describe certificate -n <tenant-namespace>
kubectl describe certificaterequest -n <tenant-namespace>

Common causes: DNS record not yet propagated, ingress controller LB not yet assigned an IP, ACME rate limits hit (use letsencrypt-staging for testing).

Policy Nodes not connecting to NATS

Check that the NATS URL in the Policy Node config template matches the in-cluster NATS service name (nats://mpc-nats.<namespace>.svc.cluster.local:4222 or the rel-nats alias, depending on the release name).

ACI Policy Nodes (external topology)

The guide above runs all three Policy Nodes as in-cluster AKS Deployments. Azure also supports an external topology where Policy Nodes run as Azure Container Instances (ACI) Confidential Containers outside the AKS cluster. This provides hardware-level attestation and stronger isolation guarantees for the MPC signing nodes.

The external ACI topology requires different chart values (policyNodes.enabled: false plus external node addressing) and additional Azure infrastructure (a VNet integration so ACI containers can reach the NATS StatefulSet inside AKS). Documentation for the ACI external topology is being added - contact your Blockdaemon account team for current guidance.

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 Azure services

DestinationPortPurposeRecommended endpoint
Azure Key Vault (<vault>.vault.azure.net)443Secret resolution at pod startupPrivate Endpoint or public
Azure AD token endpoint443Workload identity token exchangePublic
Azure Container Registry443Image pullPrivate Endpoint or public
Azure 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?