Skip to main content

Applying and updating a license

A license arrives as a single signed string. Installing it is deliberately boring, and it is boring in the same way on a laptop, in Kubernetes, and on a disconnected cell in a rack at a site with no route to the internet.

This page is the operational half of the licensing lifecycle: how a key gets in, what happens when it does, what a rejection means, and how an update reaches a fleet.

The license key itself​

A key is one line of text in a versioned envelope, s9lic.<payload>.<signature>. The payload is canonical JSON carrying the organization, tier, posture, entitlement limits and feature flags; the signature is Ed25519 over those exact bytes, checked against a CA public key compiled into the build rather than fetched or configured.

Two consequences follow, and both matter operationally:

  • Verification needs no network. Installing a license on an air-gapped cell is the same operation as installing one in the cloud.
  • The prefix is the format version. A key that does not begin with the expected prefix is hard-rejected rather than parsed leniently, so a future format ships as a new prefix instead of a surprise field.
Treat the key as a secret

The key is a bearer artifact that grants capability to an organization. Put it where you put other secrets: a secret manager, a mounted Kubernetes secret, or the console. It is not something to paste into a ticket or a chat thread.

Five ways in, one chokepoint​

There are five install paths because there are five legitimate operating patterns. There is only one activation function, and every path calls it.

Figure L4: five install paths, one chokepoint. The convergence is the point. Every path runs the same guards and registers the same complete set of runtime keys, so a license installed from a mounted file behaves identically to one pasted into the console. The keys are derived in one place from the payload, and no caller names a key, which is what stops a new install path from registering a subset and leaving stale values behind.

Table equivalent

PathWhen you use itHow an update lands
Setup wizardFirst run of a new cell, in the browserPart of initial setup
Admin consoleDay-to-day: apply, replace or renew on a running cellImmediate, no restart
Environment variableHeadless and scripted bootstrap, containers, imagesAt start
Mounted fileKubernetes secret rotation, configuration managementPicked up on change, no restart
Replicated rowMulti-cell: the node's primary fetches, the rest followReplicated, then picked up, no restart

Applying from the console​

The console path is a single authenticated call that takes the key and returns the resulting enforcement state, not just an acknowledgement. That design choice is worth calling out: the operator sees the effect of the apply immediately rather than applying, refreshing, and hoping.

The response reports the swap direction (upgrade, renewal or downgrade), the old and new tier, the organization, and the enforcement state and reason the cell is now in.

The route is session-authenticated and gated on administrative write permission.

Applying headlessly​

For images and automation, supply the key as an environment variable (S9_LICENSE_KEY) or place it at the well-known config path (config/license.s9lic). Headless bootstrap checks both.

Rotating a mounted file​

A file-mounted license is watched. When the file changes, which is what a Kubernetes secret rotation looks like on disk, the watcher verifies the new key and swaps the in-memory enforcer without a restart. The watcher ticks on a short interval, so rotation is a matter of seconds, not a rollout.

What the guards check, and what a rejection means​

Activation is not just signature verification. Before anything is registered, the chokepoint runs every guard, and a failure installs nothing. There is no half-applied state.

RejectionWhat it meansWhat to do
signature_invalidThe signature does not verify against the embedded CA keyThe key is corrupt or not genuine. Re-copy it; check for truncation or added whitespace
malformedThe envelope or payload could not be parsedUsually a copy-paste problem, or a key from an incompatible format version
wrong_orgThe key is for a different organization than this deploymentYou have someone else's key, or the wrong cell. This guard is what stops a license being applied to the wrong estate
expiredThe key's term has already run outObtain a current key. An expired key is refused at install rather than accepted into an immediately-degraded state
retention_invariantThe key's retention terms conflict with what this deployment must preserveA compliance-relevant mismatch, not a formatting problem. Escalate rather than retry
air_gap_boundsThe key's air-gap terms are inconsistent with the declared offline windowThe offline window and the air-gap addendum disagree. The key needs reissuing
eula_not_acceptedThe bundled end-user agreement has not been accepted on this cellA precondition, not a defect in the key. Record consent, and the next watcher tick activates the same key with no further action

