---
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*