> ## Documentation Index
> Fetch the complete documentation index at: https://radarhq.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Licensing

> What gets sent home, the warn-only banner ladder, key rotation.

## Format

A self-hosted license is an **Ed25519-signed JWT**:

* Signature scheme: `EdDSA` (RFC 8037 Ed25519).
* The `radar-hub` binary embeds the Skyhook-controlled public key at build time. The matching private key never leaves Skyhook's secret store.
* Verified offline at boot - no network call required for the signature check itself.

### Claims

```json theme={null}
{
  "iss": "skyhook",
  "aud": "radar-hub",
  "sub": "lic_acme_001",
  "iat": 1781000000,
  "nbf": 1781000000,
  "exp": 1812536000,
  "org": "Acme Corp",
  "tier": "enterprise",
  "max_clusters": 50,
  "features": ["sso", "audit-365d"]
}
```

The control plane rejects any JWT whose `iss != "skyhook"` or `aud` doesn't include `"radar-hub"`. Required claims: `sub`, `org`, `exp`. Everything else is optional.

## How verification works

### At boot

The control plane reads the JWT from one of:

1. `RADAR_HUB_LICENSE_KEY` env var (preferred).
2. `/etc/radar-hub/license` file (Helm Secret mount).

Boot logs report one of (all prefixed with `radar-hub:` in the log line - grep for these strings):

**License-set path** (the most common case):

* `radar-hub: license verified` - happy path. Same log line carries license id, org, tier, max\_clusters, expiry.
* `radar-hub: license is expired (warn-only - hub keeps serving)` - signature OK, past `exp`. Continues serving.
* `radar-hub: license verification failed` - signature mismatch or missing required claim. Continues serving in **warn-only** mode for v1. Investigate immediately - this almost always means the wrong build talking to a real key, or vice versa.

**License-not-set path** (separate slog call earlier in boot, also v1-warn-only):

* `radar-hub: self-hosted mode but no RADAR_HUB_LICENSE_KEY (or /etc/radar-hub/license) - running in unlicensed mode (warn-only banner)` - the JWT file/env wasn't supplied. Set `license.key` in your Helm values or mount a Secret to `/etc/radar-hub/license`.

**Placeholder-key build** (won't appear on official `ghcr.io/skyhook-dev/radar-hub` images):

* `radar-hub: embedded license public key is the all-zero placeholder; rebuild with the real key before shipping to a customer` - the build wasn't cut against the real Skyhook public key, so license verification cannot succeed against any real customer JWT. Only happens on a locally-built image - the official `ghcr.io/skyhook-dev/radar-hub:<version>` images embed the production public key. If you see this in production, you're running an unofficial build; pull the published tag instead.

### Warn-only enforcement

The control plane does NOT block writes when the license is invalid or expired. Enforcement is **contractual** via the Subscription Agreement, not technical. The user-visible behavior is:

* A persistent yellow banner in the web app when verification has failed.
* Boot log lines visible to the operator.
* Heartbeat reports the issue back to Skyhook.

Warn-only is intentional: a license blip should never compound a real outage by locking your team out of their own observability tool.

## License heartbeat

The control plane posts a heartbeat every 6 hours. On by default; disable with `--set license.heartbeat.enabled=false` if your environment forbids outbound HTTPS.

* Endpoint: `POST https://api.radarhq.io/v1/license/heartbeat`
* Payload: `license_id`, `hub_version`, `hub_mode`, `org_name`, `cluster_count`, `audit_event_count_24h`, `started_at`.
* Response: optional renewed JWT, the latest published `hub_version`, optional message string.

What's NOT sent:

* No cluster names, no user emails, no audit-event content.
* No customer Postgres data.
* No tunnel traffic.

The full heartbeat schema and a sample payload are reproduced in the Privacy Policy + Subscription Agreement so a customer's security review can audit the wire shape. The customer can audit live by tailing control-plane logs - every heartbeat call is logged.

### Failure ladder

When the control plane can't reach the heartbeat endpoint:

| Days without success | Behavior                                                    |
| -------------------- | ----------------------------------------------------------- |
| 0-7                  | Silent.                                                     |
| 7-14                 | Yellow "Cannot reach license server" banner in the web app. |
| 14-30                | Red banner; admin gets warning on each login.               |
| 30+                  | Same red banner. **No write blocking.**                     |

If the underlying JWT itself expires AND the heartbeat hasn't renewed it for 14d, the banner shows "License expired - contact your account team". Still no write block.

## Rotation

Licenses are issued for a fixed term (typically 12 months) matching the contract. Renewal is either:

* **Heartbeat-renewed**: the renewal response carries a new JWT. The control plane stores it in Postgres and uses it on next boot.
* **Manual** (fallback when heartbeat is disabled, or when your account team needs to push an out-of-cycle renewal): your account team emails a new JWT; you update the Helm value or Secret + roll the `radar-hub` Deployment.

```bash theme={null}
helm upgrade radar-hub skyhook/radar-hub \
  --namespace radar-hub \
  --reset-then-reuse-values \
  --set license.key="<new JWT>"

# or, if you used existingSecret:
kubectl -n radar-hub create secret generic radar-hub-license \
  --from-literal=license="<new JWT>" --dry-run=client -o yaml | kubectl apply -f -
kubectl -n radar-hub rollout restart deploy/radar-hub-hub
```

## Key rotation (Skyhook-side)

The embedded public key is append-only. Skyhook will publish a new chart `appVersion` with both the old and new public keys when the signing key rotates; existing deployments keep working until they upgrade past the cutover release.

You don't need to do anything for a public-key rotation - just stay on supported chart versions and run `helm upgrade` at your normal cadence.

## What you control

* The license JWT itself (rotate by upgrading the chart with a new value).
* Cookie sealing key (`hub.cookiePassword`) - independent from the license; rotating it just signs everyone out.
* The break-glass admin (always available, regardless of license state).

## What Skyhook controls

* The signing private key.
* The license-server URL.
* The terms of the Subscription Agreement that backs warn-only enforcement.
