Runtime settings

The settings Nixt Server keeps in its store — every key, its scope, type and default — and how to read, change, export and import them without a restart.

Some settings are too important to wait for a restart: a destination that has started deferring your mail, a flood of oversized messages, a tenant ready to enforce its MTA-STS policy. Those live in the server’s store rather than in the configuration file, and a running node picks up a change within seconds.

The file stays the floor. A node with nothing set in the store behaves exactly as its configuration file says; a setting in the store wins while it is there; remove it, and the file’s value comes back.

Documents and scopes

Settings are kept as one document per scope:

ScopeWhereWho may change it
The whole serverTenant 0: /api/v1/tenants/0/settingsThe operator only.
One tenant/api/v1/tenants/<id>/settingsThe operator, and the tenant’s administrator.

Some keys can only be set for the whole server; they are marked Server below. For the other keys, the value is looked up in this order, and the first one found is used:

  1. The tenant’s document.
  2. The whole server’s document.
  3. The node’s configuration file, where the key has a counterpart there.
  4. The built-in default.

Every document has a version. Each write increases it, keeps the previous version in the history, and writes an audit line with the document before and after.

Floors and locks

For some security controls, the operator can set a floor that every organisation must keep. An organisation’s own value is used only when it is at least as strict. The operator can also lock a control, so every organisation uses the operator’s value.

ControlStricter is
security.second_factoroff, then optional, then required
passwords.refuse_breachedoff, then on, then strict
passwords.min_lengthlonger
recovery.self_serviceoff
security.step_up_minutesfewer minutes; 0, which turns the check off, is weakest
approvals.*required
approvals.operator_accessask
audit.keep_dayslonger
mdn.sendany, then inside, then off
sharing.personaloff
security.admin_second_factorany, then passkey
security.user_verificationrequired
forwarding.externalallowed, then approval, then off
outbound.plaintext_retrynever
filtering.impersonation_leveloff, then standard, then strict
filtering.external_subject_tagon
ceilings.complaints_per_thousandfewer; 0, which turns it off, is weakest
offboarding.on_deprovisionoffboard
versealx-server admin security floor security.second_factor required
versealx-server admin security floor audit.keep_days 365
versealx-server admin security floor mdn.send inside --lock
versealx-server admin security floors
versealx-server admin security unfloor mdn.send

Wherever the server reads a floored setting for an organisation, a weaker or unset value is read as the floor. When the setting’s default is stricter than the floor, the default applies. An organisation that tries to save a weaker value is refused, and the error names the floor. A value it saved before the floor was set keeps working until the organisation changes it, but it is read as the floor. Through the API, the operator uses GET and PUT /api/v1/security/floors.

Floors for domains and people

An organisation can set floors of its own below the installation’s: for one of its domains, for all its people, or for the people of one domain. A domain’s administrators can set floors for their domain’s people. Each level is read under every floor above it, so the installation’s floors and locks win over the organisation’s, and the organisation’s over a domain’s.

These floors are taken only for the controls the server reads per domain or per person: security.second_factor, for domains and people, and mta_sts.mode, for domains, which is then the mode the domain publishes. A floor on any other control is refused, since nothing would hold it.

vsx admin security levels
vsx admin security floor-domain finance.example.com security.second_factor required
vsx admin security floor-domain finance.example.com mta_sts.mode enforce
vsx admin security floor-people security.second_factor required
vsx admin security floor-domain-people example.com security.second_factor required
vsx admin security unfloor-domain finance.example.com mta_sts.mode

A change under a floor is refused, naming the floor and who set it. A person whose floor requires a second step cannot remove their last one. The posture lists every floor with whom it is for and whether it holds: for MTA-STS, what the domain’s DNS last showed, and for a second step, how many people it covers still have none. A floor that does not hold is named in the drift alert. On the console, the organisation’s floors are on the Security posture card, and each domain’s page has a Security floors card, where the domain’s administrators set and remove the floors for its people. Through the API, they are GET and PUT /api/v1/tenants/<id>/security/floors, and PUT /api/v1/tenants/<id>/security/floors/<domain> for a domain.

