Trusted keys now database-backed for multi-instance consistency

The trusted key service has been rebuilt from an in-memory store to a database-backed service with leader-driven refresh. All instances now read from the same source, eliminating stale-key bugs in multi-instance deployments.
The trusted key service previously kept keys in an in-memory Map — fine for single-instance deployments, but a consistency nightmare when multiple instances need the same keys. Each worker would store its own copy, diverge over time, and cause token exchange failures that were hard to diagnose.
A leader instance now handles key refresh. On startup, it parses the static key configuration, syncs sources to the database, and runs a periodic refresh poller. Workers, meanwhile, never refresh — they just read from the database on every lookup. This means all instances see the same keys, always.
The token exchange flow was also tightened. Every incoming token now requires a valid iss claim, and that issuer is used as part of key resolution. This prevents a key meant for one tenant from being accidentally used to validate tokens for another.
The change lives in the token exchange module of the CLI package, part of a broader effort to harden n8n's token exchange authentication flow for multi-instance production deployments.
View Original GitHub Description
Summary
Refactors TrustedKeyService from an in-process Map to a DB-backed service with leader/worker separation. The leader resolves all configured key sources (static keys from env-var config) and writes them to the database on startup and on a periodic refresh interval. All instances — including workers — read keys from the database on every getByKidAndIss call, ensuring multi-instance consistency without stale reads.
This is part of the Embed auth flow (Phase 2a). The previous PR (IAM-523) added the DB tables, entities, and repositories for trusted keys and key sources. This PR wires the service layer to use them, completing the storage tier. The next step (IAM-472) will add a health-check endpoint for trusted key configuration.
Key implementation decisions:
- Leader lifecycle follows the
JtiCleanupServicepattern:initialize()+@OnLeaderTakeover/@OnLeaderStepdown/@OnShutdowndecorators - Per-source transactional refresh with advisory lock (
DbLock.TRUSTED_KEY_REFRESH): DELETE old keys → resolve cross-source kid conflicts (last-writer-wins) → INSERT new keys → UPDATE source status - Crypto cache: local
Map<kid, { keyMaterialHash, cryptoKey }>— DB is always queried, cache only avoids repeatedcreatePublicKey()calls when key material is unchanged - Issuer-scoped lookup:
getByKid→getByKidAndIss— token exchange now validatesissclaim and uses it for key resolution, preventing cross-tenant key confusion - Orphan cleanup: on startup, config-managed sources (static/jwks) not in the current config are removed; UI sources are preserved
- Zod validation on read path:
TrustedKeyDatais now a Zod schema, and entities are validated when read from DB to guard against corrupted data - JWKS/UI sources: recognized and persisted but skipped at refresh time with a warning (JWKS resolution is IAM-518)
How to test manually
- Configure
N8N_TOKEN_EXCHANGE_TRUSTED_KEYSwith a static key JSON array
export N8N_ENV_FEAT_TOKEN_EXCHANGE=true
export N8N_TOKEN_EXCHANGE_ENABLED=true
export N8N_TOKEN_EXCHANGE_KEY_REFRESH_INTERVAL_SECONDS=30
export N8N_TOKEN_EXCHANGE_TRUSTED_KEYS='[{"type":"static","kid":"test-key-1","algorithms":["RS256"],"key":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6jZKAsZVTVLAB9cc7xT\nEqBq3/OvO2bw90RAaZYF2erBxNkOs92Ed/NvmhM2jGLR5Ov8tBbQ3sbxzPf3Tv30\n4zP+SpFVqGkXibRQoB6F+7rW2Zm3b8mPqUTJa4bMmc//G3YFSCIddym0LYMED5Xa\nzSvhXTCKfEsSBOr8wKGCV5gbsbl3UZ3HZTA3gqSpE83Pf7l6QDU7ytuVSuV9elIb\njTyrUtL6i6oUgjgCyrxVgnHFjESLRI+NCGeBmua3uvWp8xCiJ+9jMc054M4ltFV1\n+NSFJE9nFYuh7IRkYz+onZtMIi5y84QHVPhRBKdTKbOL8u6Lfyuij2KGNPD08g+b\nSQIDAQAB\n-----END PUBLIC KEY-----","issuer":"https://test-issuer.example.com","expectedAudience":"https://n8n.example.com"}]'
These are example envvars
- Start n8n — verify leader logs show source sync, refresh, and "healthy" status
- Query the
trusted_keyandtrusted_key_sourcetables — verify keys and source rows are present - Stop/restart — verify keys are re-synced from config
Related Linear tickets, Github issues, and Community forum posts
closes https://linear.app/n8n/issue/IAM-524 closes https://linear.app/n8n/issue/IAM-466/2a3-trustedkeystore-db-backed-storage-and-multi-instance-sync
Review / Merge checklist
- PR title and summary are descriptive. (conventions)
- Docs updated or follow-up ticket created.
- Tests included.
- PR Labeled with
Backport to Beta,Backport to Stable, orBackport to v1(if the PR is an urgent fix that needs to be backported)