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 keeps the credential out of the service’s configuration. The service holds only an entry ID and calls the vault/release Trust Task to request it. Each request is:

  • Encrypted to the calling service’s own DID with DIDComm authcrypt.
  • Recorded in the audit trail.

For what the vault stores and which roles can reach it, see Secrets vault. The VTA encrypts each released secret to the caller’s DID with DIDComm authcrypt, so a relay carrying the response cannot read it, and releasing a secret needs a DIDComm connection.

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 admin or initiator role in the target context, which storing and managing entries needs. The service that releases the secret at runtime needs only the application role. 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 callers whose role allows release (application, initiator, or admin) and whose scope covers that context can release this entry, the same isolation model used for signing keys. A super-admin’s scope covers every context. See Contexts for how context isolation works across a VTA. targets records the intended destination on the entry. The audit trail records which DID released which entry, and in which context.

{
  "contextId": "my-app",
  "targets": [
    { "kind": "web-origin", "origin": "https://example.com" }
  ],
  "label": "Example API key",
  "secretKind": "bearer-token"
}

targets is an array of the destinations the secret is meant for. Use "kind": "web-origin" for HTTP APIs and web services, or "kind": "did" when the target is identified by a DID. The ios-app and android-app kinds also exist for mobile apps.

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": {
    "contextId": "my-app",
    "createdAt": "2026-08-12T10:00:00Z",
    "createdBy": "did:key:z6Mk...",
    "id": "vault_abc123",
    "label": "Example API key",
    "secretKind": "bearer-token",
    "targets": [
      {
        "kind": "web-origin",
        "origin": "https://example.com"
      }
    ],
    "updatedAt": "2026-08-12T10:00:00Z",
    "updatedBy": "did:key:z6Mk...",
    "version": 1
  }
}

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 role allows release, that its scope covers the entry’s context (the contextId set in entry.json), and that the context’s policy and any step-up requirement allow it. It then encrypts the secret to the caller’s DID with DIDComm authcrypt.

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, without secret values)
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 when the VTA sets one
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

echo '{"contextId": "my-app"}' | pnm vault list --filters-file -

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 with a credential whose DID is enrolled on this VTA, but not in the entry’s context, for example a second PNM registration of the same VTA under its own slug. 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 <second-slug> vault release vault_abc123

Expect the release to fail with vault scope denied: caller is not authorised for context my-app. 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 fails with vault scope denied: caller is not authorised for context <id>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 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 upsert or vault release fails with … requires the DIDComm transportVault secrets are sealed with DIDComm authcrypt, and PNM connected over REST or TSP.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: an encrypted backup carries the vault, so its entries come back after a restore.