--- title: Offline JWT licence & daily‑run quota description: How Stella‑Ops enforces a **runs‑per‑day** limit in fully air‑gapped deployments. nav: order: 36 --- # JWT‑based daily‑run licence (offline‑capable) When *Stella‑Ops* scanners operate entirely **offline**, they cannot phone home for metering. Instead, the backend accepts a **signed JSON Web Token (JWT)** that states the **maximum number of scans per UTC day**. If no token is supplied, a _grace quota_ of **33 runs/24 h** applies. --- ## 1  Token contents | Claim | Purpose | Example | |-------|---------|---------| | `sub` | Customer / licensee identifier | `"f47ac10b…"` | | `iat` | Issued‑at timestamp | `1722566400` | | `exp` | Absolute licence expiry | `2025‑12‑31T23:59:59Z` | | `tier` | **Max scans per UTC day** | `{{ quota_token }}` | | `tid` | Token identifier (32‑byte) | `"7d2285..."` | | `pkg` | Product SKU / edition | `"stella‑core"` | Tokens are signed with **RS256** and verified locally using the bundled public key. Only the public key ships inside the container; the private key never leaves the build pipeline. --- ## 2  Obtaining a token 1. **Request** → `POST /​register { email:"alice@example.org" }` 2. Service hashes the e‑mail (SHA‑256), stores it, and issues a JWT (60 days by default). 3. Token is e‑mailed to you. A new request for the same e‑mail returns the **same** token until it nears expiry, avoiding quota “top‑ups” by re‑registration. --- ## 3  Supplying the token to an air‑gapped stack ```bash # recommended docker run \ -v /opt/stella/license/alice.jwt:/run/secrets/stella_license.jwt:ro \ stella‑ops ```` Other supported paths: | Method | Mount point | Hot‑reload | | ------------- | ------------------------ | ----------- | | Docker secret | `/run/secrets/…` | ✓ (inotify) | | Bind‑mounted | user‑chosen path (above) | ✓ | | Env variable | `STELLA_LICENSE_JWT` | ✗ restart | --- ## 4  Quota‑enforcement algorithm ```mermaid flowchart TD Start --> Verify[Verify JWT signature] Verify -->|Invalid| Deny1[Run in non licensed mode] Verify --> Load[load today's counter UTC] Load -->|SUM of last 24h scans < daily_quota| Permit[allow scan, add scan] Permit --> End Load -->|SUM of last 24h scans ≥ daily_quota| Deny1 ``` ## 5  Renewal procedure | Scenario | Action | | -------------- | --------------------------------------------------------------------------------- | | More capacity | Request new token with higher `daily_quota`; replace file – **no restart needed** | | Licence expiry | Same as above; new `exp` date | | Key rotation | Container image ships new public key(s); older tokens still verify | --- ## 6  Fallback limits | Situation | Daily quota | | ----------------------- | ----------------------------------- | | Valid JWT present | value of `daily_quota` claim ({{ quota_token }}) | | No JWT | **33** | | JWT expired (if used) | treated as **anonymous** unless policy enforces hard‑fail | | Token signature invalid | **0** (reject) | --- ## 7  Threat‑model highlights (future work / optional hardening) | Threat | Mitigation | | --------------------------- | ---------------------------------------------------------------------- | | Copy token & DB to 2nd node | Bind `sub`/`tid` to host fingerprint (TPM EK) – optional enterprise control | | Counter DB rollback | Hash‑chain + monotonic clock – optional enterprise control | | Flooding single node | Redis‑backed cluster rate‑limit (30 hits / 60 s) + edge Nginx (20 r/s) | | Key compromise | Rotate RS256 key‑pair, ship new pubkey, re‑sign tokens | --- ## 8  Anonymous (33 runs) mode Offline PoCs without registration still work: ```bash docker compose exec stella-ops stella-jwt reload # reloads, discovers no token ``` …but **production deployments *must* register** to unlock real‑world quotas and receive security advisories via e‑mail. --- *Last updated: 2025‑08‑02*