Getting Started

License Activation: Online, Offline, and Air-Gapped

How a self-hosted Versus install proves its entitlement with a signed, offline-verifiable license token — activated online with a code, offline by exchanging a fingerprint, or fully air-gapped with nothing ever phoning home.

A self-hosted Versus install proves what it's entitled to run with a license token — a small, signed file that lists the customer, the license class, the modules you've bought, any numeric limits, and an expiry. This page explains what that token is, the three ways to activate one (online, offline, and air-gapped), and how the platform behaves as a license moves through its lifecycle.

The design goal throughout is simple: the signed file is always authoritative, and nothing is ever forced to phone home. Online activation is a convenience layer on top of a primitive that verifies entirely offline.


The license token

A Versus license is a compact, JWS-style token — three base64url segments, header.payload.signature — signed with Ed25519 (EdDSA). The header carries typ: VESLIC, alg: EdDSA, and a key id (kid); the payload is the entitlement:

{
  "license_id": "6f3c…",
  "issued_at": "2026-08-19T00:00:00Z",
  "not_before": "2026-08-19T00:00:00Z",
  "expires_at": "2027-08-19T00:00:00Z",
  "customer": { "org_name": "Acme Corp", "org_code": "acme", "sub": "" },
  "class": "subscription",
  "edition": "enterprise",
  "features": ["tls_clm", "adc", "ipam_ddi"],
  "limits": { "max_devices": 500 },
  "binding": { "install_fingerprint": "…" },
  "grace_days": 14
}

The private signing key never ships with the product — it lives only in the issuer (the hosted Licensing Service and the ops tooling). The product carries an embedded public keyring keyed by kid, so every install can verify a token's signature with no network access at all. Because each token names the kid it was signed under, old licenses keep verifying across key rotations; additional trusted keys can be supplied at runtime through the LICENSE_TRUSTED_KEYS environment variable without a code change.

Verification checks the signature and format only. Policy — expiry, not_before, grace, and fingerprint binding — is applied separately when the token is installed, so a syntactically valid but expired or mis-bound license is still rejected.


When licensing applies

License enforcement is off unless the deployment is explicitly on-premise. An install enforces licensing only when VERSUS_DEPLOYMENT_MODE=onprem (or LICENSING_ENFORCED=true) is set. On the managed cloud service, on developer machines, and on existing installs, the licensing subsystem is inert: every feature is available and no token is required. Multi-tenant SaaS entitlements are handled by a separate mechanism.

Everything that follows describes a self-hosted, enforcing install.


The install fingerprint

On first access, each install generates a stable fingerprint — a random per-install identifier that is created once and never changes for the life of the deployment. Licenses can be node-locked by binding them to a fingerprint: a bound token installs only on the install whose fingerprint matches, and is rejected everywhere else.

Read your fingerprint from the API or the System → Licensing page:

GET /api/licensing/fingerprint
Authorization: Bearer …

200 OK
{ "fingerprint": "a1b2c3…" }

You need this value for offline activation (below), where it's the input the Licensing Service binds the token to.


Three ways to activate

1. Online activation (activation code)

The fastest path for a connected install. When a license is issued, the Licensing Service can mint a one-time, human-friendly activation code in the form VS-XXXX-XXXX-XXXX (the alphabet excludes ambiguous characters like 0/O and 1/I). You paste that code into System → Licensing or post it to the API, and the install does the rest:

POST /api/licensing/activate
Authorization: Bearer …            # Network-Admin
Content-Type: application/json

{ "activation_code": "VS-7K4M-92QF-JD3P" }

Under the hood the install sends the code together with its own fingerprint and product version to the Licensing Service's public endpoint (POST /api/activation/v1/activate). The service validates the code — it must exist, be unconsumed, and be unexpired — re-signs the license bound to your fingerprint, marks the code consumed, and returns the token. The install then verifies and stores it exactly as if you'd uploaded a file. The response is your refreshed licensing status.

Activation is a no-op — and returns a clear error — on any install that isn't enforcing licensing or has no license server configured (i.e. an air-gapped install), so you can't accidentally trigger a network call where one isn't wanted.

2. Offline activation (fingerprint → file → apply)

If the install can't reach the Licensing Service but you can, activate out-of-band:

  1. Copy the install's fingerprint (GET /api/licensing/fingerprint).
  2. Give it to your Versus account team, who issue a .veslic token node-locked to that fingerprint.
  3. Paste or upload the token into System → Licensing, or post it:
POST /api/licensing/apply
Authorization: Bearer …            # Network-Admin
Content-Type: application/json

{ "token": "eyJhbGc….eyJ….sig" }

Applying a token verifies its signature against the embedded keyring, checks not_before and expiry (with grace), and confirms the fingerprint binding matches this install. A newly-applied token replaces any previous one and clears any prior revocation — a freshly issued license always supersedes what came before.

3. Air-gapped

An air-gapped install is simply an enforcing install with no license server configured. It never contacts anything: you activate it exactly like the offline flow above — fingerprint out, signed .veslic in — and the token's offline signature verification is the whole trust story. No heartbeat runs, no usage is reported, and a network failure can never lock you out because there is no network call to fail. This is the intended posture for classified or isolated environments.


License states and the grace period

Every read of the current license computes a state:

StateMeaning
not_enforcedLicensing is off (cloud, dev, or non-onprem install).
unlicensedEnforcing, but no valid token is applied.
validA verified token, within its term.
gracePast expires_at but still inside grace_days.
expiredPast the grace period.
revokedRevoked by the issuer (delivered via heartbeat).
invalidA token that fails signature verification.

