Rotate an integration's admin DID
By the end of this guide, an admin-scoped integration holds a freshly minted admin DID, and its previous one no longer has access.
Use this when an admin-scoped integration’s admin DID (a mediator, a WebVH host, or another admin-scoped integration) is compromised, lost, or due for rotation. Rotating this admin DID isn’t a standalone operation: it rides along with re-running the same provision-request / provision-integration flow used to provision the integration in the first place, this time passing --admin-template vta-admin. The VTA mints a fresh admin did:key, binds it to the integration’s context, and updates the access control list (ACL), while re-rendering the integration’s own DID material in the same bundle.
This flow always grants the admin role, scoped to the integration’s context, and re-renders the integration’s own DID material at the same time. Plan for both to change together: the admin DID rotates as part of re-provisioning the integration, rather than on its own.
Use this for admin-scoped integrations, such as a mediator or WebVH host.
For a least-privilege application identity that only needs to sign, use auth-credential create --role application instead, and see Rotate an application credential to replace one.
Use this guide when:
- An integration’s admin private key has been exposed, or you suspect it has.
- An integration’s admin DID is due for rotation under your own key-rotation policy.
- You are migrating an integration to new infrastructure and want it to present a new admin identity.
You do not need this guide for an application credential that only signs or reads; see Rotate an application credential instead.
When to use this
| Situation | What to do |
|---|---|
| Compromised admin private key | Re-run provisioning with --admin-template vta-admin: revoke old, mint new |
| Routine key rotation policy | Same re-run |
| First provisioning (setup to long-term DID) | Already handled by passing --admin-template vta-admin on the integration’s initial provision-request |
| Lost admin DID, no access to VTA | A super-admin must run pnm acl create with a new DID manually |
How it works
The VTA mints a fresh admin did:key and delivers it, together with the integration’s re-rendered DID material, as a sealed bundle, the same mechanism every credential-bearing VTA operation uses. The steps below walk through it: generate a request, send it, hand the bundle to the integration’s own setup tooling, then revoke the old admin DID once the new one is confirmed working.
Prerequisites
A running VTA. See Quickstart.
Install Rust 1.95 or later on your machine.
pnminstalled and connected to the VTA.cargo install pnm-cli@0.16.4 --locked --registry crates-ioAdmin access scoped to the integration’s context, or super-admin access to the VTA.
Step 1: Generate a new provision request
Use the same --template this integration was originally provisioned with (for example didcomm-mediator or did-hosting-control), and add --admin-template vta-admin to roll the admin DID over in the same request.
pnm bootstrap provision-request \
--template <integration-template> \
--admin-template vta-admin \
--context-hint my-app \
--label my-app-admin-rotation \
--out my-app-rotation-req.vp.jsonStep 2: Send the request to the VTA
Note the SHA-256 digest from stdout. Keep it out-of-band from the bundle file.
pnm bootstrap provision-integration \
--request my-app-rotation-req.vp.json \
--context my-app \
--out my-app-rotation-bundle.armorThe VTA re-renders the integration’s DID material from --template, mints a new admin did:key under its own key custody because --admin-template was set, binds authorisation to it, and adds it to the ACL. The old admin DID is left in the ACL until you remove it in Step 5.
Step 3: Verify the bundle
This bundle carries the integration’s re-rendered DID material and the new admin authorisation together, not a standalone admin credential, so pnm bootstrap open --out <path> refuses to write it out as a plain credential file. Inspect and verify it instead:
pnm bootstrap open \
--bundle my-app-rotation-bundle.armor \
--expect-digest <digest-from-step-2>This checks the digest against the value from Step 2, verifies the authorisation VC, and prints a summary: the template name, how many DIDs it carries key material for, the template outputs, and the VTA URL. A digest mismatch stops here rather than printing anything.
Step 4: Install the new credentials
pnm bootstrap open prints the payload; it does not write it into the integration’s keystore. That install step is specific to each integration and belongs to its own setup tooling, the same tooling that consumed the bundle when the integration was first provisioned. An integration built on the VTA SDK does this through the SDK’s provisioning client rather than by hand.
Whatever performs the install, these are the fields it must place:
| Field in the bundle | Where it goes |
|---|---|
secrets[<integration-did>] | The integration’s own signing and key-agreement keys, into its keystore. |
secrets[<admin-did>] | The new long-term admin DID keys, persisted as the integration’s admin identity. This is the half that completes the rotation. |
outputs (for a did:webvh template) | The did.jsonl log content, saved to disk and published at the URL supplied in Step 1. |
| VTA trust bundle | The VTA DID, root key, and context ID, so the integration trusts inbound DIDComm from this VTA. |
| The authorisation VC | Archive it for audit. The VTA’s ACL is the authority in steady state, so this credential is never re-verified after the first open. |
Once the install completes, tokens issued to the previous admin DID expire within 15 minutes. After that, any call using the old DID returns 401.
my-app-rotation-bundle.armor carries HPKE-sealed admin key material, and my-app-rotation-req.vp.json is bound to the ephemeral seed that opens it. Delete both once the integration has installed the credential, and keep both out of source control.
Keep the digest from Step 2 on a separate channel from the bundle. A digest travelling alongside the file it is meant to vouch for confirms nothing, because anyone who can replace the bundle can replace the digest with it.
Rotate immediately, rather than at the next scheduled rotation, if the bundle or the seed directory is ever exposed.
Step 5: Revoke the old admin DID
The old admin DID is not removed automatically. It retains ACL access until you delete it.
pnm acl delete <old-admin-did>Once deleted, any token issued to the old DID is rejected at its next refresh, within 15 minutes. If the key was compromised, do this immediately.
Confirm
Test 1: the new admin DID is in the ACL
pnm acl list --context my-appBoth the new and the old admin DID appear at this point. Run this before Step 5, so you confirm the replacement exists before removing what it replaces.
Test 2: the integration operates with the new admin DID
Exercise an admin-scoped operation through the integration itself, such as the action its setup tooling performs on first boot. It completes without re-authenticating as the old DID.
Test 3: the old admin DID is gone
pnm acl list --context my-appThe old admin DID is absent and the new one remains.
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.