Grant and revoke access
By the end of this guide, you can grant a DID access to a context, change what it is allowed to do there, and revoke it when it should no longer have access.
Every DID that calls the VTA holds an access control list (ACL) entry assigning it a role and, usually, a set of allowed contexts. Managing that list is how you control which applications, services, and operators reach the VTA, and what each one may do once it gets there. For what the five roles mean and how role and context bound each other, see Roles and access.
A DID with no entry is refused regardless of how it was created, so access is something you grant deliberately rather than something a valid key confers on its own. Provisioning an application with pnm bootstrap writes that entry for you, which leaves this guide for the identities bootstrap did not create and for every change after the first one.
Use this guide when:
- You need to grant access to a DID that was not provisioned through
pnm bootstrap. - A service’s role or context scope needs to change.
- A credential has been exposed, or a person or service should no longer have access.
- You want to give someone access that expires on its own rather than relying on a later cleanup.
Prerequisites
- A running VTA. See Quickstart.
pnmCLI connected to your VTA (pnm vta inforeturns without error).adminorinitiatorrole to list, create, or delete entries. Changing an entry withpnm acl updateorpnm acl change-roleneedsadmin. Context-scoped admins can only manage entries within their allowed contexts.- To end sessions immediately: Rust 1.95 or later, and a
vta-sdkclient connected with an admin or initiator credential, as described in Ending existing sessions immediately.
Roles
Every ACL entry carries one role. The role determines which endpoints the caller can reach once authenticated. Choose the least-privileged role that covers the caller’s needs. See Roles and access for why the VTA has five roles instead of one shared credential.
| Role | Who it’s for | What it can do |
|---|---|---|
admin | Operators, context owners | Full management: create keys, manage ACL, sign, read |
initiator | Trusted services that onboard others | Create and delete non-admin ACL entries. Mint keys and sign within allowed contexts |
application | Apps and services | Sign, read keys and contexts within allowed contexts |
reader | Read-only integrations | Read-only: list keys, contexts, DIDs |
monitor | Infrastructure health checks | Read-only: metrics and health endpoints, and listing the contexts it can access. No access to keys, DIDs, or the vault. pnm cannot assign it: pnm acl create and pnm acl change-role accept only the four roles above |
admin and initiator can both manage ACL entries. Only admin can assign the admin role. Creating a top-level context needs more than the admin role itself: it requires a super-admin entry. A context-scoped admin can still create sub-contexts inside a context it administers.
An admin entry created without --contexts is a super-admin: it carries no context restriction and can see and manage every entry across the VTA. Scope --contexts on every other admin entry to limit its reach.
Listing ACL entries
Returns every ACL entry visible to your role. Context-scoped admins only see entries in their allowed contexts. Super-admins see all.
# All entries
pnm acl list
# Entries scoped to a specific context
pnm acl list --context my-appGetting an entry
Shows full details for a single entry: role, label, context scope, created-at, and who created it.
pnm acl get <did>Granting access
Creates a new ACL entry. The command is not idempotent. If an entry for the DID already exists, the server returns 409 Conflict. Use pnm acl update to change an existing entry.
Basic grant
pnm acl create \
--did did:key:z6Mk... \
--role application \
--contexts my-app \
--label "my-app service account"Multiple contexts
pnm acl create \
--did did:key:z6Mk... \
--role application \
--contexts my-app,my-other-app \
--label "shared service account"Temporary / expiring access
Use --expires to create a setup ACL entry that the VTA removes once its deadline passes, whether or not the DID used it. Accepts N[s|m|h|d|w]:
pnm acl create \
--did did:key:z6Mk... \
--role admin \
--contexts my-app \
--label "bootstrap admin (expires 24h)" \
--expires 24hThe VTA’s ACL background task removes the entry once its deadline passes. To let the DID roll over once to a long-term successor before then, add --handoff to the command; it needs --expires. This rollover is available on Isolated and self-hosted VTAs. A Shared VTA rolls a hand-off entry over only when its DID provisions an integration with pnm bootstrap provision-integration. Otherwise, grant the DID a permanent entry with pnm acl create before the temporary one expires, if you want it to keep working past that deadline.
Updating an entry
Changes one or more fields on an existing entry. Only supply the fields you want to change. Omitted fields are left as-is.
# Change context scope
pnm acl update <did> --contexts my-app,my-other-app
# Change label
pnm acl update <did> --label "updated label"Changing a role
Role changes require a compare-and-swap: you must supply the role the entry currently holds (--from) so that a concurrent admin change is surfaced rather than silently overwritten. Re-read with pnm acl get and retry if the server rejects the change.
pnm acl change-role \
--did <did> \
--from application \
--to readerAn optional --reason is recorded in the audit log:
pnm acl change-role \
--did <did> \
--from admin \
--to application \
--reason "retiring operator account"Revoking access
Removes the DID from the ACL immediately. The caller must be an admin or initiator. An initiator can delete only entries below the admin role, every caller keeps its own entry, and context-scoped callers can only delete entries within their allowed contexts.
A REST session the DID already holds outlives the deletion, so pair the delete with a session revocation, in the order that fits your role:
- Super-admin: delete the entry first, then revoke the DID’s sessions as described in Ending existing sessions immediately. Once the entry is gone the DID can open no new session, so nothing slips in between the two calls.
- Context-scoped admin or initiator: revoke the DID’s sessions first, then delete the entry straight away. Only a super-admin can revoke sessions once the entry is deleted. A session the DID opens between the two calls lasts until its access token expires, 15 minutes by default.
pnm acl delete <did>Deletion is immediate and permanent. The VTA checks every DIDComm message, and every TSP message on a self-hosted VTA with TSP enabled, against the ACL, so it refuses the DID’s next message on those transports, and the DID can open no new REST session. A REST access token (JWT) the DID is already holding is separate: it keeps working until it naturally expires, per the session lifetime every access token has. Revoking the DID’s sessions closes that window.
Ending existing sessions immediately
The VTA checks the session record on every request that carries an access token, so a revoked session stops working on the DID’s next REST call rather than when its token would have expired. DIDComm and TSP callers carry no token: deleting their ACL entry is what stops them.
pnm has no session subcommand yet, and session management is not a plain REST call: it runs as the auth/sessions/list/0.1 and auth/revoke-session/0.2 Trust Tasks, which the caller signs. Send them from the VTA SDK, with a client that authenticates as an admin or initiator with authority over the DID. To get one, issue a credential bundle and connect with it as in Steps 3 to 5 of Sign application payloads without exposing your keys, passing --role admin or --role initiator to pnm auth-credential create in place of --role application, and --contexts set to the contexts it needs to manage. Store that bundle as you would any admin secret, and delete its ACL entry with pnm acl delete <credential-did> once you no longer need it.
The code in this guide uses vta-sdk, the Rust client library for the VTA, published on crates.io with API documentation on docs.rs. pnm is built on the same crate. From another language, connect through the MCP bridge, or send signed Trust Task documents to the VTA’s HTTPS endpoint yourself, as described in Which client to use for your language.
Revoke every session one DID holds. This covers all of that DID’s sessions without you having to identify each one, and returns the number of sessions it ended:
use vta_sdk::client::RevokeSessions;
let revoked = client
.revoke_sessions(RevokeSessions::Subject("<did>"), Some("access removed"))
.await?;
println!("revoked {revoked} sessions");RevokeSessions::Session("<session-id>") ends one named session instead, and RevokeSessions::AllMine ends every session of the caller itself. client.list_my_sessions() lists only the caller’s own sessions: no task enumerates another DID’s sessions, so revoke by DID rather than looking them up.
A caller can always end its own sessions. Ending another DID’s sessions needs an admin or initiator role that can see that DID’s ACL entry: an initiator is limited to entries below the admin role, and a context-scoped caller to entries that share at least one of its contexts, which excludes every super-admin entry. Deleting the entry needs more: the caller must cover every context the entry acts in. A DID whose ACL entry is already deleted belongs to no context, so only a super-admin can revoke its sessions afterwards.
Common patterns
Reference snippets for the scenarios you’ll encounter most often. See Sign application payloads without exposing your keys for the pnm bootstrap flow that creates an app’s ACL entry automatically.
Granting a second admin to an existing context
pnm acl create \
--did did:key:z6MkSecondAdmin... \
--role admin \
--contexts my-app \
--label "backup admin"Granting read-only access for monitoring
pnm acl create \
--did did:key:z6MkMonitor... \
--role reader \
--contexts my-app \
--label "read-only monitoring"Temporary access for onboarding
pnm acl create \
--did did:key:z6MkTemp... \
--role admin \
--contexts my-app \
--expires 7d \
--label "setup DID (expires in 7 days)"Confirm
Test 1: the entry returns the role and contexts you granted
pnm acl get did:key:z6Mk...Name: my-app service account
DID: did:key:z6Mk...
Role: application
Contexts: my-app
Capabilities: (none granted by name — everything `application` allows)
Created At: 1789525447
Created By: did:key:z6MkAdmin...Role and Contexts are the two fields that decide what this DID can reach. An admin entry with no context restriction shows Role: super admin.
Test 2: the entry appears in the context listing
pnm acl list --context my-appThe DID from Test 1 appears in the listing. A context the DID is not scoped to omits it.
Test 3: after deletion, the DID no longer appears
pnm acl delete did:key:z6Mk...
pnm acl list --context my-appThe deleted DID is absent from the listing. If you are a super-admin, super-admin entries, your own included, still appear, because a super-admin can act in every context.
A token the DID already holds keeps working until it expires. See Revoking access for ending the session immediately.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
pnm acl create fails with ACL entry already exists for DID: <did> | An ACL entry for this DID already exists. | Use pnm acl update <did> to modify the existing entry, or pnm acl delete followed by pnm acl create to replace it. |
| Context-scoped admin cannot see all entries | The caller’s ACL entry lists specific contexts (the Contexts line of pnm acl get). | Check your own entry with pnm acl get <your-did>. Super-admin access is required to view entries outside your allowed contexts. |
pnm acl change-role fails with role of <did> is <role>, not <from> — the change was based on stale state | The entry’s current role differs from the value in --from. | Re-read the entry with pnm acl get <did> and retry with the correct --from value. |
pnm acl update <did> --role ... fails with an error pointing you to pnm acl change-role | Role changes must go through the compare-and-swap flow, not pnm acl update. | Run the pnm acl change-role command the error prints, which already includes the entry’s current role. |
| Revoked DID continues to authenticate until its access token expires, 15 minutes by default | ACL deletion closes off new sessions, and a JWT the DID already holds stays valid until it expires. | Revoke the DID’s sessions with the auth/revoke-session/0.2 Trust Task. Once the entry is deleted, only a super-admin can do this, so a context-scoped admin or initiator revokes first, as in Revoking access. See Ending existing sessions immediately. |
Revoking a DID’s sessions fails with permission denied: the named subject is outside your authority | The caller cannot manage that DID’s ACL entry: it is outside the caller’s contexts, holds a higher role, or was already deleted. | Use a super-admin credential. |
✗ Forbidden or ✗ Not found: ACL entry not found for DID: <did> when managing an entry in another context | The caller is context-scoped and the target entry is outside that scope. | Use a super-admin credential, or ask a super-admin to make the change. |
Next steps
Glad to hear it. Thanks for letting us know.
Sorry to hear that. Thanks for letting us know.