ADR 0004: Offline licensing and what a licence may switch off

Date: 2026-09-11 Status: accepted

Context

Nazeel is sold as an annual on-prem licence, per developer seat, and installed in places that have no route to the internet at all. Three things follow immediately.

A licence cannot be checked against a server. There is no server to check against, and a product that phones home would fail the first security review it met. So the licence has to carry its own proof.

A licence will expire while someone is still working. Renewals go through procurement, and procurement in a bank is measured in weeks. A product that stops on the expiry date would delete a team's environments over a purchase order.

And whoever is holding the key is not an attacker. The people who install Nazeel already have cluster-admin on the cluster it runs in. A licence check is a commercial mechanism, not a security boundary, and treating it as one produces a product that is unpleasant to run and no harder to bypass.

Decision

An ed25519-signed key, verified locally. The licence is a base64 payload plus a signature over it, prefixed NZL2.. The public vendor key is compiled into the binary. The private key never leaves the vendor. Verification is a signature check and a clock read, so it works in a sealed room.

NZL1. is still accepted. A customer who bought before the format changed must not be shut out by an upgrade; the prefix is part of the signed material, so old keys cannot be replayed as new ones.

The development key is behind a build tag. devlicence compiles in a second vendor key for local work. Release builds are built without it, so a key signed by the development key is refused in production. Without the tag, anyone with the repository could mint licences.

Plans, not loose flags. The tool issues against a plan (community, team, enterprise, sovereign) and the plan implies its features. Flags are still in the payload, so a one-off arrangement is expressible, but the normal path makes an issued key match the price list rather than whatever the person running the tool remembered.

No key at all is the free tier, not a refusal. An install with no licence key runs as Community: one seat, one workspace, and none of the optional features. It is a working product, not a nag screen — a developer can install the chart, sign in, define a baseline and run nazeel up without talking to anybody. A registered Community key, issued free, raises the limit to ten seats. This is why the code carries licence.Unkeyed(): the missing case returns a real Community licence value rather than nil, so every caller asking "what plan, how many seats, which features" gets an answer instead of a special case. Making nil mean "no licence" and letting callers interpret it was how the feature gate came to read l == nil || l.Has(feature) — which handed an unkeyed install every paid feature and made deleting the key the cheapest upgrade path.

Missing and invalid are different states. missing is the free tier and works. invalid — a key that fails signature verification, or one signed by the development key in a release build — is refused, in the API and identically in the admission webhook. Somebody installed something wrong, and silently downgrading them to Community would hide it.

Expiry is a warning, then a grace period, never a deletion. Thirty days of grace after expiry, seven for a trial. During grace everything works and the dashboard says so plainly. After grace, the product refuses to create new things: no new environments, no new applications. Existing environments keep running, terminals keep opening, and nothing is deleted, ever. A licence problem is a conversation with procurement, not an outage.

Seats are soft. The seat count is a billing number, not a gate. Passing it raises a warning; a burst percentage (10% by default) is tolerated outright; beyond that, new invites are refused while everyone already working keeps working. An active seat is someone who signed in or holds a pending invite, because counting only sign-ins let an admin invite an unlimited number of people and get them working before anyone was counted.

What a licence gates, and what it never gates. Optional features check the licence: terminal, SIEM forwarding, SAML, per-PR environments, executive reports. Core work does not. Creating an environment, waking a sleeping one, opening a preview, reading the audit log: a developer opening their own preview must never meet a billing error, and an auditor must never be told the evidence is unavailable for commercial reasons.

A security control is never a paid feature. The line between what a licence may switch off and what it may not is not "how much did it cost to build": it is whether the thing is a capability or a control.

Capabilities are what a customer buys more of as they grow, and they are commercial: the web terminal, SIEM forwarding, SAML, per-PR environments, building from source, the AI assist, the executive reports. A customer on a smaller plan does less with the product; nothing about their installation is less safe.

Controls are what keep the installation defensible, and every edition has them, including the free one: recording retention and session limits, image scanning, encryption of backups and archives, storing evidence in the customer's own object storage, the support bundle, the audit log and its export. Selling these would mean shipping a product that is less safe for the customers who can least afford to make it safe themselves, and it would mean a price list that reads as a list of things we left switched off. A bank's security team would notice, and they would be right to.

The practical test when adding something new: if the answer to "what happens if this is off" is the customer does less, it may be licensed. If the answer is the customer is less able to detect, contain or prove something, it may not.

Binding to a cluster is optional. A licence may carry the kube-system namespace UID, which pins it to one cluster. It is off by default: most customers run several clusters under one agreement, and a key that stops working when they build a DR cluster generates a support call rather than a sale.

Consequences