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

  1. 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.
  2. 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

RoleName in the logReach
OperatoroperatorEverything, in every tenant, including the list of tenants and the whole server’s settings.
Tenant administratortenant-adminEverything inside one tenant, except that the audit log is read-only.
Domain administratordomain-adminThe domains they hold in one tenant, and the accounts in those domains. Read-only sight of the tenant.
HelpdeskhelpdeskLook 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.
AuditorauditorRead everything in one tenant, the audit log included. Change nothing.
UseruserThemselves: 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

ActionOperatorTenant adminDomain adminHelpdeskAuditorUser
List tenants, create a tenantYesNoNoNoNoNo
See the tenantYesYesYesYesYesNo
Suspend or restore the tenantYesYesNoNoNoNo
List domainsYesYesOnly their domainsYesYesNo
Look at a domain, its DKIM keys and its reportsYesYesTheir domainsYesYesNo
Add a domain, mark it verified, make or retire DKIM keysYesYesTheir domainsNoNoNo
List accountsYesYesAccounts in their domainsYesYesNo
Look at an accountYesYesIn their domainsYesYesTheir own
Create an account, change its status, deprovision itYesYesIn their domainsNoNoNo
Reset a password, unlock, sign out everywhereYesYesIn their domainsYesNoSign out their own
See where somebody signs in fromYesYesIn their domainsYesYesTheir own
See somebody’s two-step sign-inYesYesIn their domainsYesYesTheir own
Remove somebody’s second step, for a lost phoneYesYesIn their domainsYesNoTheir own
Require or turn off two-step sign-inYesYesNoNoNoNo
See the organisation’s sign-ins, and who is locked outYesYesAccounts in their domainsYesYesNo
See app passwordsYesYesIn their domainsYesYesTheir own
Make an app passwordNoNoNoNoNoTheir own
Revoke app passwordsYesYesIn their domainsYesNoTheir own
See an automatic replyYesYesIn their domainsYesYesTheir own
Set or stop an automatic replyYesYesIn their domainsNoNoTheir own
See what somebody has sent, and who has sent the mostYesYesAccounts in their domainsYesYesNo
See how full a mailbox isYesYesIn their domainsYesYesTheir own, in their mail app
Set a mailbox’s size and message ceilings and its warning levelYesYesIn their domainsNoNoNo
See the organisation’s storage and its fullest mailboxesYesYesNoNoYesNo
Set the organisation’s storage ceilingYesYesNoNoNoNo
Set or reset somebody’s sending limitsYesYesIn their domainsNoNoNo
See whose sending is held for reviewYesYesIn their domainsNoYesNo
Release or delete held sending, or exempt a senderYesYesIn their domainsNoNoNo
Secure an account that was taken overYesYesIn their domainsYesNoNo
Approve, refuse or revoke forwarding outside the organisationYesYesIn their domainsNoSee onlyNo
Let somebody send as another addressYesYesIn their domainsNoNoNo
See somebody’s delegatesYesYesIn their domainsYesYesTheir own
Change somebody’s delegatesYesYesIn their domainsNoNoNo
The organisation’s allowed and blocked sendersYesYesNoNoReadNo
A domain’s allowed and blocked sendersYesYesTheir domainsNoReadNo
How many senders a person allows and blocksYesYesIn their domainsYesYesNo
Which senders a person lists, saying whyNoNoNoNoYesTheir own, in their mail app
Read the access rulesYesYesYesNoYesNo
Change the access rulesYesYesThe rules about their domainsNoNoNo
Read the tenant’s settingsYesYesNoNoYesNo
Change the tenant’s settingsYesYesNoNoNoNo
Read or change the whole server’s settings (tenant 0)YesNoNoNoNoNo
See the queue and the message traceYesYesMail to or from their domainsYesYesNo
Take a message back out of mailboxesYesYesMailboxes in their domainsNoLook onlyNo
Put a mailbox on legal hold, or lift a holdYesYesNoNoSee onlyNo
Set the organisation’s alertsYesYesNoNoSee onlyNo
Read the audit logYesYesNoNoYesNo

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

ActionWhat it allows
see-peopleSee people: their details, how they sign in, their sign-ins and limits. A role that acts on people always has this.
add-peopleAdd people.
hand-overHand a new account to the person it is for.
change-peopleChange people’s names, addresses, limits, delegates and automatic replies.
remove-peopleRemove people from the organisation.
reset-passwordReset a password, sign somebody out everywhere, take away a second step, or revoke an app password.
unlockUnlock somebody locked out by failed sign-ins.
see-quarantine, release-quarantine, discard-quarantineSee, release and throw away what the filter held back.
find-mail, purge-mailFind where a delivered message was filed, and take it out of a mailbox.
see-reported, decide-reportedSee 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-mailSee the mail somebody deleted that can still be got back, and put it back.
import-mailImport somebody’s old mail from an MBOX file, watch the import and cancel it.
trace, manage-queueSee the queue and follow a message through it, and act on the queue. The whole organisation only.
see-forwarding, decide-forwardingSee and decide on forwarding outside the organisation.
see-held-sending, release-held-sendingSee and act on sending held because it looked unlike its sender.
see-domains, change-domainsSee domains, and check their records, make signing keys and mark them verified.
groups-and-aliases, domain-sendersManage groups and aliases, and a domain’s allowed and blocked senders.
see-holds, set-holdsSee, and put on, change and lift, legal holds.
see-auditRead the audit log. The whole organisation only.
discoveryDiscovery 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:

ScopeWho it reaches
organisationEverybody (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-used alert 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

CallerRole
Anything that opens the local admin socketOperator. The socket is readable only by the service user, which can already read the store; a token would add nothing.
A bearer token over HTTPSThe 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:

ScopeRole
admin:operatorOperator. Only valid on a token that belongs to no tenant; otherwise refused with an operator token cannot belong to a tenant.
admin:tenantTenant administrator.
admin:auditAuditor.
admin:helpdeskHelpdesk.
admin:domain:<domain>, repeated for each domainDomain administrator over those domains.
None of theseUser.

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 and vsx 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 (role directory-sync), commands the operator runs on the server itself — pausing, resuming, exporting, importing and moving an organisation (role operator, as for the local socket) — retention (role retention), networks refused mail for guessing at addresses (role receiving, in tenant 0’s log), an organisation’s storage total set right by the daily recount (role recount), each copy of a reported phishing message the server took back on its own (role protection, verb take-back), and what the organisation’s policies refused (role policy, verb refuse, 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, authorization or seed is 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
ParameterMeaning
tenantWhose log. The caller’s own tenant unless the operator names one. The operator’s own is tenant 0.
afterStart after this line number. Use the last id you saw to read the next page.
limitLines to return, at most 200 (also the default).
roleOnly 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
  }
}
FieldMeaning
idThe line’s number in this tenant’s log.
atWhen, in Unix milliseconds.
whorole, the caller’s tenant, their account where they had one, and source: the address the request came from, or local socket.
verbcreate, update, delete, reset-password, trace or read; for lines no request wrote, lock, suspend, reactivate, sync, pause, resume, moved, export, import, prune or refuse.
resourceWhat 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, afterThe state before and after, as compact JSON text with secrets redacted; null where there is none.
writtenHow many lines this tenant’s log has ever had.
chainHow 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.