Licensing: the licence token and the handshake
How a Bastrel server proves who it is to the Bastrel API, what the API sends back, and what happens when the API cannot be reached.
Status: the API side and the resource side (reading the convars, verifying the token, the four states below) are both written. The licence stays inactive in every source and development build and becomes active only in the release package: until then nothing here changes how Bastrel protects your server. It detects, bans and logs locally with or without the API.
What you install
Section titled “What you install”Two convars in server.cfg, both with set, never setr (a setr convar is sent to every player):
set bastrel_license_key "bl_..."set bastrel_api_secret "bs_..."- The licence key identifies one server. It is a name, not a proof: it is sent with every request.
- The secret proves that the request comes from that server. It never leaves your server: it signs requests, it is not sent.
- Both are shown once, when the server is created in the panel. If you lose the secret, rotate it in the panel; Bastrel cannot show it again.
- One key and one secret per server. Use
server_kinddevfor a staging server: each plan includes development servers (see the pricing grid).
How a request is authenticated
Section titled “How a request is authenticated”Every call from the resource carries four headers:
| Header | Content |
|---|---|
X-Bastrel-Key |
the licence key |
X-Bastrel-Timestamp |
Unix time in seconds |
X-Bastrel-Nonce |
16 random bytes, 32 hexadecimal characters, new for every request |
X-Bastrel-Signature |
HMAC-SHA256 in lower-case hexadecimal, key = the secret (its whole text, UTF-8) |
The signed text is seven lines joined by a line feed (no final line feed):
METHOD upper case, for example POSThost the API host, lower case, for example api.bastrel.compath for example /v1/servers/handshake (ASCII, no escaping)canonical query pairs sorted by name then value, percent-encoded except A-Z a-z 0-9 - . _ ~ ; empty when nonetimestamp the same text as the headernonce the same text as the header (32 lower-case hexadecimal characters)SHA-256 of the body, lower-case hexadecimal (of the empty string when there is no body)The API refuses a request whose timestamp is more than 60 seconds away from its own clock, and a nonce it has already seen from the same server (kept a little longer than two minutes, the span in which a signed request is accepted). A body larger than 64 KiB is refused with 413. A refused request always gets the same 401 answer, whether the key does not exist or the signature is wrong. Only a caller who proved it knows the secret is told, with the code BAS-API-010, that its clock is off, together with the API time (server_time): the resource then corrects its offset and tries once more, so a server whose clock is two minutes fast still gets a licence.
The API answers a successful request with the header X-Bastrel-Response-Signature: the HMAC-SHA256 of response, the request nonce and the SHA-256 of the response body, joined by line feeds. The nonce ties the answer to the request, so an old answer replayed later is useless.
The handshake
Section titled “The handshake”POST /v1/servers/handshake registers the server and returns the licence token. The body (JSON) carries server_uuid (a random identifier generated at the first start), resource_version, fx_artifact, server_kind (production or dev), framework, the list of active modules, and optionally a resmon measure in milliseconds. Unknown fields are refused.
The answer carries server_id, the token, config_hash (empty until a configuration exists), features, latest_resource_version, server_time and entitlement (active, until).
Your plan limits the number of servers seen in the last 7 days, per kind (servers_prod, servers_dev in features). A server that exceeds it gets 403 with BAS-API-011; a server not seen for 7 days frees its place. A licence key is one server: while it was seen in the last 7 days, a handshake with another server_uuid is refused with the same code (one key shared by several machines). Limit: after a reinstall (a new server_uuid) the server stays refused for up to 7 days, even with a new key on a one-server plan, and runs in local / observe; releasing the server from the panel is planned.
The token
Section titled “The token”v1.<payload>.<signature>: the payload is JSON with sorted keys, base64url without padding; the signature is Ed25519 over the text v1.<payload>. The algorithm is fixed (there is no alg field to tamper with). The payload holds kind (license: a configuration can never be used as a licence), kid (which public key verifies it), tenant_id, server_id, plan, features, max_mode, iat, expires_at, entitlement_until and ref_key, with every time in Unix seconds.
- With an active subscription:
max_modeisenforceand the token expires after 7 days or when the longest entitlement of your subscriptions ends, whichever comes first (the features are those of your best plan when the token is issued). The entitlement runs to the end of the paid period plus a short grace period. - Without an active subscription: the handshake still succeeds, but the token says
max_modeobserve, grants no feature and carries the plannone. ref_keyis the key of your player references: the pseudonymous values (license_ref) that the cloud stores instead of raw identifiers. It is yours, and the same on all your servers.
Public keys are rotated by kid: the resource embeds the current key and the next one, so a rotation does not need a new release.
The four states
Section titled “The four states”When the licence is active, the resource is always in one of these states (/bas license shows it, never the token). Changing state is written once in the server console and in the log, then recalled every hour for the last two.
| State | When | What changes |
|---|---|---|
active |
the token is valid and the API answered | nothing: full protection |
grace_offline |
the token is valid but the API cannot be reached | nothing: the cached token keeps working until it expires (at most 7 days after the last handshake); the resource retries after 1, 2, 4 then 5 minutes |
cloud_off |
the API said, in a signed answer, that your subscription has no active right | the cloud functions stop (panel, synchronisation) and the effective mode becomes observe: detection, logs and bans stay, there is no automatic sanction. An active subscription again restores your configured mode |
local |
the token is missing, expired or its signature is not valid | the effective mode is observe: Bastrel detects and logs, bans already in force and manual bans (/bas ban) still apply, there is no automatic sanction. It never stops your server |
A trial token is observe by design (a property of the trial plan). A token that the API sends but whose signature does not verify never replaces the one you already have. If the clock of your server is wrong by up to a day, the resource corrects the time it signs with after the first refusal; it never uses that correction to extend a token. A token also ends when the most recent time the API told the server (kept across restarts) is past its expiry: turning the clock back does not extend it, and an accidental jump forward is forgotten once the clock is right.
Optional convars: set bastrel_api_url "https://..." (default https://api.bastrel.com; https, or http on the loopback only; its host must be the one the API signs with) and set bastrel_server_kind dev for a staging server (default production). The secret is read only by the Node.js script of the resource and is never logged.
When the API cannot be reached
Section titled “When the API cannot be reached”Protection never depends on the API: see grace_offline and local above. A valid token keeps working offline until it expires, at most 7 days after the last successful handshake. Bastrel never stops your server.
Limits you should know
Section titled “Limits you should know”The licence is a deterrent and an operating guard, not a lock: the files that are not under Asset Escrow can be modified by their owner. What enforces the licence is Cfx Asset Escrow, tied to your account, and the API refusing cloud features (panel, synchronisation, captures, network ban, signed configuration) to a licence that is not valid.