Security posture

An organisation’s administrators and auditors can see how its controls stand against the Strict profile, including any floors the installation sets. Use the Security posture card on Settings, run versealx-server admin security posture, or call GET /api/v1/tenants/<id>/security/posture.

Strict asks for:

  • a second step for everyone
  • the strict leaked-password check
  • passwords of at least 14 characters
  • no self-service recovery
  • a recent sign-in, within 10 minutes, before anything that can’t be undone
  • a year of audit

The posture also checks what is actually true, not only what the settings say:

  • every listener that serves TLS, connected to on the server itself the way a client would, serves at least TLS 1.2 and a certificate with at least 14 days left. A listener that couldn’t be reached is shown as not checked, never as passing;
  • each of the organisation’s domains publishes DMARC at p=reject, as DNS last showed it;
  • each domain publishes and serves MTA-STS in enforce mode;
  • everyone has a second step;
  • every administrator has a second step;
  • every node refuses to send mail in the clear (BSI TR-03108 04-M);
  • each domain’s MX hosts publish DANE TLSA records, in a signed zone, that match the certificate the server serves (05-M);
  • each domain publishes a TLS reporting record with a report address (10-M);
  • the server’s certificate comes from an issuer the operator lists in posture.trusted_issuers (12-R). With no list, this is not checked.

A check the server could not make, such as a zone that is not signed or a lookup that failed, is shown as not checked, never as passing. The domain checks use the server’s last DNS look at each domain, which it refreshes every few hours and whenever someone asks from the domain’s page. The people who have no second step are named, up to ten per check.

The operator can raise the listeners’ floor, for example to TLS 1.3 or 30 days, on the console’s Listeners’ TLS page under System, which also shows each listener as good, out of profile with why, or not checked, or with GET and PUT /api/v1/security/listener-tls. Each listener is checked hourly.

To be told when a control falls short, turn on the below-strict alert. For each setting short of Strict it says who set its value and when, and the settings version that did it, so you can see that change in the settings history and set it back; a setting nobody changed is marked as its default.

Exceptions

Sometimes a control can’t meet Strict for a while, for a good reason: shared terminals in a call centre that can’t take a second step until new ones arrive, say. An administrator can record an exception to that control, with the reason and the day it ends, at most a year away.

An exception is a record, not a switch. It changes no setting and no floor. While it stands:

  • the posture and the security report show it beside the control, with who granted it and why;
  • the below-strict alert lists the control as excepted until its day instead of short.

When the day passes, the control counts against Strict again, and the alert says so.

On the Security posture card, use Exceptions to grant one, and End to end one early. Or run:

vsx admin security except security.second_factor 2027-03-31 'the call centre terminals, until they are replaced'
vsx admin security unexcept security.second_factor

Over the API, PUT /api/v1/tenants/<id>/security/exceptions/<control> with {"reason": "…", "until": <Unix milliseconds>} grants one, and DELETE on the same path ends it. The control is a setting’s name, or one of dmarc-reject, mta-sts-enforce, second-step-everyone and second-step-administrators. Each change is recorded in the audit log. Only the organisation’s administrators can grant or end one.

Security report

An auditor, or a customer’s security questionnaire, often asks how the organisation stands against a framework. The security report prints the same controls under the clauses of one:

  • NIS2: the cybersecurity measures of Article 21(2) of the NIS2 Directive, points (a) to (j);
  • ISO 27001: the ISO/IEC 27001:2022 Annex A controls that the organisation’s settings serve, such as secure authentication (A.8.5), logging (A.8.15) and use of cryptography (A.8.24);
  • BSI TR-03108: the 16 requirements of the BSI’s Secure E-Mail Transport guideline, version 2.0, each under its own title with its MUST or SHOULD. The server checks inbound MTA-STS (TR-03108-07-R) itself. Each of the others is listed as not checked by the server, with the reason, such as the listeners’ TLS, DANE or the certificates’ issuer, so you know what to show some other way.

