Helm charts for deploying VTA and WebVH on Kubernetes.
- VTA is deployed as two independent instances:
personalandcommunity - WebVH is deployed as a single instance
Both services use pre-built binaries. On first run, the container waits for manual initialization via kubectl exec.
- Service Configuration
- Prerequisites
- Step 1 — Configure DNS Records
- Step 2 — Deploy
- Step 3 — Initialize Community VTA
- Step 4 — Initialize Personal VTA
- Step 5 — Create WebVH Server DID
- Step 6 — Download DID Files from Pods
- Step 7 — Setup WebVH Server
- Step 8 — Start Services
- Step 9 — Install Local Tools
- Step 10 — Connect PNM
- Step 11 — Bind Community and Personal VTA with CNM
| Service | Default Port | DNS Record | WebVH Path |
|---|---|---|---|
| WebVH | 8000 | webvh.yourdomain.com |
https://webvh.yourdomain.com |
| Community VTA | 8100 | vta-c.yourdomain.com |
https://webvh.yourdomain.com/vta-c |
| Personal VTA | 8100 | vta-p.yourdomain.com |
https://webvh.yourdomain.com/vta-p |
Keep a note open and record each value as prompted — you will need them in later steps.
| ID | What to Save | Used In |
|---|---|---|
| 3a | Community VTA mnemonic phrase | Recovery only |
| 3b | Community VTA DID | Step 11 |
| 3c | Community VTA admin DID | Step 11 |
| 3d | Community VTA admin secret | Step 11 |
| 4a | Personal VTA mnemonic phrase | Recovery only |
| 4b | Personal VTA DID | Step 11 |
| 4c | Personal VTA admin DID | — |
| 4d | Personal VTA admin secret | Step 10 |
| 5a | WebVH Server DID | — |
| 5b | WebVH Server DID secrets bundle | Step 7 |
If you set up your cluster with ic3software/hetzner-k8s, all prerequisites below are already satisfied.
- Kubernetes cluster with:
- NGINX Ingress Controller
- cert-manager with a
letsencrypt-prodClusterIssuer (for TLS) - Longhorn storage class — or override
persistence.storageClasswith your own
kubectlpointed at the target cluster- Helm 3.x
You will need the node IP from your cluster. If you used ic3software/hetzner-k8s, this is the public IP of the node printed at the end of setup.
Replace yourdomain.com with your actual domain and create the following A records:
| Type | Name | Content (IPv4) | Notes |
|---|---|---|---|
| A | vta-c.yourdomain.com |
<NODE_IP> |
DNS only |
| A | vta-p.yourdomain.com |
<NODE_IP> |
DNS only |
| A | webvh.yourdomain.com |
<NODE_IP> |
DNS only |
If you are using Cloudflare, set these records to DNS only (grey cloud, proxy disabled). We use Let's Encrypt for TLS, which requires direct DNS resolution.
git clone https://github.com/ic3software/vta-helm.git
cd vta-helm
make deploy VTA_VERSION=0.4.0 DOMAIN=yourdomain.comOn first deploy the containers wait for manual initialization — they do not start the service until the steps below are completed.
VTA_VERSION and WEBVH_VERSION control which binary is baked into each Docker image and which image tag is deployed. Each build also tags the image with the current git SHA for traceability, pushing three tags total: <version>, <git-sha>, and latest.
Build and push a specific version:
make push-vta VTA_VERSION=0.5.0
make push-webvh WEBVH_VERSION=0.6.0Deploy a specific version:
make deploy-vta VTA_VERSION=0.5.0 DOMAIN=yourdomain.com
make deploy-webvh WEBVH_VERSION=0.6.0 DOMAIN=yourdomain.comRoll back to a previous version (no rebuild required — uses the image already in the registry):
make deploy-vta VTA_VERSION=0.4.0 DOMAIN=yourdomain.com
make deploy-webvh WEBVH_VERSION=0.5.0 DOMAIN=yourdomain.comkubectl exec -it vta-community-0 -- vta setupWhen prompted, use the values below. Replace yourdomain.com with your actual domain.
| Prompt | Action |
|---|---|
| Config file path [config.toml]: | Press Enter |
| VTA name (leave empty to skip): | Enter your community name |
| Services to enable: | Press Enter (both selected by default) |
| Public URL for this VTA: | https://vta-c.yourdomain.com |
| Server host: | Press Enter (default: 0.0.0.0) |
| Server port: | Press Enter (default: 8100) |
| Log level: | Press Enter (default: info) |
| Log format: | Press Enter (default: text) |
| Data directory: | Press Enter (default: data/vta) |
BIP-39 mnemonic:
- Choose: Generate new 24-word mnemonic
-
⚠️ SAVE THIS (3a) — the 24-word mnemonic phrase. You cannot recover this VTA without it. - I have saved my mnemonic phrase [y/N]: → y
Seed storage backend:
- Choose: Config file (hex-encoded seed in config.toml)
DIDComm messaging:
- Choose: Use an existing mediator DID
- Mediator DID:
did:webvh:Qmdmf5QFBbYXVGy8MSk92rivwDDrcJsE6guwwm5hFjbL7L:mediator.fpp1.ic3.devIf you deployed your own mediator using ic3software/mediator-helm, use your own mediator DID here instead.
VTA DID:
- Choose: Create a new did:webvh DID
- VTA DID URL:
https://webvh.yourdomain.com/vta-c - Is this correct? [Y/n]: → Y
- DID creation mode: → Simple — VTA creates keys and document (recommended)
- Make this DID portable? [Y/n]: → Y
- Number of pre-rotation keys [1]: → 1
-
⚠️ SAVE THIS (3b) — the created DID, e.g.did:webvh:...:webvh.yourdomain.com:vta-c - Save DID log to file [VTA-did.jsonl]: → Press Enter
- Export DID secrets bundle? [y/N]: → N
Admin DID:
- Choose: Generate a new did:key (Ed25519)
-
⚠️ SAVE THIS (3c) — the admin DID (did:key:z6Mk...).⚠️ SAVE THIS (3d) — the admin secret. You need both for admin access. - I have saved the admin credential [y/N]: → y
kubectl exec -it vta-personal-0 -- vta setupWhen prompted, use the values below. Only the URL and port differ from community VTA.
| Prompt | Action |
|---|---|
| Config file path [config.toml]: | Press Enter |
| VTA name (leave empty to skip): | Enter your personal VTA name |
| Services to enable: | Press Enter (both selected by default) |
| Public URL for this VTA: | https://vta-p.yourdomain.com |
| Server host: | Press Enter (default: 0.0.0.0) |
| Server port: | Press Enter (default: 8100) |
| Log level: | Press Enter (default: info) |
| Log format: | Press Enter (default: text) |
| Data directory: | Press Enter (default: data/vta) |
BIP-39 mnemonic:
- Choose: Generate new 24-word mnemonic
-
⚠️ SAVE THIS (4a) — the 24-word mnemonic phrase. - I have saved my mnemonic phrase [y/N]: → y
Seed storage backend:
- Choose: Config file (hex-encoded seed in config.toml)
DIDComm messaging:
- Choose: Use an existing mediator DID
- Mediator DID:
did:webvh:Qmdmf5QFBbYXVGy8MSk92rivwDDrcJsE6guwwm5hFjbL7L:mediator.fpp1.ic3.dev
VTA DID:
- Choose: Create a new did:webvh DID
- VTA DID URL:
https://webvh.yourdomain.com/vta-p - Is this correct? [Y/n]: → Y
- DID creation mode: → Simple — VTA creates keys and document (recommended)
- Make this DID portable? [Y/n]: → Y
- Number of pre-rotation keys [1]: → 1
-
⚠️ SAVE THIS (4b) — the created DID, e.g.did:webvh:...:webvh.yourdomain.com:vta-p - Save DID log to file [VTA-did.jsonl]: → Press Enter
- Export DID secrets bundle? [y/N]: → N
Admin DID:
- Choose: Generate a new did:key (Ed25519)
-
⚠️ SAVE THIS (4c) — the admin DID.⚠️ SAVE THIS (4d) — the admin secret. - I have saved the admin credential [y/N]: → y
Run this command from the community VTA pod. Replace yourdomain.com with your actual domain.
kubectl exec -it vta-community-0 -- vta create-did-webvh --context ctx1 --label serverWhen prompted:
| Prompt | Action |
|---|---|
| Context 'ctx1' does not exist. Create it with name [ctx1]: | Press Enter |
| server DID URL: | https://webvh.yourdomain.com |
| Is this correct? [Y/n]: | Y |
| Service endpoints: | Choose No service endpoints |
| Edit DID document in your editor? [y/N]: | N |
| Make this DID portable? [Y/n]: | Y |
| Number of pre-rotation keys [1]: | 1 |
-
⚠️ SAVE THIS (5a) — the created DID, e.g.did:webvh:...:webvh.yourdomain.com - Save DID log to file [server-did.jsonl]: → Press Enter
- Export DID secrets bundle? [y/N]: → y
-
⚠️ SAVE THIS (5b) — the DID secrets bundle (theeyJ...string). You need it to run the WebVH service.
Download the DID log files from the VTA pods to your local machine, then upload them to the WebVH pod.
# Download from community VTA pod
kubectl cp vta-community-0:/root/vta/server-did.jsonl ./server-did.jsonl
kubectl cp vta-community-0:/root/vta/VTA-did.jsonl ./vta-c-did.jsonl
# Download from personal VTA pod
kubectl cp vta-personal-0:/root/vta/VTA-did.jsonl ./vta-p-did.jsonl
# Upload all to WebVH pod
kubectl cp ./server-did.jsonl webvh-0:/root/webvh/server-did.jsonl
kubectl cp ./vta-c-did.jsonl webvh-0:/root/webvh/vta-c-did.jsonl
kubectl cp ./vta-p-did.jsonl webvh-0:/root/webvh/vta-p-did.jsonlGenerate config.toml locally. Replace yourdomain.com with your actual domain.
make webvh-config DOMAIN=yourdomain.comThis creates a config.toml file in your current directory. You can inspect it before copying:
cat config.tomlCopy it into the pod:
kubectl cp config.toml webvh-0:/root/webvh/config.tomlImport the VTA secrets bundle using the value from 5b:
kubectl exec -it webvh-0 -- webvh-daemon import-secrets --vta-bundle <5b>You should see output like:
VTA bundle decoded for DID: did:webvh:...:webvh.yourdomain.com
Load the DID files:
kubectl exec -it webvh-0 -- webvh-daemon load-did --path .well-known --did-log server-did.jsonl
kubectl exec -it webvh-0 -- webvh-daemon load-did --path vta-c --did-log vta-c-did.jsonl
kubectl exec -it webvh-0 -- webvh-daemon load-did --path vta-p --did-log vta-p-did.jsonlRestart the pods in order. Each container will detect its initialized data and start the service.
First, restart WebVH:
kubectl rollout restart statefulset/webvhOnce WebVH is ready, verify the DID files are publicly accessible:
curl https://webvh.yourdomain.com/.well-known/did.jsonl
curl https://webvh.yourdomain.com/vta-c/did.jsonl
curl https://webvh.yourdomain.com/vta-p/did.jsonlEach command should return the contents of the corresponding DID log file.
Then restart the VTA instances:
kubectl rollout restart statefulset/vta-community statefulset/vta-personalClone the repository and install pnm and cnm via Cargo. These tools connect to the deployed VTA services via their public HTTPS URLs.
git clone https://github.com/OpenVTC/verifiable-trust-infrastructure
cd verifiable-trust-infrastructure
cargo install --path pnm-cli
cargo install --path cnm-clipnm setupWhen prompted:
| Prompt | Action |
|---|---|
| What would you like to do?: | Choose Connect to an existing VTA — I have an admin credential bundle |
| Admin credential: | Paste the Personal VTA admin secret from 4d |
| Name for this VTA [vta-p]: | Press Enter |
You should see: Authentication successful.
Verify the setup:
pnm health
pnm contexts list
pnm keys listpnm contexts bootstrap \
--id myapp \
--name "My Application" \
--admin-label "MyApp Admin"
⚠️ SAVE THIS — the credential (eyJ...string) printed at the end. It is shown only once.
cnm setupWhen prompted:
| Prompt | Action |
|---|---|
| Personal VTA DID (press Enter to skip): | Paste Personal VTA DID from 4b |
| Personal VTA URL: | Press Enter (use default) |
| Personal VTA credential (base64): | Paste Personal VTA admin secret from 4d |
You should see: Authentication successful.
| Prompt | Action |
|---|---|
| Community name: | Enter the same community name as in Step 3 |
| Community slug [your-community]: | Press Enter |
| Community VTA DID (press Enter to skip): | Paste Community VTA DID from 3b |
| Community VTA URL: | Press Enter (use default) |
| How do you want to join this community?: | Choose Import existing credential |
| Community admin credential (base64): | Paste Community VTA admin secret from 3d |
You should see: Authentication successful.
Verify:
cnm healthThere should be no errors. If all steps were done correctly, the binding is complete.