Actor assertions¶
Your browser never talks to the VirtualSutra API directly. The console has a server side of its own, and every console request passes through it. That server side holds one API credential for the whole deployment, which means the credential alone cannot say who a request is for - and a role check that judged the credential would pass for every signed-in user, viewer included.
An actor assertion is how the console tells the API who you are, in a form the API can verify.
You do not configure this and you cannot see it. It is documented here because it explains several things that are otherwise surprising: why AUTH_ADMIN_EMAILS is not what the API checks, why a direct API key behaves differently from the same person in the console, and why a console request can never grant itself more access than the deployment's console credential already has.
What is asserted¶
Five HTTP headers, all of them or none:
| Header | Value |
|---|---|
X-Actor-Email | your email, trimmed, lowercased and percent-encoded (RFC 3986 on the UTF-8 bytes of the NFC address, uppercase hex, every byte outside A-Z a-z 0-9 - . _ ~ escaped), so any address can travel in a header |
X-Actor-Org | the organization you are acting in (empty when no organization is selected) |
X-Actor-Role | your console session role, lowercased - viewer, operator, admin or platform_admin |
X-Actor-Timestamp | Unix seconds, base 10, unpadded |
X-Actor-Signature | HMAC-SHA256 over the four other fields (the email decoded), hex-encoded |
The signature covers this exact string, the five parts joined by single line feeds:
The email in the signed string is the decoded address. The older form (v1, the address sent as is) is still accepted for one release and then removed. An address containing a control, invisible or direction-changing character (such as a right-to-left override or a zero-width space) is rejected with 401 when it arrives in the current form, and so is a value that is not the one canonical encoding of an address (lowercase hex, an escaped unreserved character, bytes that are not UTF-8, an address that is not NFC).
The key is the console's own API credential - the same value it already presents as X-API-Key. There is no second secret to manage, no extra environment variable and no Helm value: a deployment that can reach the API at all can already sign assertions, and rotating the console's API key rotates the signing key with it.
What the API does with it¶
- All five headers, or none. A request carrying some but not all of them is rejected outright (
401 Incomplete actor assertion). Partial headers never fall back to the "no actor" path - that would be the way in for anyone trying to forge one. - The signature is checked first, before the timestamp. The window is only evaluated once the signature verifies, so without the key you only ever see the signature failure - nobody can use the difference between "bad signature" and "stale timestamp" to probe the deployment's clock before proving they hold the key.
- The timestamp must be within 300 seconds either side of now. A captured assertion stops working after five minutes.
- Your role is resolved from your own user record, not from the signed role. The signed role is only a fallback when no record can be read - an identity the deployment keeps no record for, or a database the API cannot reach. For a non-admin the role comes from your membership of the organization you are acting in, not from the account row. This is the part that matters: the API decides what you may do from what the deployment says you are.
- The result is capped by the presenting credential. Your effective scopes are the intersection of your role's scopes and the scopes of the credential the console presented. A browser session can never exceed its transport credential's own ceiling, whoever is signed in.
- Resolved scopes are cached for at most 5 seconds per API process. To spare the database a lookup on every poll of the console, the API keeps the scopes it resolved for you (your account, organization and signed role) for up to 5 seconds. Only a successful resolution is kept: an error while reading your record, a disabled account and a removed identity are never cached, so each is refused again on the next request. The cap of your credential is applied on every request and is not cached.
ACTOR_SCOPES_CACHE_TTL_Son the API sets the time; 5 is the most it accepts, and0turns the cache off. A change to your role, membership, tenant-admin standing or platform-admin standing, or disabling your account, takes effect at once on the API process that handled the change, and on every other API replica within 5 seconds. A change made outside the API (a script or a scheduled job writing to the database directly) is not signalled to any process and is seen within 5 seconds as well. If reading your organization membership fails, you are held at read-only access for at most 5 seconds before the next lookup. - Only the console may claim an identity. When a deployment names the console's key in its
CONSOLE_API_KEYSsetting, every other credential that sends anX-Actor-*header, or the older unsignedx-authenticated-userheader, is refused. A stolen integration key cannot pretend to be a person.
What this means in practice¶
AUTH_ADMIN_EMAILS assigns, it does not gate. Listing your email there gives your console session the admin role when you next sign in. What the API checks is the role it resolves for you through the assertion. The email allowlist survives as a fallback only for a console-style caller that sends no assertion at all - a deployment running a console build that predates this mechanism.
A direct API key is judged on its own scopes. Integrations, CI jobs, the CLI with a static key and service-account tokens send no assertion, and nothing changes for them: they are authorized by the scopes on the credential itself. This is why a personal API key you mint for a script can be narrower than what you can do in the console, and why it should be.
Your console role and your organization membership role are different things. The role in the assertion is your account-level console role. Organization membership is tracked separately, and inviting someone as "Admin" of an organization does not by itself change their console role.
When something is refused¶
| Response | What happened |
|---|---|
401 Incomplete actor assertion | Some of the five headers arrived without the rest. Almost always a proxy in front of the console stripping headers. |
401 Invalid actor assertion | The signature did not verify, or the email was not a valid encoding of an address or held a refused character. For a signature: the console's API key and the one the API accepts have drifted apart. Check that the console's key is still one of the API's accepted keys. |
401 Actor assertion expired | More than 300 seconds of clock skew between the console and the API, or a replayed request. Check NTP on both. |
401 Actor assertion required | The API was given the console's key by something that is not the console. See the API-key notes - an unattended caller needs its own machine key, not the console's. |
403 Admin access required - your account role does not have admin access on this deployment | The assertion verified, and the role it resolved for you is not admin. |
401 Identity headers are accepted from the console only | A credential that is not the console's sent an X-Actor-* header, or the older unsigned x-authenticated-user. Integrations act as themselves; they never claim a user. |
403 Account disabled | Your user record exists but is disabled. Removing someone soft-deletes that row, which is what does the revoking. |
Every one of these is a deployment-configuration symptom rather than something you can fix from the console. Send the exact message to whoever manages the deployment.
Related¶
- Tier & Scope Matrix - which role reaches which area
- Admin Console - the pages the
adminrole unlocks - OAuth / OIDC setup - where the console session role comes from