Under each clause the report lists the controls that serve it, each with its value now or what its check found, and how many of those Strict asks about meet it. For NIS2 it also names the points that no setting of the organisation shows, such as business continuity, so you know to cover them in your own documents. The report shows how your controls stand. It does not say that you meet a framework.

Use the Security report card on Settings, which shows both frameworks, or run these with the vsx shell function from the Quick start:

vsx admin security report nis2
vsx admin security report iso-27001
vsx admin security report bsi-tr-03108

Over the API, call GET /api/v1/tenants/<id>/security/report?framework=nis2, framework=iso-27001 or framework=bsi-tr-03108. Administrators and auditors can read it.

A signed report

A report can be handed to somebody who checks it without trusting whoever passed it on. Download signed JSON on the card, --signed, or &signed=true gives the report with when it was made, for which organisation, against which framework and by which node, signed with the server’s own signing key:

vsx admin security report nis2 --signed report.json
vsx admin security verify-report report.json

verify-report checks the signature against this server’s key. Anybody else can check it too: the file holds the signature as a JWS and the address of the server’s published keys (/.well-known/jwks.json). Its header names the key, and the signature is Ed25519 over the first two parts. A report and a sign-in token can never be taken for each other.

Every setting’s default is its strictest value: a setting an organisation has not chosen reads as the strictest, and its administrators can change it within the installation’s floors. Two are the exceptions, each for a reason: mta_sts.mode defaults to testing, since enforce would stop mail from senders the moment a certificate goes wrong, and approvals are required by default only once an organisation has two administrators, since one cannot approve their own requests.

An organisation that had left a setting unset when these defaults became the strictest now reads it as the strictest. versealx-server doctor names each such organisation and the settings that changed for it, and so does its security posture (unsetNowStricter); setting the old value back keeps things as they were.

Every setting

