# 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: "user@example.com", 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": "user@example.com", "email": "user@example.com", "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: user@example.com 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.