# 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](/products/affinidi-elements/vta/concepts/roles-and-access.md).

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](/products/affinidi-elements/vta/get-started.md).

- pnm CLI connected to your VTA (pnm vta info returns without error).

- admin or initiator role for the DID you’re managing entries with. Context-scoped admins can only manage entries within their allowed contexts.

## 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](/products/affinidi-elements/vta/concepts/roles-and-access.md) 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 | Manage ACL entries. 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 only. No access to keys, contexts, or DIDs |

admin and initiator can both manage ACL entries. Only admin can assign the admin role. Context creation needs more than the admin role itself: it requires a super-admin entry specifically, so a context-scoped admin can’t create contexts even though it holds the admin role.

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.

```bash
# 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.

```bash
pnm acl get 
```

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

```bash
pnm acl create \
    --did      did:key:z6Mk... \
    --role     application \
    --contexts my-app \
    --label    "my-app service account"
```

### Multiple contexts

```bash
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 that auto-expires if never claimed. Accepts N[s|m|h|d|w]:

```bash
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, with no automatic handoff to a permanent entry. 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.

```bash
# Change context scope
pnm acl update  --contexts my-app,my-other-app

# Change label
pnm acl update  --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.

```bash
pnm acl change-role \
    --did   \
    --from application \
    --to   reader
```

An optional --reason is recorded in the audit log:

```bash
pnm acl change-role \
    --did     \
    --from   admin \
    --to     application \
    --reason "retiring operator account"
```

## Revoking access

Removes the DID from the ACL immediately. The caller must be an admin. Context-scoped admins can only delete entries within their allowed contexts.

```bash
pnm acl delete 
```

Deletion is immediate and permanent, and it stops the DID opening any new session. A JWT the DID is already holding is separate: it keeps working until it naturally expires, per the [session lifetime](/products/affinidi-elements/vta/concepts/request-model.md#session-lifetime) every access token has. Revoking the session as well is what closes that window, and the next section covers it.

### Ending existing sessions immediately

The VTA checks the session record on every authenticated request, so a revoked session stops working on the DID’s very next call rather than when its token would have expired.

pnm has no session subcommand yet, so these are direct REST calls. Use your own admin session token to authenticate them:

```bash
export VTA_TOKEN=$(pnm --vta  auth show-token)
```

See which sessions the VTA is holding. A super-admin sees every session; a context-scoped admin sees those belonging to DIDs in their own contexts:

```bash
curl -s "$VTA_URL/v1/auth/sessions" \
    -H "Authorization: Bearer $VTA_TOKEN"
```

Revoke every session one DID holds. This is the call to reach for after removing an ACL entry, because it covers all of that DID’s sessions without you having to identify each one:

```bash
curl -s -X DELETE "$VTA_URL/v1/auth/sessions?did=" \
    -H "Authorization: Bearer $VTA_TOKEN"
```

```json
{ "revoked": 2 }
```

To end one specific session instead, name its ID from the listing above:

```bash
curl -s -X DELETE "$VTA_URL/v1/auth/sessions/" \
    -H "Authorization: Bearer $VTA_TOKEN"
```

A single session can also be revoked as the auth/revoke-session/0.1 Trust Task over DIDComm. That task revokes exactly the one session you name, so the REST query-parameter form above is the one that covers a DID’s whole set.
Revoke in the order your role supports

As a super-admin, delete the ACL entry first and then revoke the DID’s sessions. The deletion closes off new sessions, so the revocation that follows has nothing left to race against.

As a context-scoped admin, revoke the sessions first and then delete the entry. Session revocation is scoped by what the ACL makes visible to you, so it needs the DID’s entry to still be present. Should an entry already be gone while sessions remain, a super-admin can clear them.

## Common patterns

Reference snippets for the scenarios you’ll encounter most often. See [Sign application payloads without exposing your keys](/products/affinidi-elements/vta/integration-guides/provisioning-app-signing.md) for the pnm bootstrap flow that creates an app’s ACL entry automatically.

### Granting a second admin to an existing context

```bash
pnm acl create \
    --did      did:key:z6MkSecondAdmin... \
    --role     admin \
    --contexts my-app \
    --label    "backup admin"
```

### Granting read-only access for monitoring

```bash
pnm acl create \
    --did      did:key:z6MkMonitor... \
    --role     reader \
    --contexts my-app \
    --label    "read-only monitoring"
```

### Temporary access for onboarding

```bash
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

```bash
pnm acl get did:key:z6Mk...
```

```text
DID:              did:key:z6Mk...
Role:             application
Label:            my-app service account
Contexts:         my-app
Created At:       2026-09-16 10:24:07 +08:00
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

```bash
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

```bash
pnm acl delete did:key:z6Mk...
pnm acl list --context my-app
```

The deleted DID is absent from the listing. If it was the only entry in that context, the listing reports No ACL entries found instead.

A token the DID already holds keeps working until it expires. See [Revoking access](#revoking-access) for ending the session immediately.

## Troubleshooting

| Symptom | Likely cause | Fix |
| 409 Conflict when running pnm acl create | 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 allowed_contexts. | 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 rejected with a compare-and-swap error | 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 ... is rejected | Role changes must go through the compare-and-swap flow, not pnm acl update. | Use pnm acl change-role --did <did> --from <current-role> --to <new-role> instead. |
| Revoked DID continues to authenticate for up to 15 minutes | ACL deletion closes off new sessions, and a JWT the DID already holds stays valid until it expires. | Revoke the DID’s sessions as well, with DELETE /auth/sessions?did=<did>. See [Ending existing sessions immediately](#ending-existing-sessions-immediately). |
| Session revocation returns cannot revoke sessions for a DID outside your contexts | Session revocation is scoped by the ACL entries visible to a context-scoped admin, and this DID’s entry has already been deleted. | Ask a super-admin to clear the remaining sessions. In future, revoke sessions before deleting the entry. |
| forbidden when managing entries 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

  [Sign application payloads without exposing your keys](/products/affinidi-elements/vta/integration-guides/provisioning-app-signing.md): the bootstrap provisioning flow creates the ACL entry automatically.

  [Retrieve credentials from the VTA vault at runtime](/products/affinidi-elements/vta/vta-management/vault-deliver-secrets.md): ACL context scope determines which services can release a vault entry.

  [Back up and restore VTA state](/products/affinidi-elements/vta/vta-management/backup-and-restore.md): ACL entries are included in VTA backups.
