Grant and revoke access

Grant and revoke access roles, scope permissions to contexts, set expiry, and end an existing session immediately using pnm acl commands.

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.
Incoming requestauthenticated as a DIDACL lookupEntry foundNo entryRole and context scopedetermine what's allowed.Request is rejected, regardless ofhow the DID was created.Every remote caller needs an ACL entry,whether pnm bootstrap provisioned it or you added it with pnm acl create.

Prerequisites

  • A running VTA. See Quickstart.
  • pnm CLI connected to your VTA (pnm vta info returns without error).
  • admin or initiator role to list, create, or delete entries. Changing an entry with pnm acl update or pnm acl change-role needs admin. Context-scoped admins can only manage entries within their allowed contexts.
  • To end sessions immediately: Rust 1.95 or later, and a vta-sdk client 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.

RoleWho it’s forWhat it can do
adminOperators, context ownersFull management: create keys, manage ACL, sign, read
initiatorTrusted services that onboard othersCreate and delete non-admin ACL entries. Mint keys and sign within allowed contexts
applicationApps and servicesSign, read keys and contexts within allowed contexts
readerRead-only integrationsRead-only: list keys, contexts, DIDs
monitorInfrastructure health checksRead-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-app

Getting 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  24h

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

An 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.

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.

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-app

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

The 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

SymptomLikely causeFix
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 entriesThe 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 stateThe 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-roleRole 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 defaultACL 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 authorityThe 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 contextThe 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

  Sign application payloads without exposing your keys: the bootstrap provisioning flow creates the ACL entry automatically.

  Retrieve credentials from the VTA vault at runtime: ACL context scope determines which services can release a vault entry.

  Back up and restore VTA state: ACL entries are included in VTA backups.