Some checks failed
		
		
	
	Build Test Deploy / authority-container (push) Has been cancelled
				
			Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Build Test Deploy / build-test (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
			
				
	
	
		
			123 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Markdown
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			123 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Markdown
		
	
	
		
			Executable File
		
	
	
	
	
---
 | 
						||
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* |