KeyScopeTakesDefaultWhat it does
mta_sts.modeTenant or servernone, testing or enforce[mta_sts] mode in the file, or testingThe MTA-STS policy this tenant’s verified domains publish. Start at testing and read the TLS reports; enforce makes senders refuse to deliver over anything but verified TLS.
mta_sts.max_ageTenant or serverSeconds, 86400 to 31557600604800 (a week)How long senders may cache the policy.
outbound.unproved_domainsTenant or serverrefuse or allowrefuseWhether an account may send to outside addresses from a domain not yet proved to belong to the tenant. refuse accepts such mail only for recipients this server hosts. allow sends anyway, for a deployment with no DNS of its own; that mail will fail DKIM and SPF alignment at the other end.
outbound.plaintext_retryTenant or servernever or after-failed-handshakeneverWhat to do when a TLS handshake fails on a connection where TLS was only opportunistic. never defers the message. after-failed-handshake delivers it again without encryption. A connection under DANE, MTA-STS or REQUIRETLS is never retried without encryption.
passwords.min_lengthTenant or server12 to 12814The fewest characters a password set here may have, whoever sets it: the person, an administrator’s reset, an invitation or SCIM. It can only be raised. A password already set is not asked again.
passwords.refuse_breachedTenant or serveroff, on or strictstrictWhether a password that has appeared in a data breach is refused wherever it is chosen. on takes a password the check cannot be made for; strict refuses new passwords until it can be. See Passwords that have leaked.
security.second_factorTenant or serveroff, optional or requiredrequiredWho is asked for a code from an authenticator app after the password on the server’s sign-in pages: whoever has added one, everybody, or nobody. Wherever it is asked, mail apps sign in with app passwords. See Two-step sign-in.
security.passkeysTenant or serverallowed or offallowedWhether people may add passkeys and sign in with them. See Passkeys.
security.passwordlessTenant or serveroff or allowedoffWhether somebody may sign in with only a passkey, and no password, on the server’s own pages. Never where passkeys are off.
security.user_verificationTenant or serverpreferred or requiredrequiredWhether a passkey must have the device check it is the person every time it is used. Signing in with only a passkey always requires it.
security.admin_second_factorTenant or serverpasskey or anypasskeyHow the organisation’s administrators take their second step on the server’s sign-in pages: with a passkey and nothing else, or any way anybody may. See Administrators sign in with a passkey.
forwarding.externalTenant or serverapproval, allowed or offoffWhether a person’s mail may be forwarded outside the organisation: once an administrator approves it, always, or never. What an administrator set goes whatever this says. See Forwarding outside the organisation.
forwarding.destinationsTenant or serverA list of domains and addresses, at most 1000EmptyDestinations anybody may forward to without waiting: a domain (not its subdomains) or a single address.
sending.hold_multipleTenant or server0 to 100010Hold an account’s sending for review when it sends to more recipients in a day than this many times its usual day, and to more than 200. 0 turns this sign off. See Sending held for review.
sending.hold_new_recipientsTenant or server0 to 100000100Hold it when it writes to more than this many recipients in an hour that it had never written to. 0 turns this sign off.
sending.hold_spam_scoresTenant or server0 to 10003Hold it when the outbound filter scores its mail as spam this many times in an hour. 0 turns this sign off.
sending.hold_complaintsTenant or server0 to 10005Hold its sending outside the organisation when the feedback loops you trust complain about its mail this many times in an hour. 0 turns this sign off.
ceilings.complaints_per_thousandTenant or server0 to 10001Complaints in a day per thousand messages sent outside that day, counted from a thousand, past which outside mail is deferred; 0 turns it off. See An organisation’s sending ceilings.
offboarding.on_deprovisionTenant or serveroffboard or disableoffboardWhat SCIM deactivating or deleting somebody, or a directory sync finding they have gone, does: offboard them with the organisation’s default choices, or only disable the account. See When somebody leaves.
sieve.redirects_per_messageTenant or server0 to 1004Places one message may be redirected to by a person’s Sieve script. See Limits the organisation sets.
sieve.redirects_per_dayTenant or server0 to 100000No ceilingMessages one person’s scripts may redirect in a day.
sieve.vacations_per_dayTenant or server0 to 100000No ceilingAutomatic replies one person may send in a day.
sieve.script_bytesTenant or serverBytes, 1024 to 104857665536How large one Sieve script may be.
sieve.stepsTenant or server100 to 10000000100000Commands and tests one run of a script may evaluate.
quarantine.keep_daysTenant or serverDays, 1 to 365030How long quarantined mail waits to be released before it is deleted. A tenant’s value wins over the server’s.
filtering.impersonation_levelTenant or serverstrict, standard or offstrictWhat an impersonation finding does: quarantine the message, file it in Junk, or only add the header. See Impersonation.
filtering.external_subject_tagTenant or serveron or offonWhether [External] is added, once, to the subject of mail from outside the organisation in the copy filed in each mailbox.
protection.auto_takebackTenant or serveron or offoffWhether the server takes a reported message back from everybody without waiting for an administrator, when enough people report it as phishing or a later verdict finds it. Only unread copies are taken, into quarantine. See Reported phishing.
protection.takeback_reportsTenant or server2 to 1003How many different people must report one message before the server takes it back, when protection.auto_takeback is on.
calendar.auto_add_externalTenant or serverauthenticated or neverneverWhether an invitation from outside the organisation, proved to come from its organiser, is added to the invited person’s calendar by itself. See Calendars and scheduling.
calendar.freebusyTenant or serverorganisation or nobodynobodyWho in the organisation may see when a person is busy. See Busy time.
calendar.publishingTenant or serverdetails, freebusy or offoffWhat a calendar a person publishes as a link shows, or nothing. See Publishing a calendar.
sharing.personalTenant or serverallowed or offoffWhether people may share one of their own folders with a colleague from their mail app. See Sharing one folder.
classifier.modeTenant or serveron or offonWhether the classifier learns from people moving mail into and out of Junk, and scores incoming mail by it. off keeps what it learnt.
mdn.sendTenant or serveroff, inside, anyoffWhere the organisation’s people may send read receipts: nowhere, only to the organisation’s own domains, or to anybody who asks for one. Each receipt is still a person’s own choice, sent from their mail app.
classifier.lessons_per_dayTenant or server1 to 100000200How many messages one person may teach the classifier in a day by moving mail into and out of Junk. Moves beyond that are ignored, so one person cannot outweigh everyone else.
sender_history.pointsTenant or server0 to 103How many points what your people made of a sender’s earlier mail may add to a message’s spam score, or take away. 0 turns it off. See Sender history.
quarantine.digestTenant or serverdaily or offdailyWhether each person gets a daily message listing what was newly quarantined for them, with a link to release it. A tenant’s value wins over the server’s.
trace.keep_daysTenant or serverDays, 1 to 3650[admin] trace_days in the file, or 30How long a message trace is kept. An organisation’s value wins over the server’s.
audit.keep_daysTenant or serverDays, 90 to 365002555 (seven years)How long the organisation’s audit log is kept. The server’s value is the least any organisation keeps: an organisation’s shorter one is read as the server’s. Older lines are removed once they are sealed into the log’s chain, and one line says which were removed.
changes.keep_daysServerDays, 1 to 365030How long each account’s record of changes is kept, which mail apps read to learn what changed since they last looked. An app away for longer reads the account again. See Changes and resynchronising.
changes.keep_countServer1000 to 10000000100000The fewest changes kept per account, however old, so a quiet account keeps more than its last 30 days.
deliverability.feedback_idTenant or serveroff, bulk or allbulkWhich mail carries a Feedback-ID for Gmail’s Postmaster Tools. See Feedback-ID for Gmail.
deliverability.feedback_sendersServerA list of addressesEmptyThe feedback loops whose complaint reports count, each by the exact address its reports come from. See Complaints from feedback loops.
posture.trusted_issuersServerA list of up to 32 issuers, each as its distinguished nameEmptyThe certificate issuers the security posture counts as meeting BSI TR-03108 12-R. Empty, the issuer is not checked.
limits.message_sizeServerBytes, 1024 to 26214400[limits] message_size in the file, or 26214400The largest message accepted, advertised as SIZE. It can never be raised past 25 MiB.
limits.recipientsServer1 to 100[limits] recipients in the file, or 100Recipients one message may name, advertised as LIMITS RCPTMAX.
oauth.console.redirect_urisServerA list of one or more addresseshttps://localhost/callbackEvery address the versealx-console sign-in application may be sent back to. Each must be absolute, carry no fragment, and use https, or http on a loopback address. Addresses are matched whole and exactly.
outbound.destination.<domain>.connectionsServer1 to 1024The file’s [outbound] ceiling, or the built-in oneConnections this node opens to one destination at once.
outbound.destination.<domain>.messages_per_connectionServer1 to 10000As aboveMessages sent down one connection before another is opened.
outbound.destination.<domain>.messages_per_minuteServer0 to 1000000As aboveMessages a minute to the destination. 0 lifts the ceiling.
outbound.destination.<domain>.recipients_per_minuteServer0 to 1000000As aboveRecipients a minute to the destination. 0 lifts the ceiling.

