Retrieve credentials from the VTA vault at runtime

Store any credential type in the VTA vault and deliver it to authorised services at runtime using the pnm CLI.

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 a custom entry.

Every release follows the flow below:

Serviceauthenticated as a DIDVTA Vaultholds the secretvault_release(entryId)sealed secretSealed to the caller:only this service's key can open the released secret.ServiceSecret in memoryreleased to memorydecrypted for usecleared after useReleased to memory:the secret reaches your service atruntime, where your own handling keeps it out of disk and logs.

Supported secret kinds

The vault stores typed secrets. Choose the kind that matches your credential:

secretKindWhat it storesTypical use
bearer-tokenA token + optional header name and prefixGitHub PATs, API keys, JWT access tokens
passwordUsername + passwordInternal tools, legacy APIs
oauth-tokensOAuth refresh token + optional access tokenGoogle, Slack, and other OAuth providers
ssh-keySSH private key + optional passphraseGit servers, remote hosts
passkeyWebAuthn credential ID + private keyPasskey-based authenticators
did-self-issuedA DID + its signing key IDSelf-issued DID credentials
didcomm-peerA did:peer + its signing key IDDIDComm peer relationships
customAny named fields you defineOAuth 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 application role (or higher) in the target context. See Grant and revoke access.
  • pnm CLI connected to your VTA (pnm vta info returns 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 }
  ]
}

Step 3: Store the entry

pnm vault upsert --entry-file entry.json --secret-file secret.json

The 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:

  1. Checks that the caller’s DID is enrolled in the context that owns the entry (the contextId set in entry.json).
  2. 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_abc123

Output:

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:

  1. Create a new secret.json with the replacement credential.
  2. Add "id": "vault_abc123" to entry.json to target the existing entry.
  3. Run the upsert again:
pnm vault upsert --entry-file entry.json --secret-file new-secret.json

The 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_abc123

Confirm

Test 1: the entry is stored and listed

pnm vault list --context my-app

The 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_abc123
Released 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_abc123

Expect an authorisation failure. Enrol that DID with Grant and revoke access if it genuinely needs the credential.

Troubleshooting

SymptomLikely causeFix
pnm vault upsert fails with a schema validation errorThe 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 ForbiddenThe 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 upsertThe 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 restoredThe 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 rotationThe 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 blobThe 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

  Keep API tokens out of your service config: a practical example using bearer-token with the GitHub API.

  Grant and revoke access: control which DIDs can release secrets from which contexts.

  Back up and restore VTA state: vault secrets are currently excluded from VTA backups and must be re-stored after a restore. Keep a secure copy of your secrets outside the vault for disaster recovery.