Rotate an application credential

Replace a compromised, stale, or expiring application credential without re-provisioning the app or changing its context.

By the end of this guide, your application authenticates to the VTA with a freshly minted credential, and the old one no longer works.

This guide re-runs the same sealed-transfer bootstrap used to provision the application in the first place, issuing a new DID and private key for the same context and role. The context, its keys, and the application’s configuration are untouched. For how that delivery works, see Sealed transfer.

A credential that has leaked stays valid until you replace it and remove the old DID from the access control list. Rotation is also the routine answer to a credential reaching the end of its planned lifetime, rather than waiting for it to fail in production.

Use this guide when:

  • An application credential has been exposed, or you suspect it has.
  • A credential is due for rotation under your own key-rotation policy.
  • A service is being handed to a different team and should not keep the old identity.

You do not need this guide to rotate an admin DID bound to an integration such as a mediator or WebVH host; see Rotate an integration’s admin DID instead.

Prerequisites

  • A running VTA. See Quickstart.

  • Install Rust 1.95 or later on your machine (required for compiling the Personal Network Manager (PNM) CLI).

  • pnm installed and connected to the VTA.

    cargo install pnm-cli@0.16.4 --locked --registry crates-io
  • Admin access to the context the credential is scoped to.

  • The old credential’s DID, visible via pnm acl list --context <context-id>.

Step 1: Generate a new bootstrap request

Run this on a trusted machine. The generated private key stays local.

pnm bootstrap request --out rotation-request.json

Step 2: Create the new app credential

Note the SHA-256 digest printed to stdout. You need it in Step 3.

pnm auth-credential create \
    --role      application \
    --contexts  <context-id> \
    --label     "<your-app>-rotated" \
    --recipient rotation-request.json > rotation-bundle.txt

Step 3: Open the bundle

pnm bootstrap open \
    --bundle        rotation-bundle.txt \
    --expect-digest <digest-from-step-2> \
    --out           credential-new.json

Step 4: Update your secrets manager

Replace the existing credential.json secret with the contents of credential-new.json. Restart the service to load the new credential.

Step 5: Revoke the old credential

Run this once the service is authenticating with the new credential, confirmed below. The old DID stays active until you remove it. If the credential was compromised, treat this as urgent rather than routine.

# Find the old DID if you do not have it recorded
pnm acl list --context <context-id>

# Revoke it
pnm acl delete <old-did>

Any token issued to the old DID is rejected at its next refresh, within the access token’s session lifetime of 15 minutes.

Confirm

Test 1: the service authenticates with the new credential

Trigger a request from the service that exercises the VTA, then check the new DID holds the role and context you expect:

pnm acl get <new-did>
DID:              did:key:z6Mk...
Role:             application
Contexts:         my-app

Run this before Step 5. Revoking the old credential while the new one is not yet working takes the service offline.

Test 2: the old DID is gone from the ACL

pnm acl list --context <context-id>

The old DID is absent and the new one is present. A token the old DID already holds keeps working until it expires, up to 15 minutes.

Next steps

  Grant and revoke access: manage roles and context scope for all DIDs registered with the VTA.

  Sign application payloads without exposing your keys: the initial provisioning flow this guide re-runs.