A key that is not in this table is refused. In the outbound.destination keys, <domain> is a destination written as the file writes one — gmail.com, or .google.com (with its leading dot) for everything under a domain — using the ASCII form of the name and at least two labels.

When a change takes effect

SettingRead
limits.message_size, limits.recipientsEvery second. A new connection is told the new ceiling; a connection already open keeps the ceiling it was promised.
outbound.destination.*Every second, on the next delivery pass.
mta_sts.mode, mta_sts.max_ageWithin a minute.
outbound.unproved_domainsOn the next message.
outbound.plaintext_retryOn the next delivery attempt.
passwords.min_length, passwords.refuse_breachedOn the next password set.
security.second_factor, security.passkeys, security.passwordless, security.user_verificationAt the next sign-in. Whether any organisation allows signing in with a passkey alone is read within 30 seconds.
forwarding.external, forwarding.destinationsOn the next message.
sending.hold_multiple, sending.hold_new_recipients, sending.hold_spam_scoresOn the next message sent.
quarantine.keep_daysAt the next expiry pass, which runs every ten minutes.
protection.auto_takeback, protection.takeback_reportsOn the next report, and on the next pass of the later-verdict check, every ten minutes.
calendar.auto_add_external, calendar.freebusy, calendar.publishingOn the next invitation or busy-time question.
sharing.personalAt the colleague’s next command.
classifier.modeOn the next message, and for learning within seconds.
mdn.sendOn the next receipt.
classifier.lessons_per_dayOn the next move learnt.
sender_history.pointsOn the next message.
quarantine.digestAt the next digest pass, which runs every ten minutes; each person gets at most one digest a day.
trace.keep_daysAt the next trace expiry, which runs every ten minutes.
audit.keep_daysAt the next housekeeping pass, which runs every ten minutes.
changes.keep_days, changes.keep_countAt the next housekeeping pass, which runs every ten minutes and works through the accounts a page at a time, so a large server is covered over several passes.
oauth.console.redirect_urisOn the next sign-in request.

