Roles and access

How callers authenticate to a WebVH Hosting control plane, what the Admin, Owner, and Service roles can each do, how ACL entries scope owners to domains and quotas, and how sessions expire.

A hosting server that several applications and tenants share needs to keep each of them to its own DIDs, its own domains, and a fair share of storage, while operators keep full control.

WebVH Hosting does this with one access control list (ACL). Every management request is a signed Trust Task document, sent over HTTPS, DIDComm v2.1, or TSP, and the signer’s ACL entry sets its role, the domains it may publish on, and optional quotas. Deploy the unified daemon →

How callers authenticate

Each mechanism below issues the same kind of token: a short-lived JSON Web Token (JWT) access token with a refresh token.

MechanismWhen it applies
Challenge-responseVTAs, applications, and backend services, for example the management UI’s wallet sign-in. The caller signs an auth/authenticate Trust Task that carries a challenge from /api/auth/challenge. A VTA-proxied sign-in can carry a SIOPv2 ID token that the caller’s VTA signs.
Passkey (WebAuthn)Human operators signing in to the management UI. An admin creates an enrolment invite with did-hosting-daemon invite --did <DID> --role <admin|owner>, did-hosting-control invite, or from the management UI. The invite is an enrolment URL plus a claim code, sent over separate channels. Redeeming both registers the passkey and adds the DID to the ACL if it is not already there. Requires public_url to be set.
VTA WalletHuman operators signing in to the management UI with their VTA instead of a passkey.

Every Trust Task, on any transport, is checked against the ACL: a signer with no ACL entry is refused.

A watcher is outside this model and has no ACL. A standalone watcher applies a signed sync only when its signer is listed in [sync] source_dids. A watcher embedded in the unified daemon reads the daemon’s own store instead.

What each role can do

Roles are stored in each ACL entry as admin, owner, or service.

RoleWho it is forWhat it can do
AdminOperatorsFull management: act on any DID, including the root .well-known DID, and manage the ACL, hosting domains, the service registry, and passkey invites.
OwnerDID owners, such as applications or tenantsCreate, update, publish, disable and enable, roll back, transfer, and delete their own DIDs, and manage agent names and witness files for them. Limited to the domains and quotas set on their ACL entry. Cannot manage the ACL, domains, or registry.
ServiceBackend service accounts, such as a standalone edge serverRegister with the control plane and push usage statistics. Cannot use admin routes or DID management routes.

When a unified daemon is set up in VTA-managed mode, the provisioning VTA is added to the ACL automatically as an admin with access to all domains. It can then create and manage DIDs on your behalf. DIDs your VTA creates are owned by the VTA’s DID.

The acl/revoke and acl/change-role Trust Tasks refuse any revocation or role change that would leave no admin, with last_authority_protected. The remove-acl command edits the ACL store directly and skips this check, so confirm another admin remains before you remove one with it.

How owners are scoped to domains and quotas

An ACL entry carries more than a role. Its fields are did, role, an optional label, a domain scope, and optional quotas.

The domain scope applies to owners only, because admins and service accounts can act on every domain. It takes one of three forms:

  • All domains: the owner can publish on any hosting domain on the server.
  • Allowed domains: the owner can publish only on the listed domains, and must name one on every request.
  • Allowed domains with a default: as above, but requests that name no domain use the entry’s default.

An owner added through the acl/grant Trust Task, a signed request document sent to the control plane’s /api/trust-tasks endpoint, must carry a domain scope, and a grant without one is refused with malformedRequest. In the management UI, a new entry’s scope starts at All. An owner added with the add-acl command, or created by redeeming a passkey invite, can publish on all domains. For how hosting domains work, see Hosting domains.

Quotas cap how much an owner can store. The control plane and the unified daemon check them when an owner creates or publishes a DID. An owner whose entry sets no quota has no cap.

QuotaLimits
max_did_countNumber of DIDs.
max_total_sizeTotal size of the owner’s DID content, in bytes.

Admins are exempt from quotas. A request that would exceed a quota is refused with 403 and a message such as DID count limit reached (<limit>).

In the management UI, these fields appear on the Access page as Domain scope, Default domain, Max DIDs, and Max size (MB).

How sessions and tokens expire

By default, a challenge is valid for 30 seconds, an access token for 15 minutes, and a session can be refreshed for 24 hours. An enrolment invite is valid for 24 hours. Each of these is configurable under [auth].

A caller’s role is read from the ACL on every request:

  • ACL changes apply to the caller’s next request.
  • Signing out of the management UI ends the session on the server, so its access token stops working immediately.

Challenge requests are rate limited to 30 per minute from each IP address. Behind a reverse proxy, set trusted_proxies under [server] so the limit applies to the real client address instead of the proxy’s.

Which operations need a passkey

Signing in with a passkey gives a session a higher assurance level (AAL2) than challenge-response authentication (AAL1). A session can raise its level mid-session through a passkey or VTA step-up. Deleting a hosting domain is the one control plane operation that requires the higher level, and is refused until the session steps up.

  Hosting domains

  DID lifecycle

  Create your first hosted DID