Enforcement is deliberately gentle and never hard-bricks an install. Within grace_days (default 14) after expiry, everything keeps working and the UI warns you. Once an enforcing install passes grace — or is revoked — it goes read-only: reads succeed, but writes are refused with an HTTP 402. Even then, login, license management (/api/licensing), and auth are always exempt, so an admin can sign in and apply a renewal to recover.

Beyond term and grace, a license also gates modules. A request into a module the license doesn't include is refused with 402 regardless of state; the always-included Platform Core is never gated.


Modules and classes

Licenses select from a set of à-la-carte functional modules:

Module keyLabel
tls_clmTLS / CLM
firewallFirewall
secure_vaultSecure Vault
adcADC
ipam_ddiIPAM / DDI
data_warehouseData Warehouse (Snowflake)

Curated bundles — Keystone, Perimeter, Fabric, and Continuum — are presets that select a set of these modules. Platform Core (Versus Desk, Workflow, AI Ops, Vulnerability Management, Observe, ITSM, GitOps/CI-CD, Cloud, Compliance) ships with every license and is never gated.

Licenses come in three classes: trial (45-day term by default), subscription (1/2/3-year terms), and nfr (not-for-resale, for internal and SE demo use).


The heartbeat (connected installs)

A connected install — one that's enforcing and has a license server configured — periodically checks in with the Licensing Service to pick up renewals and revocations. This is the only ongoing network activity licensing performs, and it's optional: air-gapped installs never run it.

The heartbeat is driven by a management command on a timer (a systemd timer or Kubernetes CronJob), at the server-advised cadence (default 24 hours):

python manage.py validate_license
python manage.py validate_license --dry-run   # report only; contact nothing, write nothing

Each cycle posts the license_id, fingerprint, a fresh random nonce, and best-effort usage counts (managed device and collector counts — counts only, no PII) to POST /api/activation/v1/validate. The service answers with the license's current state, a revoked flag, and — on a renewal or upgrade — a superseding token the install verifies and swaps in automatically.

Two invariants make this safe:

  • Fail-open on transport. An unreachable or slow server is not an error. The install records the blip and keeps running on its offline license; a network outage can never downgrade a valid, in-window license.
  • Fail-closed on cryptography. Every actionable answer is wrapped in a signed envelope that echoes the nonce the install sent and matches the reported state. A forged "not revoked" reply, a replayed answer, or a man-in-the-middle response fails verification and is ignored — the offline license keeps governing. A renewal token is likewise re-verified before it's applied.

You can trigger a check on demand from System → Licensing ("Check now") or the API:

POST /api/licensing/validate
Authorization: Bearer …            # Network-Admin
→ returns the refreshed licensing status

Because a revocation is delivered through the heartbeat, revoking a license only takes effect on connected installs — an air-gapped install is governed entirely by the term and fingerprint baked into its signed file.


Checking status

Any authenticated user can read the current status; it's what the System → Licensing page renders:

GET /api/licensing/status
Authorization: Bearer …

200 OK
{
  "enforced": true,
  "state": "valid",
  "class": "subscription",
  "edition": "enterprise",
  "features": ["tls_clm", "adc", "ipam_ddi"],
  "limits": { "max_devices": 500 },
  "expires_at": "2027-08-19T00:00:00Z",
  "days_remaining": 364,
  "fingerprint": "a1b2c3…",
  "activation_source": "online",
  "connectivity": { "status": "ok", "last_verified_at": "…", "next_check_estimate": "…" }
}

The page surfaces the class chip, current state, days remaining, activation source (online vs. offline), and — for connected installs — a connectivity indicator (ok / stale / offline / never) with the last successful check-in. The connectivity hint is display-only; it never gates anything, so a flaky link shows a warning icon but changes nothing about what you can do.


API reference

Consumer (the install), under your Versus base URL

EndpointMethodAccessPurpose
/api/licensing/statusGETAuthenticatedCurrent license state, entitlement, connectivity.
/api/licensing/fingerprintGETAuthenticatedThis install's fingerprint (for offline activation).
/api/licensing/applyPOSTNetwork-AdminApply a .veslic token (offline / air-gapped).
/api/licensing/activatePOSTNetwork-AdminOnline activation with an activation code.
/api/licensing/validatePOSTNetwork-AdminRun one heartbeat now ("Check now").

Licensing Service (issuer), at license.<host>

These are called by connected installs — the activation code or signed nonce is the bearer, so no user auth is required — and are rate-limited.

EndpointMethodPurpose
/api/activation/v1/activatePOSTExchange an activation code for a node-bound token.
/api/activation/v1/validatePOSTHeartbeat: state, revocation, superseding token (signed envelope).

Security notes

  • Offline-verifiable by design. Ed25519 signatures verify against an embedded public keyring, so a license is trustworthy with zero network access. The private key never leaves the issuer.
  • No lock-out from network faults. Transport failures are tolerated (fail-open); only cryptographic failures reject a response (fail-closed).
  • Node-locking. Bound tokens install only on the matching fingerprint, so a leaked file can't be reused on another install.
  • Tamper-evident heartbeats. Nonce-echo plus a signed envelope defeat replay and forged answers.
  • No secrets in transit or at rest here. Usage reports are bare counts; license management credentials live in the Versus Vault.

Related