Merged
Size
XL
Change Breakdown
Feature55%
Refactor30%
Security15%
#28136feat(core): Add DB-backed TrustedKeyService with leader refresh and crypto cache (no-changelog)

Trusted keys now database-backed for multi-instance consistency

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 JtiCleanupService pattern: initialize() + @OnLeaderTakeover / @OnLeaderStepdown / @OnShutdown decorators
  • 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 repeated createPublicKey() calls when key material is unchanged
  • Issuer-scoped lookup: getByKidgetByKidAndIss — token exchange now validates iss claim 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: TrustedKeyData is 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

  1. Configure N8N_TOKEN_EXCHANGE_TRUSTED_KEYS with 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

  1. Start n8n — verify leader logs show source sync, refresh, and "healthy" status
  2. Query the trusted_key and trusted_key_source tables — verify keys and source rows are present
  3. 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, or Backport to v1 (if the PR is an urgent fix that needs to be backported)
© 2026 · via Gitpulse