Roles and the audit log
The administrative roles in Nixt Server and what each may do, how a caller's role is decided, and the append-only audit log of every change.
Every call to the admin API is checked against the caller’s role before anything happens, and every change is written to an audit log in the same transaction as the change itself.
Two rules above every role
- Tenants are separate. Every role except the operator lives inside one tenant and cannot see or change anything in another. A request for another tenant’s resources is refused with
that belongs to another tenant. - Nobody reads mail through the API. No role, the operator included, can read message content through the administrative API. The queue view and the message trace show envelopes, hosts and replies, never what a message says.
The roles
| Role | Name in the log | Reach |
|---|---|---|
| Operator | operator | Everything, in every tenant, including the list of tenants and the whole server’s settings. |
| Tenant administrator | tenant-admin | Everything inside one tenant, except that the audit log is read-only. |
| Domain administrator | domain-admin | The domains they hold in one tenant, and the accounts in those domains. Read-only sight of the tenant. |
| Helpdesk | helpdesk | Look things up, reset passwords, unlock, sign somebody out, revoke a lost device’s app passwords, follow a message, release held mail. Never a change to what an account may do. |
| Auditor | auditor | Read everything in one tenant, the audit log included. Change nothing. |
| User | user | Themselves: read their own account record, make and revoke their own app passwords, set their own automatic reply, sign themselves out everywhere. |
What each role may do
| Action | Operator | Tenant admin | Domain admin | Helpdesk | Auditor | User |
|---|---|---|---|---|---|---|
| List tenants, create a tenant | Yes | No | No | No | No | No |
| See the tenant | Yes | Yes | Yes | Yes | Yes | No |
| Suspend or restore the tenant | Yes | Yes | No | No | No | No |
| List domains | Yes | Yes | Only their domains | Yes | Yes | No |
| Look at a domain, its DKIM keys and its reports | Yes | Yes | Their domains | Yes | Yes | No |
| Add a domain, mark it verified, make or retire DKIM keys | Yes | Yes | Their domains | No | No | No |
| List accounts | Yes | Yes | Accounts in their domains | Yes | Yes | No |
| Look at an account | Yes | Yes | In their domains | Yes | Yes | Their own |
| Create an account, change its status, deprovision it | Yes | Yes | In their domains | No | No | No |
| Reset a password, unlock, sign out everywhere | Yes | Yes | In their domains | Yes | No | Sign out their own |
| See where somebody signs in from | Yes | Yes | In their domains | Yes | Yes | Their own |
| See somebody’s two-step sign-in | Yes | Yes | In their domains | Yes | Yes | Their own |
| Remove somebody’s second step, for a lost phone | Yes | Yes | In their domains | Yes | No | Their own |
| Require or turn off two-step sign-in | Yes | Yes | No | No | No | No |
| See the organisation’s sign-ins, and who is locked out | Yes | Yes | Accounts in their domains | Yes | Yes | No |
| See app passwords | Yes | Yes | In their domains | Yes | Yes | Their own |
| Make an app password | No | No | No | No | No | Their own |
| Revoke app passwords | Yes | Yes | In their domains | Yes | No | Their own |
| See an automatic reply | Yes | Yes | In their domains | Yes | Yes | Their own |
| Set or stop an automatic reply | Yes | Yes | In their domains | No | No | Their own |
| See what somebody has sent, and who has sent the most | Yes | Yes | Accounts in their domains | Yes | Yes | No |
| See how full a mailbox is | Yes | Yes | In their domains | Yes | Yes | Their own, in their mail app |
| Set a mailbox’s size and message ceilings and its warning level | Yes | Yes | In their domains | No | No | No |
| See the organisation’s storage and its fullest mailboxes | Yes | Yes | No | No | Yes | No |
| Set the organisation’s storage ceiling | Yes | Yes | No | No | No | No |
| Set or reset somebody’s sending limits | Yes | Yes | In their domains | No | No | No |
| See whose sending is held for review | Yes | Yes | In their domains | No | Yes | No |
| Release or delete held sending, or exempt a sender | Yes | Yes | In their domains | No | No | No |
| Secure an account that was taken over | Yes | Yes | In their domains | Yes | No | No |
| Approve, refuse or revoke forwarding outside the organisation | Yes | Yes | In their domains | No | See only | No |
| Let somebody send as another address | Yes | Yes | In their domains | No | No | No |
| See somebody’s delegates | Yes | Yes | In their domains | Yes | Yes | Their own |
| Change somebody’s delegates | Yes | Yes | In their domains | No | No | No |
| The organisation’s allowed and blocked senders | Yes | Yes | No | No | Read | No |
| A domain’s allowed and blocked senders | Yes | Yes | Their domains | No | Read | No |
| How many senders a person allows and blocks | Yes | Yes | In their domains | Yes | Yes | No |
| Which senders a person lists, saying why | No | No | No | No | Yes | Their own, in their mail app |
| Read the access rules | Yes | Yes | Yes | No | Yes | No |
| Change the access rules | Yes | Yes | The rules about their domains | No | No | No |
| Read the tenant’s settings | Yes | Yes | No | No | Yes | No |
| Change the tenant’s settings | Yes | Yes | No | No | No | No |
Read or change the whole server’s settings (tenant 0) | Yes | No | No | No | No | No |
| See the queue and the message trace | Yes | Yes | Mail to or from their domains | Yes | Yes | No |
| Take a message back out of mailboxes | Yes | Yes | Mailboxes in their domains | No | Look only | No |
| Put a mailbox on legal hold, or lift a hold | Yes | Yes | No | No | See only | No |
| Set the organisation’s alerts | Yes | Yes | No | No | See only | No |
| Read the audit log | Yes | Yes | No | No | Yes | No |
A refusal names the wall that was hit: that belongs to another tenant, that domain is not one of yours, that is somebody else's, or a <role> may not do that, with status 403.
Roles your organisation makes
Besides the built-in roles, an organisation can make its own: a role that resets students’ passwords and nothing else, or one for the London office’s IT that manages London’s people but not its executives.
On the console, open Roles and choose New role…. Give it a name, tick what it may do from the same list the built-in roles are made of (a built-in role can be the starting point), and choose over whom. Give it to somebody on their page, as you would a built-in role.
From the command line:
vsx admin roles add 'Student helpdesk' --may reset-password,unlock --scope group:students@example.com --except group:staff@example.com
vsx admin roles add 'London IT' --from helpdesk --scope 'department=IT' --except group:executives@example.com
vsx admin role grant hana@example.com 'Student helpdesk'
vsx admin roles show
vsx admin roles change 'Student helpdesk' --name 'Pupil helpdesk'
vsx admin roles remove 'Pupil helpdesk' --take-from-holders
What a role may do
| Action | What it allows |
|---|---|
see-people | See people: their details, how they sign in, their sign-ins and limits. A role that acts on people always has this. |
add-people | Add people. |
hand-over | Hand a new account to the person it is for. |
change-people | Change people’s names, addresses, limits, delegates and automatic replies. |
remove-people | Remove people from the organisation. |
reset-password | Reset a password, sign somebody out everywhere, take away a second step, or revoke an app password. |
unlock | Unlock somebody locked out by failed sign-ins. |
see-quarantine, release-quarantine, discard-quarantine | See, release and throw away what the filter held back. |
find-mail, purge-mail | Find where a delivered message was filed, and take it out of a mailbox. |
see-reported, decide-reported | See the messages people reported as phishing and who reported them, and mark one as not phishing or remove it from people’s mailboxes. A role sees a report, and its reporters, only in the domains and groups its scope reaches. Removing copies needs purge-mail as well. |
see-deleted-mail, recover-deleted-mail | See the mail somebody deleted that can still be got back, and put it back. |
import-mail | Import somebody’s old mail from an MBOX file, watch the import and cancel it. |
trace, manage-queue | See the queue and follow a message through it, and act on the queue. The whole organisation only. |
see-forwarding, decide-forwarding | See and decide on forwarding outside the organisation. |
see-held-sending, release-held-sending | See and act on sending held because it looked unlike its sender. |
see-domains, change-domains | See domains, and check their records, make signing keys and mark them verified. |
groups-and-aliases, domain-senders | Manage groups and aliases, and a domain’s allowed and blocked senders. |
see-holds, set-holds | See, and put on, change and lift, legal holds. |
see-audit | Read the audit log. The whole organisation only. |
discovery | Discovery for legal cases: search, open and export other people’s mail, and keep it for a case. The whole organisation only. |
No role the organisation makes can give or take away roles, change the organisation’s settings, access rules or approvals, make roles, or do anything of the operator’s. Nobody can save a role that may do more than they may themselves, so a role is never a way round the built-in ones. The one exception is discovery, which no administrator holds: a role that gains it, and giving such a role to somebody, always waits for a second administrator.
Over whom
A role’s scope is one or more of these, and a person is inside it when any of them matches:
| Scope | Who it reaches |
|---|---|
organisation | Everybody (the default). |
domain:<domain> | Everybody in that domain. |
group:<address> | The members of that group. |
department=<value>, title=<value> | People whose directory entry says so. |
--except takes the same forms and leaves those people out, however they came in. An exception naming a group that no longer exists leaves everybody out, until the role is changed. Scopes are read at the moment of each request, so somebody who leaves a group is out of its scope at once. Something that belongs to a whole domain, such as its groups or senders, is inside a scope only when the scope reaches everybody in that domain.
A refusal says which role’s wall was hit, such as that is outside the scope of Student helpdesk. The console shows the holder only the pages and buttons their role opens.
Removing a role that somebody holds is refused, unless you choose to take it from them (--take-from-holders); each is in the audit log. Where the organisation requires approval for changing roles, saving its roles waits for a second administrator too.
Roles for a time
A role held all the time can be used at any hour, including by whoever steals the session. Instead, make somebody eligible for a role. They hold nothing until they need it, then take the role for a set time and say why. It ends by itself when the time is up.
- An administrator makes a person eligible for any role they could grant themselves, for at most a set number of minutes at a time, up to a week.
- The person takes it from the Your role for a time card on the Overview, or with
roles take, and gives a reason. The reason is kept in the audit log. Taking it again replaces the time left. - They then sign in again to use it. A sign-in made while the role lasts ends when the role does.
- Where your organisation asks a second administrator to approve role changes (
approvals.change_roles), taking a role waits for that approval too, and the role starts when it is approved. See Approvals. - Taken now on the Roles page lists who holds what until when, and why. The person, or an administrator, can end it early with End now. An auditor can see both lists.
versealx-server admin roles make-eligible ada@example.com administrator --minutes 120
versealx-server admin roles eligible
versealx-server admin roles take 30 --reason 'Unblocking the queue after the outage'
versealx-server admin roles active
versealx-server admin roles end ada@example.com
versealx-server admin roles not-eligible ada@example.com
Somebody who is eligible but holds no role can still sign in to the console, where they can take their role and do nothing else.
Through the API: GET and PUT /tenants/{tenant}/roles/eligible read and replace the whole list against its version. GET /tenants/{tenant}/roles/take shows what the person signed in may take, and POST takes it with minutes and reason. GET /tenants/{tenant}/roles/active lists the roles taken now, and DELETE /tenants/{tenant}/roles/active/{id} ends one.
A role taken away stops at the next refresh. Each time an app renews its sign-in, the server checks the role again. If the role was taken away or has run out, the renewal is refused and the person signs in again.
Break-glass accounts
Break-glass accounts are for when everything else is broken, for example:
- the identity provider is down
- an access rule locks every administrator out
- the second administrator needed for an approval is away
Choose up to two administrators as break-glass accounts. Each one must already hold the administrator role.
- They sign in with their password on the server’s own sign-in page, even where the domain signs in through an identity provider.
- No access rule refuses them on the sign-in page or in the console. The mail apps’ own sign-ins still follow the rules.
- Every sign-in sends the
break-glass-usedalert to all your administrators at once, and the account’s last sign-in is shown beside it. - A session lasts an hour from the sign-in, however often it is refreshed.
Give each account a long password that you keep offline, and add a passkey. Sign in with each one now and then, so you know it still works when you need it.
On the console, use the Break-glass accounts card on the Roles page. From the command line:
versealx-server admin break-glass set emergency1@example.com emergency2@example.com
versealx-server admin break-glass show
versealx-server admin break-glass set
break-glass set with no addresses clears the list. Through the API, GET and PUT /tenants/{tenant}/break-glass read and replace the whole list against its version. Changing it needs a recent sign-in.
A recent sign-in for what can’t be undone
Some actions need a sign-in from the last few minutes, not just a session that’s still open:
- removing people or a domain
- purging a message
- lifting a legal hold
- changing roles, or who may take one
- taking away someone’s second step
- changing the access rules or the organisation’s security settings
If the session began longer ago, the console asks you to sign in again, with your second step if you have one, and then to repeat the action. The command line and the API get 401 with the problem type https://nixtoffice.com/problems/step-up.
The organisation sets how many minutes count as recent with security.step_up_minutes. The default is 10, and 0 turns the check off. The node’s local socket and machines using provisioning tokens are never asked.
How a caller’s role is decided
| Caller | Role |
|---|---|
| Anything that opens the local admin socket | Operator. The socket is readable only by the service user, which can already read the store; a token would add nothing. |
| A bearer token over HTTPS | The role its scopes name, for the tenant the token belongs to. |
A token must be issued by this server for the versealx-admin audience. Its scopes decide the role:
| Scope | Role |
|---|---|
admin:operator | Operator. Only valid on a token that belongs to no tenant; otherwise refused with an operator token cannot belong to a tenant. |
admin:tenant | Tenant administrator. |
admin:audit | Auditor. |
admin:helpdesk | Helpdesk. |
admin:domain:<domain>, repeated for each domain | Domain administrator over those domains. |
| None of these | User. |
When a token carries more than one, the strongest wins, in the order above.
The audit log
The audit log is the durable answer to “who changed this, and what did it look like before”.
- Every change is recorded — a creation, an update, a deletion, a password reset — with who made it, from where, and the state before and after.
- Refusals and failures are recorded too, with the reason, because an attempt that failed is what an auditor is often looking for.
- Reads are not recorded, unless they were refused.
- It is append-only. Nothing in the API changes or removes a line. Lines are kept for the organisation’s retention period, seven years unless it sets
audit.keep_days(see Runtime settings); the server’s value is the least an organisation keeps. Older lines are removed by the server, which writes one line saying which it removed. - It is sealed into a chain. Every ten minutes the server links each new line to the one before it with a keyed hash, under a key derived from the organisation’s own data key, which only the server holds. It also reads the chain back, a part at a time. A line changed or removed after it was sealed breaks the chain at that line, and somebody who can edit the database but does not hold the server’s key cannot make the links agree again. The log’s answer says how far the chain reaches (
chain, below), and so do the console’s Audit log page andvsx admin audit list. - It records what no request did, too. Locks taken by sign-in protection (role
sign-in: an account locked after too many wrong passwords, or a network after failures across many accounts), every change a directory sync makes (roledirectory-sync), commands the operator runs on the server itself — pausing, resuming, exporting, importing and moving an organisation (roleoperator, as for the local socket) — retention (roleretention), networks refused mail for guessing at addresses (rolereceiving, in tenant0’s log), an organisation’s storage total set right by the daily recount (rolerecount), each copy of a reported phishing message the server took back on its own (roleprotection, verbtake-back), and what the organisation’s policies refused (rolepolicy, verbrefuse, below). - It is kept per tenant. A change is recorded in the log of the tenant it was about; a change that belongs to no tenant, such as creating a tenant, is recorded in tenant
0’s log. - Lines are numbered. Each tenant’s lines count up from 1, so a missing number would be visible.
- Secrets never reach it. In the before and after states, any field whose name contains
password,passwd,secret,token,private,key_material,hash,salt,credential,cookie,authorizationorseedis replaced whole with[redacted]. A state larger than 16 KiB is replaced with a note giving its size.
Reading the log
vsx admin get audit tenant=1
vsx admin get audit tenant=1 after=200 limit=50
| Parameter | Meaning |
|---|---|
tenant | Whose log. The caller’s own tenant unless the operator names one. The operator’s own is tenant 0. |
after | Start after this line number. Use the last id you saw to read the next page. |
limit | Lines to return, at most 200 (also the default). |
role | Only lines written in this role: an administrator’s role, or sign-in, directory-sync, operator, retention, receiving, recount, protection or policy. |
{
"items": [
{
"after": "{\"addresses\":[\"alex@example.com\"],\"created_at\":1789387200000,\"display_name\":\"Alex Morgan\",\"forward_to\":[],\"id\":2,\"keep_copy\":true,\"kind\":\"User\",\"send_as\":[],\"status\":\"Active\"}",
"at": 1789387200000,
"before": null,
"id": 7,
"outcome": "done",
"resource": "account/example.com",
"verb": "create",
"who": {
"account": null,
"role": "operator",
"source": "local socket",
"tenant": 0
}
}
],
"written": 7,
"chain": {
"sealed": 7,
"anchor": 0,
"verified": 7,
"verifiedAt": 1789387800000,
"broken": null
}
}
| Field | Meaning |
|---|---|
id | The line’s number in this tenant’s log. |
at | When, in Unix milliseconds. |
who | role, the caller’s tenant, their account where they had one, and source: the address the request came from, or local socket. |
verb | create, update, delete, reset-password, trace or read; for lines no request wrote, lock, suspend, reactivate, sync, pause, resume, moved, export, import, prune or refuse. |
resource | What was acted on: tenant, tenants (or tenants/<id> in the operator’s log), domain/<domain>, account/<domain> or account/<domain>/<id>, group/<domain>, network/<prefix> (a network sign-in protection locked, or one refused mail for guessing at addresses), directory (a sync run), queue, audit or config (the settings). |
outcome | "done", {"refused": "<reason>"} or {"failed": "<reason>"}. |
before, after | The state before and after, as compact JSON text with secrets redacted; null where there is none. |
written | How many lines this tenant’s log has ever had. |
chain | How far the log’s chain reaches. sealed: every line up to this one is linked into the chain. anchor: the last line retention removed, where the chain now begins (0 before any was). verified and verifiedAt: the server last read the chain from the anchor to this line, and every link matched, at this moment. broken: null, or the first line found changed or missing. Lines after sealed are sealed within ten minutes. |
To follow the log, read with after set to the highest id you have, repeatedly.
What the organisation’s policies refused
When one of the organisation’s policies refuses one of its people, the log says so, with the role policy and the verb refuse:
- the access rules, or the person’s plan, keeping a right password or token out of a door;
- a message whose From or Sender is not an address the account may send as;
- a sending limit reached;
- the organisation’s sending ceiling reached;
- a message from a domain not yet proved the organisation’s, to somebody outside it.
The line is about the person (account/<domain>/<id>), its outcome is the words they were given, and after says which policy (access-rules, send-as, sending-limit, sending-ceiling or unproved-domain), at which door (imap, submission, jmap, …), and how many more such refusals there were since the last line (more). It never names a recipient or says anything of the message.
So that a phone retrying every minute does not fill the log, there is one line an hour for each person, policy and door; the refusals in between are counted into the next line.
vsx admin get audit tenant=1 role=policy
In the console, the Audit log page finds them under The organisation’s policies.
When the chain is broken
A broken chain means a line was changed or removed after the server sealed it: somebody wrote to the database directly. The Audit log page says so across its top, vsx admin audit list ends by saying so, and the server logs it at every housekeeping pass. The line named is where to start looking; the lines around it are still readable. A backup taken before that line’s time holds it as it was written.
Something unclear or out of date on this page? Tell us.