Identity-Backend - Service secrets, scale to KID

Scaling JWT Secrets: From Single Key to Key Rotation with kid

When you first start building microservices, a single shared secret for JWT signing and verification feels simple and sufficient. But as soon as you introduce multiple clients, microservices, and the need for key rotation, this model starts breaking down.

We just ran into this exact scenario with our identity-backend and logging-backend services.


Current Setup

  • Identity-Backend: Issues JWT tokens signed with JWT_SECRET_KEY.

  • Logging-Backend: Validates JWT tokens using the same secret.

  • Audience (aud) and Issuer (iss): Hardcoded to enforce proper service-to-service calls.

  • Single Secret in .env: Shared between both services.

Pros:

  • Very simple to implement.

  • Easy to debug in early MVP stages.

Cons:

  • Any secret rotation requires redeploying all services simultaneously.

  • Impossible to issue different secrets for different clients.

  • No audit trail for which key signed what.


Next Evolution: Key IDs (kid) and Client Secrets

We’re introducing key IDs (kid) in JWT headers and moving to a model where identity-backend manages multiple keys in a SQLite database:

  • Each client has:

    • client_id (e.g., careergpt-backend)

    • key_id (e.g., key-2025-01)

    • secret (random string)

  • JWTs are issued with:

    • kid in the header (so consumers know which key to verify against).

  • logging-backend extracts kid and retrieves the right secret to validate the token.

This enables:

  1. Per-Client Secrets: Different clients get unique keys.

  2. Key Rotation: Add a new key, start signing with it, keep verifying old tokens until expiry.

  3. Audit & Traceability: Easily identify which key signed which token.


How Key Rotation Works

  1. Add new key to identity-backend DB (e.g., key-2025-02).

  2. Start issuing tokens with new key.

  3. Keep old key in DB for verification until tokens expire.

  4. Remove old key when no longer needed.


Implementation Plan

Step 1: Identity-Backend

  • Add client_secrets table.

  • Endpoints:

    • POST /add-client-secret → Add new key for a client.

    • GET /list-client-secrets → Admin view of active keys.

Step 2: Logging-Backend

  • Extract kid from JWT header.

  • Request correct secret from Identity-Backend for verification.

Step 3: Transition

  • Keep current single-secret fallback (JWT_SECRET_KEY) until rollout is complete.

  • Gradually migrate clients to new key-ID model.


Why This Matters

This pattern future-proofs your architecture for:

  • Microservice sprawl (multiple services, each with unique secrets).

  • Zero-downtime rotations (no redeploys to rotate secrets).

  • Enhanced security posture (different secrets = limited blast radius).


Next Post

In the next post, we’ll share the code changes:

  • How to embed kid in JWT headers using PyJWT.

  • Database schema and migrations.

  • Verification logic changes in logging-backend.


Comments

Popular posts from this blog

Feature: Audit log for one login, and identity service

Getting started - Build your data science lab environment

QA - Run #1 - Results