Initial commit (history squashed)
	
		
			
	
		
	
	
		
	
		
			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
				
			
		
		
	
	
				
					
				
			
		
			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
				
			This commit is contained in:
		
							
								
								
									
										123
									
								
								docs/license-jwt-quota.md
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										123
									
								
								docs/license-jwt-quota.md
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| --- | ||||
| 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* | ||||
		Reference in New Issue
	
	Block a user