vtafarm-api — VTA Farm provisioning backend
Repo: github.com/ic3software/vtafarm-api
vtafarm-api is the engine behind VTA Farm: a Go REST service that turns “create agent” in the vtafarm portal into a running VTA — DNS, TLS, a Kubernetes namespace, a Vault-sealed seed, a published did-webvh document, a mediator wiring, and a super-admin handed to the user’s own PNM or phone. It is the oldest of the three Farm repos (first commit 2026-05-26, 129 commits) and the one where the security model actually lives. It describes itself as “managing VTA setup sessions with per-user namespace isolation”, which is exactly right: a session is one provisioned stack, and everything about it is scoped to the user who owns it.
Why it is shaped this way
Hosting someone else’s trust agent is a delicate promise: the VTA is a signing oracle whose value is that only its controller can make it act. The API answers that in three layers. Per-user namespaces (vtafarm-user-<id>) with their own ServiceAccounts and RBAC keep tenants apart. HashiCorp Vault holds each VTA’s BIP-39 master seed at secret/vta/user-<id>/session-<id>/master-seed, readable only by a Vault role bound to that namespace’s vta ServiceAccount; the API’s own AppRole policy deliberately has no read capability on any seed path — it provisions and tears down access but never reads a secret. Control stays with the user: the Farm only ever runs vta import-did --role admin with a DID minted on the user’s own device — by pnm setup on a workstation or, since 0.11.0, by the Keyring phone wallet answering a QR — so the admin key never exists on Farm infrastructure. What the operator can do is documented honestly: the ClusterRole includes pods/exec and PVC access, config exports and the TOML editor reveal mediator admin material, and whoever holds Vault’s root token holds everything — this is Vault-sealed hosting, not a TEE.
What It Does
Provisioning a VTA Only session (the path every individual takes):
POST /setupvalidates (including, since 0.12.0, the account’s VTA limit), creates a proxied Cloudflare A recordvta-<name>.firstperson.dev, persists the session.EnsureUserEnvironment(namespace,pod-operatorandvtaServiceAccounts, Role, RoleBinding) andEnsureUserAccess(Vault policy + kubernetes-auth rolevta-user-<id>).- Renders
vta-setup.toml: REST, DIDComm and TSP enabled (#25);[secrets] backend = "vault";[messaging] kind = "existing"pointing at a mediator DID;[vta_did] kind = "create_webvh"at a DID host under/<name>-vta; CORS pre-allowing the VTA Wallet extension origin. By default both point at the platform stack; since 0.8.0POST /setup/connection/inspectlets the user name any DID-hosting DID and mediator DID instead, classified as platform, in-farm or external. - Runs
vta setupas a one-off Job on a 200 Mi PVC with explicit Fjall cache / write-buffer / journal budgets (0.9.3); parses the VTA DID and DID log from stdout; publishes thedid.jsonlto the did-hosting daemon — since 0.9.4 over signed Trust Tasks with verified signed replies, the daemon’s REST management and bearer-token flows having been removed upstream — using the Farm’s owndid:key, enrolled in that daemon’s ACL as admin. An external DID host instead parks atawaiting_did_publicationwith thedid.jsonloffered for download until the published history resolves. - Parks at
awaiting_admin_diduntil an Admin DID arrives — pasted frompnm setup, or posted by a phone to the signed one-time callback URL in the Automatic Mobile Connection QR (0.11.0: the code carriesvta_didand acallback_urlwhose credential the Farm minted; the app POSTs its Ed25519did:key, gets aprogress_token, polls progress and reports completion; accepted work is durable across retries and API restarts, migration 000039). Then a provision Job runsvta import-did --role admin(a super-admin; mobile administrators carry the ACL labelmobile integration) anddid-mgmt servers add. - Creates Deployment, Service and a Traefik Ingress (wildcard TLS from the controller’s default TLSStore) and marks the session
runningonly when/healthreports Ready (0.4.0).
Teardown reverses all of it — DNS, hosted DID and ACL entry, Kubernetes resources, the Vault seed. The same connection flows (PNM paste or Keyring QR) add further administrators to a running VTA (0.6.0 / 0.11.0): ACL maintenance is serialized per session, keeps a synchronized ACL snapshot (exposed as Super Admin entries only, 0.6.1) and restarts the VTA afterwards.
Full Stack (behind Fullstack Access, the flag that replaced beta_access in 0.12.0) runs the same pipeline for four components — VTA, mediator (affinidi-tdk), DID-hosting daemon and VTC (verifiable-trust-infrastructure) — on four hosts, every one using Vault kubernetes auth, the mediator keeping messages in fjall on its PVC (no Valkey), the VTC image required to be built with --features vault-secrets and, since 0.11.1, provisioned in single-admin mode so the initial administrator can add another without a second existing approver. The ephemeral VTC setup DID is granted one-time hand-off access so VTC can replace it with its permanent admin DID (0.9.1 — the same VTI-ACL-054 hand-off the browser wallet adopted). It is the Kubernetes automation of vti-setup’s single-host explore flow, including the offline sealed-bundle steps, and vti-setup’s deploy stream now documents it step by step.
The platform stack is one Full Stack at vta. / mediator. / dids. / vtc.firstperson.dev, owned by a system account, that every default VTA Only agent depends on; vta_only is refused (503, with a reason) until it is running. Admins can add co-admins to its VTA (#23), inspect and refresh its live ACL (0.6.0), and edit its components’ TOML (0.7.0).
Around the core: domains (managed / platform / custom — custom zones verified by TXT + four CNAMEs, certificates via cert-manager HTTP-01); per-component TOML configuration read, validated and applied with a readiness check and automatic restore on failure (0.7.x); image upgrades per session or in batch, with automatic rollback to the previous image when a component fails to come up (0.9.1); memory profiles — admin-tunable requests and limits per component and session, persistent factory defaults (0.10.0: 128/256 Mi mediator, 64/256 Mi VTC, 16/64 Mi VTA, 64/128 Mi DID hosting — 272/704 Mi for a full stack) feeding capacity estimates (#14); monitor endpoints for UptimeRobot (#13); config/log exports (#34); load tests (#37, #39); and a SIOPv2 wallet login (0.5.0) — its own Go verifier of Ed25519 id_tokens against did:key or did:webvh keys including did:webvh history and rotation, a second login method behind durable one-time challenges that issues the existing role-scoped cookie, with at least one passkey retained for recovery. Images are listed live from GHCR (ghcr.io/ic3software/{vta,mediator,did-hosting-daemon,vtc}), newest first with latest flagged — the Farm does not pin a coordinated release; each session records the tag it was created with, and nothing in the repo names a default tag.
Components
| Package | Role |
|---|---|
internal/setup/ | orchestrator state machines (vta_only, full_stack, VTC), TOML templates, stdout parsers, Fjall budgets; resumes interrupted sessions on restart |
internal/connection/, internal/didkey/ | DID-pair inspection (platform / in-farm / external); did:key validation for admin DIDs |
internal/k8s/, internal/upgrade/, internal/resourceprofile/, internal/capacity/ | client-go: namespaces, Jobs, Deployments, PVCs, Ingress/Traefik middleware, cert-manager Certificates, readiness, exec; upgrade + rollback; memory profiles and placement estimates |
internal/vault/ | per-user policy and kubernetes-auth role; seed deletion |
internal/didhosting/ | Trust-Task client per daemon URL (rewritten in #60 from the REST/bearer client) |
internal/siop/, internal/passkey/ | the SIOPv2 verifier and challenge store; WebAuthn |
internal/cloudflare/, internal/dnscheck/, internal/ghcr/ | proxied A records; custom-domain verification; live image-tag listing |
internal/handler/ | user, admin, setup, connection, mobile-connection, domain, upgrade, resource, monitor, load-test, signup, passkey, siop routes (OpenAPI at /docs) |
migrations/ | 40 golang-migrate steps (admins → users → sessions → passkeys → full-stack → domains → sharing → grants → load tests → wallet links → config → resources → mobile connections → fullstack access) |
helm/vtafarm-api/ | chart with bundled PostgreSQL 18.4, ClusterRole, Vault/GHCR/WebAuthn/SIOP/mobile-signing values |
docs/ | eleven design documents — VTA setup, full stack, custom domains, platform-stack grants, load tests, SIOP login (+ browser testing), the mobile connection design and the Mobile Connection App API the Keyring team integrates against |
Stack: Go 1.26, Gin, GORM, PostgreSQL 18, client-go, passkeys via WebAuthn, HS256 JWT cookies, Dependabot since 10-05.
Dependencies & relationships
Consumed by vtafarm; deployed and given its Vault, SIOP settings and MOBILE_CONNECTION_SIGNING_KEY by vtafarm-k8s (stack 04 installs Vault, vault-bootstrap.sh farm mints the API’s AppRole; stack 05 installs the chart). Drives the vta, mediator, did-hosting-daemon and vtc binaries by their setup --from recipes exactly as vti-setup documents for a human. The user’s side of the handshake is the pnm CLI from verifiable-trust-infrastructure or the Keyring phone wallet speaking the Mobile Connection App API; the wallet login verifies tokens from the vta-browser-plugin.
Recent Development
Releases: v0.1.0 / v0.2.0 (2026-08-19), v0.3.0 (08-20), v0.3.1 (08-30), v0.4.0 (08-31), then v0.5.0 → v0.12.1 between 09-18 and 10-05 — sixteen releases, each one the API half of a portal release.
Eucalyptus-era releases — 2026-09-18 → 10-05 (v0.5.0 → v0.12.1; #41–#73)
- 10-05 v0.12.1 (#73) — Go dependency refresh via the new Dependabot (#65–#71: kubernetes group, gin-contrib/cors, golang-migrate 4.20, pgx driver, x/net, x/sync). v0.12.0 (#64) — breaking:
fullstack_accessreplacesbeta_accessand the admin endpoint moves to/api/v1/admin/users/{id}/fullstack-access; users without it hold at most two undeleted VTAs (provisioning and failed sessions count; deletion frees a slot; concurrent creation cannot exceed it); profiles carryvta_count/vta_limit(migration 000040). - 10-04 v0.11.1 (#63) — Farm-provisioned VTCs enable single-admin mode.
- 09-30 v0.11.0 (#62) — Automatic Mobile Connection: signed QR callbacks, acceptance of a phone’s Ed25519
did:key, progress and completion endpoints, durable connection requests (migration 000039), themobile integrationACL label, a per-environmentMOBILE_CONNECTION_SIGNING_KEY, callback URLs derived from the cluster domain, and the app-facing API document. v0.10.0 (#61) — persistent memory defaults (admin endpoints, migration 000038), the 272/704 Mi factory profile, live-deployment fallback for session resource summaries. - 09-29 v0.9.4 (#60) — DID hosting over signed Trust Tasks; the daemon’s removed REST management and bearer-token flows no longer supported; enrollment requires the claim code beside the single-use URL (0.9.2, #57). v0.9.3 (#58) — memory profiles and Fjall budgets applied to every new workload, capacity estimates aligned. v0.9.1 (#55, #56) — failed image upgrades roll back automatically, surviving an API restart; the VTC setup DID gets one-time hand-off access.
- 09-27 v0.9.0 (#54) — admin resource endpoints: desired vs live memory, validated single or batch changes with sequential readiness and rollback; failed sessions keep the stage they stopped at. v0.8.0 (#51, #52) — breaking: the share-code API (
PUT /setup/{id}/sharing,/setup/connection/validate,MAX_STACK_CONNECTIONS) removed;POST /setup/connection/inspecttakes a DID-hosting DID and a mediator DID and classifies the host; external DID hosting retains the generateddid.jsonland pauses atawaiting_did_publicationuntil owner-only validation passes. - 09-23/24 v0.7.0 / v0.7.1 (#47, #49) — per-component TOML config read/validate/update with targeted restart, readiness check and automatic restore; the temporary Secrets the write and rollback Jobs need.
- 09-21 v0.6.0 / v0.6.1 (#43, #44, #46) — link another PNM administrator to a running VTA; read and refresh the synchronized ACL; the platform stack’s live ACL for admins; ACL endpoints filtered to unrestricted Super Admin entries.
- 09-18 v0.5.0 (#41, #42) — the SIOP verifier and linked-wallet login; accounts with a linked wallet must keep a passkey.
Earlier
- 09-13 SIOPv2 wallet-login design, proposed (#40); 09-12 ephemeral load-test admin DID (#39); 09-04 Apache-2.0 + DCO (#38).
- 08-31 v0.4.0 load tests (#37); readiness-gated
running(#36). 08-30 v0.3.1 full-stack mediatorscors = "any"(#35). 08-20 v0.3.0 exports (#34); PVCs at/work/<component>(#33). 08-19 v0.1.0/v0.2.0 GHCR publishing, changelog; Vault charts handed to vtafarm-k8s; origin and zone from config (#31, #32). 08-17/18 ingress-nginx → Traefik (#27, #28); VTA Wallet origin allowed; monitor window (#30). 08-12 TSP advertised across all four components (#25). 08-02/03 VTA Only on a shared custom stack (#22); platform-stack co-admins (#23). - 07-26 → 07-31 custom domains (#17, #18);
full_stack_with_vtcfolded (#16); admin delete (#15); shared dev PostgreSQL (#20). 07-09 → 07-23 admin session list and batch upgrades; signup requests (#9); self-service upgrades (#10); dashboard (#12); monitor (#13); capacity gate (#14). 07-01 → 07-08 Full Stack (#5); Vault in mediator and DID hosting (#6); VTC (#7). 06-09 → 06-26 VTA Only wizard (#1); passkey-only auth (#2); master seed moved from a K8s Secret into Vault (#4, 06-22). 05-26/27 scaffold.
Maturity and known gaps
Production for individuals since June; Full Stack and custom domains are behind Fullstack Access or cluster prerequisites; the mobile connection is labeled Testing and its end-to-end path depends on the Keyring app. Open by design: how a user-supplied DID host authorizes the Farm (the external-host flow makes the user publish the log themselves rather than solve it); single-region Cloudflare-only DNS; the SIOP login verifies the wallet’s proxyLogin shape, not the auth/authenticate/0.2 Trust-Task sign-in the browser plugin’s plain login moved to on 09-28.
See also: vtafarm, vtafarm-k8s, vti-setup, vta-topology, verifiable-trust-agent, affinidi-tdk, affinidi-webvh-service, trust-tasks, rp-sdk-js