Retrieve credentials from the VTA vault at runtime
By the end of this guide, your service retrieves any type of credential from the VTA vault at runtime instead of holding it in its own configuration.
Services that need credentials at runtime often end up holding them in environment variables, configuration files, or secrets managers with broad access policies. Any of those creates a shared exposure surface: credentials can be logged, over-shared, or become stale without the consuming service knowing. The VTA vault removes the credential from the service entirely. The service holds only an entry ID and calls the vault/release Trust Task to request it. Each request is:
- Sealed to the calling service’s own DID.
- Recorded in the audit trail.
For what the vault stores and which roles can reach it, see Secrets vault. For how the sealing works, and why a relay carrying the response cannot read it, see Sealed transfer.
Use this guide when:
- A service, AI agent, or pipeline needs a credential at runtime without holding it permanently in configuration.
- You want every credential retrieval audited and access-controlled by DID and context.
- You want to rotate credentials without redeploying the service.
- Your service needs OAuth 2.0 or OIDC client credentials (
client_id/client_secret) to run its own authorisation flow: store them as acustomentry.
Every release follows the flow below:
Supported secret kinds
The vault stores typed secrets. Choose the kind that matches your credential:
secretKind | What it stores | Typical use |
|---|---|---|
bearer-token | A token + optional header name and prefix | GitHub PATs, API keys, JWT access tokens |
password | Username + password | Internal tools, legacy APIs |
oauth-tokens | OAuth refresh token + optional access token | Google, Slack, and other OAuth providers |
ssh-key | SSH private key + optional passphrase | Git servers, remote hosts |
passkey | WebAuthn credential ID + private key | Passkey-based authenticators |
did-self-issued | A DID + its signing key ID | Self-issued DID credentials |
didcomm-peer | A did:peer + its signing key ID | DIDComm peer relationships |
custom | Any named fields you define | OAuth 2.0 client ID/secret, proprietary credential formats |
For a practical example using bearer-token with GitHub, see Keep API tokens out of your service config.
Prerequisites
- A running VTA with DIDComm enabled. See Quickstart.
- Your DID is enrolled with the
applicationrole (or higher) in the target context. See Grant and revoke access. pnmCLI connected to your VTA (pnm vta inforeturns without error).
Store the credential
Step 1: Create the entry file
Create entry.json with the vault entry metadata. contextId determines access control: only DIDs enrolled in that context can call vault release for this entry, the same isolation model used for signing keys. See Contexts for how context isolation works across a VTA. targets records the intended destination. The VTA logs it alongside the caller’s DID, so the audit trail shows which service accessed which credential for which system.
{
"contextId": "my-app",
"targets": [
{ "kind": "web-origin", "origin": "https://example.com" }
],
"label": "Example API key",
"secretKind": "bearer-token"
}The targets array accepts one target per entry. Use "kind": "web-origin" for HTTP APIs and web services, or "kind": "did" when the target is identified by a DID.
Step 2: Create the secret file
Create secret.json with the cleartext credential. The kind field must match secretKind in entry.json. The VTA validates the secret schema at store time, not at release time. Catching mismatches immediately means format errors surface when you run the upsert command, not at 3am when a service tries to start and the vault returns an unexpected structure.
Bearer token:
{
"kind": "bearer-token",
"token": "<your-token>"
}Password:
{
"kind": "password",
"username": "service-account",
"password": "<your-password>"
}OAuth tokens:
{
"kind": "oauth-tokens",
"provider": "google",
"refreshToken": "<refresh-token>",
"accessToken": "<access-token>",
"accessTokenExpiresAt": "2026-09-01T00:00:00Z",
"scopes": ["https://www.googleapis.com/auth/drive.readonly"]
}oauth-tokens stores tokens your service has already obtained from an OAuth provider. To store the client_id/client_secret your service needs to run the OAuth 2.0 or OIDC flow itself, use custom instead:
Custom (OAuth 2.0 client credentials):
{
"kind": "custom",
"fields": [
{ "name": "client_id", "value": "<your-client-id>" },
{ "name": "client_secret", "value": "<your-client-secret>", "hidden": true }
]
}Do not commit secret.json to source control. Add it to .gitignore and delete it after the upsert completes. The VTA is the authoritative store. Keep the credential in one place, not two.
pnm vault release prints the credential to stdout. In CI, capture it with --json into a masked variable so it stays out of build logs. On a shared machine, keep the command out of shell history.
Step 3: Store the entry
pnm vault upsert --entry-file entry.json --secret-file secret.jsonThe CLI encrypts secret.json to the VTA’s public key before transmitting it. This is an end-to-end application-layer guarantee that operates independently of TLS. Even if TLS were intercepted or misconfigured, the ciphertext is unreadable without the VTA’s private key. Output:
Upserted entry:
{
"created": true,
"entry": {
"id": "vault_abc123",
"contextId": "my-app",
"targets": [{ "kind": "web-origin", "origin": "https://example.com" }],
"label": "Example API key",
"secretKind": "bearer-token",
"version": 1,
"createdAt": "2026-08-12T10:00:00Z"
}
}Note the id: pass it to pnm vault release at runtime.
Release the credential at runtime
The service calls this command when it needs the credential. Replace vault_abc123 with the id from Step 3’s output.
Before releasing the secret, the VTA:
- Checks that the caller’s DID is enrolled in the context that owns the entry (the
contextIdset inentry.json). - Encrypts the secret to the caller’s public key, derived from their DID, using the same sealed transfer mechanism every credential-bearing VTA operation uses.
Only the holder of the matching private key can decrypt the response, so a compromised relay, log aggregator, or network observer sees only an opaque ciphertext.
pnm vault release vault_abc123Output:
Released secret (cleartext):
{
"kind": "bearer-token",
"token": "<your-token>"
}For automation, use the global --json flag to output raw JSON and pipe to jq:
TOKEN=$(pnm --json vault release vault_abc123 | jq -r '.token')For a password entry, extract username and password:
CREDS=$(pnm --json vault release vault_abc123)
USERNAME=$(echo "$CREDS" | jq -r '.username')
PASSWORD=$(echo "$CREDS" | jq -r '.password')Rotate the credential
The vault entry ID is a stable pointer. Services reference the ID, not the credential value itself. Rotating the credential is a vault-internal operation: the ID stays the same, and the service receives the new value on its next vault release call. No configuration change or redeployment is required.
To rotate the credential:
- Create a new
secret.jsonwith the replacement credential. - Add
"id": "vault_abc123"toentry.jsonto target the existing entry. - Run the upsert again:
pnm vault upsert --entry-file entry.json --secret-file new-secret.jsonThe service continues using the same entry ID, with no configuration change required.
List and manage entries
Archive and delete behave differently:
- Archive: hides the entry from the default list and blocks release, but keeps it restorable. Use this to disable an entry without starting a deletion countdown.
- Delete: starts a recovery countdown toward permanent removal.
# Show all vault entries (metadata only — no secrets)
pnm vault list
# Show a single entry's metadata
pnm vault get vault_abc123
# Archive an entry — hides it and blocks release, but keeps it restorable
pnm vault archive vault_abc123
# Restore an archived entry to active
pnm vault unarchive vault_abc123
# Soft-delete an entry — the output prints the recovery deadline
pnm vault delete vault_abc123
# Restore a soft-deleted entry before the recovery deadline
pnm vault restore vault_abc123
# Permanently remove an entry (no recovery)
pnm vault purge vault_abc123Confirm
Test 1: the entry is stored and listed
pnm vault list --context my-appThe entry you created in Step 3 appears, with the id you passed to pnm vault release.
Test 2: releasing the entry returns the stored credential
pnm vault release vault_abc123Released secret (cleartext):
{
"kind": "bearer-token",
"token": "<your-token>"
}The kind matches the secretKind you set in entry.json, and the payload fields match what you stored in secret.json.
Test 3: a DID outside the context is refused
Run the same release as a DID that is not enrolled in the entry’s context. The VTA refuses it rather than returning ciphertext the caller cannot open, which confirms the context scope is doing the work rather than the encryption alone:
pnm --vta <other-vta-slug> vault release vault_abc123Expect an authorisation failure. Enrol that DID with Grant and revoke access if it genuinely needs the credential.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
pnm vault upsert fails with a schema validation error | The kind in secret.json does not match secretKind in entry.json, or a required field for that kind is missing. | Verify the kind values match exactly. See Supported secret kinds for required fields. |
vault release returns 403 Forbidden | The caller’s DID is not enrolled in the context that owns this entry. | Run pnm acl get <your-did> to check your context scope. Check the owning context with pnm vault get <entry-id>. |
| Entry not found after upsert | The entry ID from the upsert output was not recorded before closing the terminal. | Run pnm vault list to find the entry by label. |
| Soft-deleted entry cannot be restored | The recovery deadline printed at delete time has passed. | Use pnm vault purge to permanently remove it, then re-create the entry with pnm vault upsert. |
| Service receives the old credential after rotation | The service cached the previous release output at deploy time. | Ensure the service calls pnm vault release on startup or on-demand rather than caching the value once. |
vault release prints (no didcomm-authcrypt sealedSecret in response) or returns a raw JSON blob | The VTA requires DIDComm transport for vault operations; a REST-only connection cannot open the sealed response. | Run pnm --transport didcomm vault release <id> to force DIDComm, or verify the VTA has a DIDComm mediator configured. |
Next steps
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.