The last row is the one that surprises people, so it is separated deliberately in the response. Every other rejection says this key is wrong. That one says this key is fine, this cell is not ready, and the distinction is why consent is reported as a conflict rather than a bad request.

The consent gate is on by default

Consent to the bundled agreement is required by default, because procurement and compliance reviews ask for it. It can be explicitly opted out of for environments where that is not wanted. While the gate is blocking, the license is not registered, a structured block reason is published for diagnostic surfaces to render, and the watcher keeps re-evaluating, so recording consent is enough. Nothing has to be reapplied.

How a renewal reaches a whole node​

A multi-cell node does not want every cell independently reaching out for its own license. The pattern is one fetch, then replication:

  1. In connected mode the node's primary cell checks for an updated key daily. This covers renewals, plan upgrades and feature-flag changes.
  2. The new key is written to the license table.
  3. Replication fans the row out to the node's other cells.
  4. Each cell's watcher notices the change, verifies the key itself, and swaps its own enforcer.

Every cell verifies independently. A replicated row is a delivery mechanism, not a statement of trust, so a cell never runs on a license it has not checked against the embedded key itself.

Refresh is connected-mode only and skips silently when the deployment is disconnected or air-gapped. It is not an error condition there, and it never becomes one.

A refresh failure is not a license failure

The refresh path distinguishes the server said this license is gone from the network did not work. Only the first changes anything. A transport fault leaves the installed license exactly as it was, which is the behaviour you want when a link is flaky.

Seeing the state of a fleet​

For an organization with many cells, per-cell inspection does not scale. A fleet view rolls licenses up across the whole organization, folding the same grading into rollups by node, region and tier, with a per-cell verdict.

The per-cell row carries what an operator actually needs to act:

  • The enforcement state, graded by the one ladder every other surface uses.
  • Whole days since the last verified sync, which is the input to the offline half of that ladder.
  • The governing air-gap window, when an air-gap addendum applies.
  • Whether the term is due to run out inside a chosen horizon.

The rollups are folded from the same underlying grading rather than computed separately, so a summary and the cell it summarises cannot disagree.

Expiry warnings arrive before the consequence​

Licenses do not lapse quietly. Warnings are pinned to the enforcement ladder, so every warning lands before the thing it warns about, on a ladder of rungs approaching expiry.

The mechanism is worth a sentence because it is the part that is usually buggy elsewhere. The sweep runs frequently, so a naive implementation sends the same warning repeatedly. Instead each license records the most urgent rung it has already warned at, and a warning fires only when the current rung is more urgent than the stored one. A renewal resets the record automatically, because the new term's rungs are all less urgent than the stored one. Correct behaviour falls out of the design rather than out of a special case.

The same worker that sends warnings also advances each license's stored status, so there is one source of truth rather than a live decision and a stale column that drift apart.

Two refusals in that sweep are deliberate:

  • A license with no expiry is left ungraded, never stamped healthy. Marking an unknown as "active" would turn an absence of information into a reassurance.
  • A row that cannot be parsed is skipped and counted, never defaulted, and the count is reported. "Nothing is degraded" and "nothing could be read" are different answers and are reported differently.

Installing with no network at all​

Nothing above needs egress except the optional daily refresh. On an air-gapped cell:

StepHow it works offline
Obtain the keyOut of band: the key is a string, carried on whatever medium the site allows
Install itConsole, environment variable or mounted file, exactly as above
Verify itAgainst the embedded CA public key. No network, no call home
Renew itInstall the new key the same way. There is no online activation step
Stay compliant while offlineGoverned by the declared offline window, which an air-gap addendum can widen deliberately. See when the meter cannot report

The property to test during an evaluation is simple, and it is the one to ask a vendor for: cut the link, install a license, and confirm the cell licenses itself.