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