Merged
Size
L
Change Breakdown
Feature75%
Testing15%
Refactor10%
#28528fea: Pin secrets and signing

Signing secrets pinned to database

Signing secrets pinned to database
BG
BGZStephen
·Apr 15, 2026

Secrets that previously changed whenever the encryption key rotated are now stable across boots—user sessions, webhook URLs, and binary-data links survive key rotation without interruption.

The problem was that four critical secrets—instance ID, HMAC signature key, JWT signing key, and binary-data signing key—were derived fresh from the encryption key every time n8n started. Change the encryption key, and all active user sessions vanished, in-flight webhook resumption URLs broke, shared links stopped working, and telemetry/licensing lost track of the instance.

A two-phase initialization pattern now decouples these secrets from the encryption key. On first boot, the derived value is persisted to a new deployment_key database table. Every subsequent boot reads from the database instead of deriving again, so the secrets remain stable regardless of what happens to the encryption key.

The pattern handles multi-main deployments safely. When two instances race to insert a secret, an INSERT OR IGNORE RETURNING * query silently handles the conflict—the losing instance reads the winner's value without exceptions or locks.

Environment variables take precedence when set, allowing operators to pin secrets externally. The upgrade path is seamless: derived values are identical to the old behavior, so existing sessions continue working on first boot of the new version.

View Original GitHub DescriptionFact Check

Summary

This PR delivers two Linear tickets (IAM-485 and IAM-486) as a single unit because the IAM-486 work is directly layered on top of IAM-485.

Problem

n8n derives four signing secrets directly from the encryption key at every boot:

SecretUsed forDerivation
instanceIdTelemetry, licenceSHA256(encryptionKey.slice(mid))
hmacSignatureSecretWebhook resumption URL signingSHA256("hmac-signature:" + encryptionKey)
jwtSecretUser session JWT signingSHA256("n8n:" + encryptionKey)
signingSecretBinary-data presigned URL signingSHA256("url-signing:" + encryptionKey)

Rotating the encryption key immediately and silently invalidates all active user sessions, in-flight webhook resumption URLs, shared binary-data links, and instance identity used for telemetry/licensing.

Solution

A two-phase initialisation pattern that decouples each secret from the encryption key after first boot:

  1. Constructor — always derives the value from the encryption key (backward-compatible fallback; no change on upgrade).
  2. initialize(repo) — called once after DB migrations complete. Precedence: env var → active DB row → insertOrIgnore (persist) → race-condition fallback read.

The derived value is persisted to the deployment_key table on first boot. Every subsequent boot reads from DB, so the secret is stable regardless of what happens to the encryption key.

Concurrency safety (multi-main)

DeploymentKeyRepository.insertOrIgnore() issues INSERT OR IGNORE RETURNING *. On a unique-index conflict (two mains racing at startup), the insert is silently dropped and null is returned; the caller reads the winner's row. No exceptions, no locks.


Changes

packages/@n8n/db (IAM-485)

  • New entity DeploymentKeyid, type, value, status, algorithm, timestamps
  • New repository DeploymentKeyRepository with findActiveByType() and insertOrIgnore()
  • New migration 1777000000000-CreateDeploymentKeyTable — table + unique partial indexes for instance.id, signing.hmac, signing.jwt, signing.binary_data

packages/core / InstanceSettings (IAM-485 + IAM-486)

  • instanceId persisted to deployment_key (type instance.id) via initialize(repo)
  • hmacSignatureSecret made mutable; persisted via same initialize(repo) call (type signing.hmac)
  • Duck-typed inline repo interface avoids circular package dependency (@n8n/dbn8n-core at runtime)

packages/core / BinaryDataConfig (IAM-486)

  • New initialize(repo) — env-var fast-path → DB read → insertOrIgnore → race fallback; type signing.binary_data

packages/cli / JwtService (IAM-486)

  • jwtSecret made private
  • New initialize(repo) — same pattern; type signing.jwt; respects N8N_USER_MANAGEMENT_JWT_SECRET

packages/cli / start.ts (IAM-486)

  • Calls initialize(DeploymentKeyRepository) for InstanceSettings, JwtService, and BinaryDataConfig in sequence after DB migrations, before licence init

Tests

  • instance-settings.test.ts — 8 new tests (4× instance.id, 4× signing.hmac)
  • jwt.service.test.ts — 4 new initialize() tests
  • binary-data.config.test.ts — 4 new initialize() tests

Each set covers: env-var fast-path, existing DB row, persist-on-first-boot, concurrent-insert race condition.


Upgrade path

No breaking changes. Derived values are identical to pre-upgrade; they are transparently persisted on first boot of the new version. Sessions, webhook URLs, and binary-data links continue to work without interruption.

Environment variables

VariableBehaviour
N8N_ENCRYPTION_KEYStill the derivation source on first boot
N8N_USER_MANAGEMENT_JWT_SECRETPinned; DB write skipped entirely
N8N_HMAC_SIGNATURE_SECRETPinned; DB write skipped entirely
N8N_BINARY_DATA_SIGNING_SECRETPinned; DB write skipped entirely
N8N_INSTANCE_IDPinned; DB write skipped entirely

Related Linear tickets

https://linear.app/n8n/issue/IAM-485 https://linear.app/n8n/issue/IAM-486

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • 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 urgent.
© 2026 · via Gitpulse