Vendor support access
Applies to: SimpleTwo 0.9.x, coordinator with ADR-0052, agent 0.5.118 · Checked: 07.10.2026
When something breaks — calls drop on one SFU, a role does not come up after an upgrade, a mail domain stops verifying — the vendor's support engineers need to see the installation's state. Until now that meant a console account created for the occasion and forgotten afterwards. A vendor support token replaces that practice:
- it only reads the console API and changes nothing;
- it does not see secrets — the screens that hold keys, passwords, tokens and decrypted data are closed to it;
- it ends on its own: the term is one to three years;
- only an owner issues and revokes it, and every issue, revocation and use is in the audit log.
The first consumer is the SimpleOne VCSM product; the "Vendor" field defaults to it.
Issue a token
Only a user with the owner role, signed in completely — password and second-factor code (or through SSO / identity) — issues one. The break-glass account does not issue tokens.
- Console → Settings → the Vendor support access card. Only owners see it.
- Issue a token: a name (for example "VCSM support, case 4711"), the vendor, the purpose — required, it goes into the audit — and the term: 1, 2 or 3 years.
- The token,
st_vst_<id>_<secret>, is shown once. Copy it with Copy and hand it to the vendor over a channel you trust. It cannot be shown again: a lost token is revoked and a new one issued. Only its hash is stored.
A token cannot be extended — issue a new one when the term ends.
The same through the API (an owner's session):
curl -fsS -X POST "$S2/admin/api/vendor-tokens" -H "X-Admin-Token: $OWNER_SESSION" \
-H 'Content-Type: application/json' \
-d '{"name":"VCSM support","vendor":"SimpleOne VCSM","purpose":"case 4711","term_days":365}'
term_days is 365 to 1095. The list is GET /admin/api/vendor-tokens (no secrets), revocation is
POST /admin/api/vendor-tokens/{id}/revoke.
How the vendor uses it
The token is accepted only by the console API — paths under /admin/api/*, in
Authorization: Bearer … or X-Admin-Token: …:
curl -fsS "$S2/admin/api/me" -H "Authorization: Bearer $VENDOR_TOKEN" # who am I, what may I do
curl -fsS "$S2/admin/api/overview" -H "Authorization: Bearer $VENDOR_TOKEN"
The client API (/v1), the nodes' doors, the break-glass sign-in and the other services' APIs
(identity, messaging, mail, drive) do not accept it.
What it sees and what it does not
The token acts with the built-in role vendor_support: every read permission of the
catalogue (gateways:read, routing:read, users:read, settings:read, audit:read) and no
write permission. On top of that, any request other than GET is refused before its handler —
with one exception: asking a node for its journal (see Node logs).
Closed although they are reads — 403 with the code vendor_token_forbidden:
| What | Why |
|---|---|
| the break-glass account, second factors, the vendor tokens themselves | access management |
| encryption: key state, coverage, recovery sheet, message export | keys and data in the clear |
guest recovery codes (/admin/api/recovery) | live codes |
client distribution (/admin/api/clients) | the publish token |
a deployment's card (/admin/api/deployments/{id}) and rendered configs | the database DSN with its password, hosts' configs |
| automations and their runs | webhook secrets and message text |
the proxy to identity's console (/admin/api/identity/control/…) | personal data and identity's management |
mail DNS (/admin/api/mail/dns) | the read may create a DKIM key |
Screens where a secret is only marked ("set / not set") are open: SSO, sign-in, APNs, FCM, AI keys, SimpleOne and HRMS, LDAP, external stores, TLS. The coordinator's log, the audit and client telemetry are open — they are what support comes for — but de-personalised (see Node logs).
Node logs
Hosts' logs, reports, the audit trail and telemetry are open to the vendor, but only de-personalised: hosts send their journals to the coordinator as they are, the console's administrators read them as they are, and the coordinator de-personalises everything it serves to a vendor token — the vendor never receives a raw line.
De-personalised for a vendor token: a node's journal chunk and the error lines in its health
(/admin/api/nodes/{id}/logs, …/health), a host's reports (/admin/api/gateways/{id}/reports),
the coordinator's log (/admin/api/logs), the audit trails (/audit, /calendar/audit,
/agents/audit), client telemetry (/telemetry, /client-sessions), the mail quarantine and group
bounces (/mail/quarantine, /mail/group-bounces), the MCU's live calls (/mcu/live), alerts
(/alerts), and deployments' log lines and errors in /overview and /servers.
What is replaced:
| What | With |
|---|---|
private keys (PEM), Authorization headers, Bearer/Basic, cookies, the user and password in URLs and connection strings, SimpleTwo tokens (st_vst_…, bot_…, session tokens and JWTs), AWS and well-known API keys, the values of password=, secret=, token=, api_key=, dsn= and the like, long random strings | [REDACTED:…] |
the subject and text of mail and messages (subject=, body=, text=, preview=…, Subject: in the mail log) | [REDACTED:text] |
users in user=, username=, login=, account=, actor=, sub=, sender=, rcpt=, sasl_username=, display_name=…, in from=/to= holding an address, in sshd, postgres, pam and coturn lines | a pseudonym u-xxxxxxxx |
| e-mail addresses | u-xxxxxxxx@domain — the domain stays; role addresses (postmaster, MAILER-DAEMON, abuse, noreply) are left alone |
phone numbers (+…, Russian 8 (9xx) …, +7 …), the number in sip:/tel: | a pseudonym tel-xxxxxxxx |
| IP addresses | truncated: IPv4 to its /24 (203.0.113.x), IPv6 to its /48 (2001:db8:1::x); 127.0.0.1, 0.0.0.0, ::1 stay |
Pseudonyms. A pseudonym is the first 8 characters of an HMAC-SHA256 of the value under the installation's key. The coordinator makes the key on a vendor's first read and keeps it as one of its own secrets: sealed under the master key when encryption at rest is on, like the ACME account key. No API route serves it. So the same person gets the same pseudonym across all of the installation's logs and on any day (you can tell two errors are the same user), and cannot be mapped back to a name without the key. While encryption at rest is locked (the master key not loaded), the coordinator de-personalises with a temporary key: pseudonyms change after a restart, and nothing is served raw.
The console shows logs as they are — the de-personalised view exists only in answers to a vendor token.
What a vendor token may do:
- ask for a unit's journal —
POST /admin/api/nodes/{id}/logswith{"unit":"…","lines":N}— the onlyPOSTa vendor token is let through; at most 4 times a minute per token, every ask in the audit trail (node.logs, byvendor:<id>, with the node, the unit and the number of lines). It does not depend on the host's agent version; - read the answer —
GET /admin/api/nodes/{id}/logs— and the node's health, de-personalised.
A node's last journal chunk is one for everybody: a vendor may read a chunk an administrator asked for — de-personalised; the administrator reads one the vendor asked for — as it is.
Limits. De-personalisation is a heuristic. Names in free text ("Ivan Petrov signed in"), in
file names and in fields not on the list are not recognised; host names and file paths stay as they
are. Lists of users, groups and sessions (/users, /sessions and the like) a vendor sees as a
console viewer does — they are not logs. That is why a vendor's reads are in the audit trail, and
why the token can be revoked at any moment.
Revoke
In the card — Revoke on the token's row, then the confirmation in the row itself. Access ends at once; the row stays, marked with who revoked it and when.
What a token is answered:
| State | Answer |
|---|---|
| revoked | 401, vendor_token_revoked |
| expired | 401, vendor_token_expired |
| wrong or unknown | 401, vendor_token_invalid |
a closed path, or not GET | 403, vendor_token_forbidden |
| more than 4 log asks a minute | 429 |
Audit
Console → Logs → the audit trail:
vendor_token.issued,vendor_token.revoked— always, by the owner;vendor_token.used— at most once an hour per token, byvendor:<id>, with the path and address;vendor_token.refused— every refused attempt: an expired or revoked token, a closed path;node.logs— every ask for a node's journal, with the node, the unit and the number of lines.
The card shows each token's last use and the address it came from.
Switching to identity
Vendor support tokens are the coordinator's own credentials, not sessions. So neither switch of
switching to identity — sign-in (sign_in_mode) or the platform
token (platform_token) — affects them: issued tokens keep working. After the switch only an owner
still issues them — signing in on identity's page, with the owner role taken from the
coordinator's projection of identity (the account's roles and its groups'). Identity's own console
and API do not accept vendor tokens.
The console's second factor
Two-factor authentication is mandatory in the console, and from this release the server
enforces it, not just the page. A password sign-in by an account without a second factor (and the
first sign-in after installation) opens the second-factor setup only: any other /admin/api
request is answered 403 two_factor_enrollment_required. Once the code is confirmed the console
gets a full session and opens. Sign-in through SSO or identity is not affected — the second factor
there is the sign-in provider's; the break-glass account always signs in with a password and a
code.
Scripts that signed in to the console with a password alone must now pass the code
(scripts/fetch-geoip-ru.sh — in ADMIN_CODE), and for scheduled reads a vendor support token is
the better fit.