Reading settings

These examples use the vsx shell function from the Quick start, which runs versealx-server as the service user with the configuration path.

vsx admin get tenants/0/settings
{
  "by": "operator 0/-",
  "settings": {
    "limits.message_size": 10485760,
    "outbound.destination.outlook.com.messages_per_minute": 120
  },
  "updated_at": 1789387200000,
  "version": 3
}
FieldMeaning
versionHow many times the document has been written. 0 means never.
updated_atWhen it was last written, in Unix milliseconds.
byWho wrote it, as <role> <tenant>/<account>. The operator on the local socket is operator 0/-.
settingsThe settings, by key.

versealx-server explain prints what the configuration file says; admin get tenants/0/settings prints what the store says over it.

Changing some settings

PATCH changes the keys you give and leaves the rest. A key set to null is removed.

# Lower the message size ceiling for the whole server
vsx admin patch tenants/0/settings limits.message_size=10485760

# Remove it again, so the file's value applies
vsx admin patch tenants/0/settings limits.message_size=null

# Slow one destination down during an incident
vsx admin patch tenants/0/settings outbound.destination.outlook.com.messages_per_minute=120

# Everything under google.com: note the two dots
vsx admin patch tenants/0/settings outbound.destination..google.com.connections=4

# Enforce MTA-STS for tenant 1's domains
vsx admin patch tenants/1/settings mta_sts.mode=enforce

The answer is the new document. A patch that changes nothing returns the current document and writes nothing.

To make sure nobody else changed the document since you read it, include the version you read. The write is refused with 409 Conflict if it is no longer current:

vsx admin patch tenants/1/settings mta_sts.mode=enforce version=3

Replacing the whole document

PUT replaces the document with the one you send, which is how you import an exported document or put back an earlier version:

vsx admin put tenants/1/settings 'settings={"mta_sts.mode":"enforce","mta_sts.max_age":1209600}' version=3

Keys you leave out of settings are removed.

Exporting and importing

  1. Export: vsx admin get tenants/1/settings > tenant-1-settings.json.
  2. Keep the file in version control if you like.
  3. Import: take the settings object from the file and send it with PUT, as above. Include version to avoid overwriting a change made in the meantime.

History

vsx admin get tenants/1/settings/history limit=5
vsx admin get tenants/1/settings/history/2

The first lists versions newest first, each with version, updated_at and by — 20 unless you give limit, at most 200. The second returns one earlier version whole, ready to PUT back.

Errors

StatusDetailCause
400`<key>` is not a settingA key the server does not read, often a typing mistake.
400`<key>` is set for the whole server, not per tenantA Server key in a tenant’s document. Set it in tenant 0.
400`<key>` takes one of none, testing, enforceA value outside the allowed words.
400`<key>` takes a whole number from <min> to <max>A number out of range, or not a number.
400`oauth.console.redirect_uris` takes a list of one or more absolute addresses with no fragment: https anywhere, http only on loopbackAn address that cannot be registered.
400`settings` must be an object of settings by keyA PUT without a settings object.
400`version` must be a whole numberA version that is not a number.
403that belongs to another tenantAnyone but the operator writing to tenant 0 or another tenant.
409version <n> is no longer current; it is <m> now. Read it again and retry.Someone wrote the document after you read it.

Something unclear or out of date on this page? Tell us.