Skip to main content

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

{
  "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 successBehavior
0-7Silent.
7-14Yellow “Cannot reach license server” banner in the web app.
14-30Red 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.
helm upgrade radar-hub skyhook/radar-hub --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.