Files
homelab-frontend/JWT_VS_RANDOM_TOKEN.md
T

487 lines
11 KiB
Markdown

# JWT vs Random Tokens - Which is Better?
Analyzing token types for Authentik + SOPS workflow.
---
## Quick Comparison
| Aspect | JWT | Random Token |
|--------|-----|--------------|
| **Stateless** | ✅ Yes | ❌ Requires lookup |
| **Self-contained** | ✅ Yes (claims inside) | ❌ Opaque |
| **Can verify locally** | ✅ Yes (signature) | ❌ Must call Authentik |
| **Size** | ⚠️ Larger (~500 bytes) | ✅ Smaller (~32 bytes) |
| **Immediate revocation** | ❌ Hard (token already signed) | ✅ Easy |
| **Contains user info** | ✅ Yes | ❌ No |
| **Standard OAuth2** | ⚠️ Optional (Bearer tokens) | ✅ Standard |
| **Good for audit** | ✅ User/groups embedded | ⚠️ Need to log lookup |
| **Git-friendly** | ✅ Sign commits | ✅ Sign commits |
---
## Use Case: Authentik + SOPS + Git Hooks
### Scenario 1: Developer Pushes Secret Update
```
Developer:
$ export SOPS_TOKEN=$token
$ sops secrets/default/db-creds.enc.yaml
$ git push
Git server hook runs:
├─ Receive token from commit metadata
├─ Need to validate token
└─ Two options:
```
**With JWT Token:**
```
Git hook:
1. Extract JWT from commit
2. Verify signature locally (no API call)
3. Read claims: {sub: "[email protected]", groups: ["k8s:secret-admin"]}
4. Check: is user in k8s:secret-admin group?
5. Allow/reject commit
Benefits:
✅ No need to call Authentik API
✅ Token self-validates
✅ Can check groups locally
✅ Instant verification
✅ Works offline
```
**With Random Token:**
```
Git hook:
1. Extract token from commit
2. Call Authentik API: GET /application/o/introspect/
└─ "Is this token valid?"
3. If valid, check groups: GET /api/v3/users/{id}/groups/
4. If admin, allow commit
Problems:
❌ Need API call on every git push
❌ Slow (network latency)
❌ Requires network connectivity
❌ If Authentik is down, can't push
❌ Rate limiting risk (many API calls)
```
---
## JWT Structure (What's Inside)
```json
// Decoded JWT payload
{
"sub": "[email protected]",
"email": "[email protected]",
"name": "John Doe",
"groups": [
"k8s:secret-admin",
"k8s:namespace:default:editor"
],
"iat": 1705336200,
"exp": 1705422600, // Expires in 24 hours
"iss": "https://authentik.riotpiao.com",
"aud": "secrets-management"
}
```
**In git commit:**
```
commit abc1234...
Author: [email protected] <[email protected]>
Date: Tue Jan 16 14:30:00 2025 +0000
chore(secret): update db-creds
X-SOPS-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**Git hook can:**
```
1. Extract token from commit
2. Decode (no signature needed yet)
3. Read claims: sub, groups
4. Verify signature with Authentik's public key
5. Check groups locally
6. Decision: accept or reject
```
---
## JWT Benefits for This Use Case
### 1. Local Token Verification (No API Calls)
```typescript
// Git hook code (serverless, fast)
function validateJWT(token: string) {
const decoded = jwt.verify(token, PUBLIC_KEY);
// ✅ Done instantly
// ✅ No network call
// ✅ Can work offline
}
```
### 2. Embedded Claims (Audit Trail)
```
JWT contains:
├─ sub: who made the change
├─ groups: what permissions they had
├─ iat: when token was issued
└─ exp: when it expires
Git commit metadata automatically includes:
└─ Who, what groups, when
Complete audit trail without additional logging.
```
### 3. RBAC at Git Level
```typescript
// Git hook can enforce RBAC locally
function canPushSecrets(token: JWT) {
const groups = token.groups;
// Only k8s:secret-admin can push to secrets/
if (!groups.includes("k8s:secret-admin")) {
return false;
}
return true;
}
```
### 4. No Dependency on Authentik Being Up
```
If Authentik is temporarily down:
✅ Developers can still push (JWT validates locally)
✅ Works offline
❌ With random tokens: push fails
```
### 5. Temporal Can Verify Tokens Locally
```
Temporal workflow (token rotation):
└─ Reads old token from K8s
└─ Decodes JWT: check exp field
└─ "Is this token about to expire?"
└─ Generate new token
No API call needed to validate old token.
```
---
## Random Token Benefits
### 1. Standard OAuth2 Practice
```
Random tokens are what most OAuth2 providers use:
├─ Opaque (attacker doesn't know content)
├─ Standard bearer tokens
└─ What Authentik probably generates by default
```
### 2. Immediate Revocation
```
If token is compromised:
└─ Delete it from Authentik
└─ All git pushes instantly fail
With JWT:
└─ Token is still valid until exp time
└─ Attacker has until expiry
```
### 3. Smaller Size
```
JWT: ~500 bytes (base64 encoded)
Random: ~32 bytes (hex)
Difference:
✅ Slightly smaller in git metadata
❌ But negligible for practical purposes
```
### 4. Simpler Conceptually
```
Developers understand random tokens:
└─ "Here's a secret token, use it"
Developers don't understand JWT:
└─ "What's this long base64 string?"
└─ "Why do I need to decode it?"
```
---
## My Recommendation: JWT
Here's why for YOUR use case:
### 1. Git Hook Verification
```
With JWT:
Git hook validates token signature locally
└─ No Authentik API call
└─ Fast
└─ Works offline
└─ Scales infinitely
With random token:
Git hook calls Authentik API
└─ Network latency
└─ Authentik bottleneck
└─ Fails if Authentik down
└─ Rate limiting risk
```
### 2. Embedded RBAC
```
With JWT:
Git hook reads groups from token
└─ Only k8s:secret-admin can push to secrets/
└─ Enforced locally
└─ No additional database lookups
With random token:
Git hook calls Authentik API twice
└─ Introspect token
└─ Fetch groups
└─ Slower
```
### 3. Audit Trail
```
With JWT:
Git commit includes token claims
└─ User, groups, timestamp all in commit metadata
└─ Complete audit trail automatically
With random token:
Git commit has only token
└─ Need to log token → user mapping separately
└─ Additional logging overhead
```
### 4. Temporal Token Rotation
```
With JWT:
Temporal reads exp field
└─ "Token expires in 2 hours"
└─ No API call needed
With random token:
Temporal calls Authentik API
└─ "Is this token still valid?"
└─ Network call needed
```
---
## How to Get JWT from Authentik
### Option 1: OpenID Connect (Standard)
```
Authentik already supports OIDC:
Developer requests token:
$ curl -X POST https://authentik.../application/o/token/ \
-d "grant_type=password&username=user&password=pwd&scope=openid profile groups"
Response:
{
"access_token": "eyJhbGc...", ← JWT
"token_type": "Bearer",
"expires_in": 86400,
"id_token": "eyJhbGc..."
}
```
### Option 2: OAuth2 with JWT
```
Authentik OAuth2 provider settings:
├─ Enable: "Use claims-based access tokens"
├─ Token claims: username, email, groups, preferred_username
└─ Signature algorithm: RS256
Result: Access token is now JWT instead of random.
```
---
## Implementation: SOPS + JWT
```bash
# Developer gets JWT token from Authentik
$ token=$(oidc-client get-token)
$ echo $token # base64 encoded JWT, not random string
# SOPS commits with JWT
$ export SOPS_TOKEN=$token
$ sops secrets/default/db-creds.enc.yaml
$ git add . && git commit -m "update" && git push
# Git hook receives commit
$ git hook:
# 1. Extract SOPS_TOKEN from commit metadata
# 2. Decode JWT (no API call)
# 3. Verify signature with Authentik public key
# 4. Read claims: groups
# 5. Check: user in k8s:secret-admin?
# 6. Accept or reject push
```
---
## Comparison in Your Architecture
### Random Token Path
```
Developer → Authentik (get random token)
→ SOPS (encrypt + commit)
→ Git push
→ Git hook (calls Authentik API to validate)
→ ArgoCD
→ K8s
Network calls: 2 (Authentik for token + git hook validation)
```
### JWT Token Path
```
Developer → Authentik (get JWT token)
→ SOPS (encrypt + commit)
→ Git push
→ Git hook (validates JWT locally, no API call)
→ ArgoCD
→ K8s
Network calls: 1 (only Authentik for token issuance)
```
**Winner: JWT (fewer API calls)**
---
## Concerns with JWT
### Q: Can JWT be revoked immediately?
**A:** Not easily. Options:
```
1. Token expires (set exp field to short time, e.g., 1 hour)
└─ Temporal rotates daily anyway
2. Blacklist approach (store revoked JWTs)
└─ Git hook checks blacklist before validating
└─ But defeats purpose of stateless tokens
3. Accept that JWT lives until expiry
└─ In your case: Temporal rotates daily
└─ So max time a compromised token is valid: 24 hours
└─ Acceptable for homelab
```
### Q: Are JWTs vulnerable?
**A:** No, if configured right:
```
✅ Signed with RS256 (Authentik public key)
✅ Signature verified on every use
✅ Expiry checked (can't use expired token)
✅ Can't be forged (no private key to sign with)
```
### Q: Does Authentik support JWT?
**A:** Yes, fully:
```
Authentik has:
✅ OIDC (returns JWT id_token + access_token)
✅ OAuth2 with claims (can return JWT)
✅ Configuration for JWT signing algorithm
✅ Public key endpoint for verification
```
---
## Final Recommendation
### Use JWT if:
```
✅ You want fast git hook validation (no API calls)
✅ You want embedded RBAC (groups in token)
✅ You want complete audit trail (claims in metadata)
✅ You want resilience (works if Authentik down)
✅ You want to scale (no API bottleneck)
```
### Use Random Token if:
```
✅ Immediate revocation is critical
✅ You prefer standard OAuth2 approach
✅ Developers shouldn't see token contents
✅ Simplicity is more important than features
```
---
## My Advice
**Go with JWT.**
Here's why:
1. Authentik supports it natively
2. Git hook verification is instant (no API calls)
3. Embedded claims give you free audit trail
4. Works offline (resilient)
5. Perfect for homelab scale
**Setup:**
1. Configure Authentik OIDC provider
2. Enable "Use claims-based access tokens" option
3. Add groups to token claims
4. Developers get JWT instead of random string
5. Git hook validates JWT locally (no API call)
**Code example for git hook:**
```bash
#!/bin/bash
# .git/hooks/update (server-side)
token=$(git log -1 --pretty=%B | grep "X-SOPS-Token" | cut -d' ' -f2)
# Verify JWT signature
jwt verify $token --key /path/to/authentik/public.key
# Decode and check groups
groups=$(jwt decode $token | jq .groups)
if [[ ! "$groups" =~ "k8s:secret-admin" ]]; then
echo "Access denied: not in k8s:secret-admin group"
exit 1
fi
# Allow push
exit 0
```
That's it. Simple, elegant, no redundancy.