refactor(jwt): gate signing-key auto-rotation cron on SIGNING_KEY_ROTATION_DAYS (#20866)
Only register the JWT signing-key rotation cron when `SIGNING_KEY_ROTATION_DAYS` is set, and move that variable to Advanced Settings.
This commit is contained in:
@@ -16,10 +16,9 @@ Each key carries a `publicKey` (kept indefinitely so it can verify previously is
|
||||
|
||||
### Rotate the current key
|
||||
|
||||
- **Manual** — **Settings → Admin Panel → Signing keys → Revoke** on the current row. Revoking wipes its encrypted private material and demotes it; the next sign call automatically mints a fresh ES256 keypair as the new current. Tokens signed under any other (non-revoked) `kid` keep verifying until they expire.
|
||||
- **Enterprise (automatic)** — a daily cron (`'15 3 * * *'` UTC) issues a new current key once the existing one has been current for `SIGNING_KEY_ROTATION_DAYS` (default `90`). The previous key is *not* revoked, so tokens signed under it keep verifying. Register it once with `yarn command:prod cron:register:all`.
|
||||
Set `SIGNING_KEY_ROTATION_DAYS` to opt in: a daily cron then issues a new current key once the existing one is older than that threshold. Previous keys are *not* revoked, so tokens signed under them keep verifying. Leave the variable unset to disable auto-rotation.
|
||||
|
||||
<Note>The Enterprise cron and `SIGNING_KEY_ROTATION_DAYS` ship in v2.6+.</Note>
|
||||
<Note>Auto-rotation ships in v2.6+.</Note>
|
||||
|
||||
### Revoke a key (leak / emergency only)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command';
|
||||
import { StaleRegistrationCleanupCronCommand } from 'src/engine/core-modules/application/application-oauth/stale-registration-cleanup/commands/stale-registration-cleanup.cron.command';
|
||||
@@ -10,6 +11,7 @@ import { EventLogCleanupCronCommand } from 'src/engine/core-modules/event-logs/c
|
||||
import { RotateSigningKeysCronCommand } from 'src/engine/core-modules/jwt/crons/commands/rotate-signing-keys.cron.command';
|
||||
import { CronTriggerCronCommand } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.command';
|
||||
import { CheckPublicDomainsValidRecordsCronCommand } from 'src/engine/core-modules/public-domain/crons/commands/check-public-domains-valid-records.cron.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-modules/workspace/crons/commands/check-custom-domain-valid-records.cron.command';
|
||||
import { TrashCleanupCronCommand } from 'src/engine/trash-cleanup/commands/trash-cleanup.cron.command';
|
||||
import { CleanOnboardingWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-onboarding-workspaces.cron.command';
|
||||
@@ -62,6 +64,7 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
private readonly marketplaceCatalogSyncCronCommand: MarketplaceCatalogSyncCronCommand,
|
||||
private readonly applicationVersionCheckCronCommand: ApplicationVersionCheckCronCommand,
|
||||
private readonly staleRegistrationCleanupCronCommand: StaleRegistrationCleanupCronCommand,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -69,6 +72,10 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
async run(): Promise<void> {
|
||||
this.logger.log('Registering all background sync cron jobs...');
|
||||
|
||||
const isSigningKeyAutoRotationEnabled = isDefined(
|
||||
this.twentyConfigService.get('SIGNING_KEY_ROTATION_DAYS'),
|
||||
);
|
||||
|
||||
const allCommands = [
|
||||
{
|
||||
name: 'MessagingMessagesImport',
|
||||
@@ -161,6 +168,7 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
{
|
||||
name: 'RotateSigningKeys',
|
||||
command: this.rotateSigningKeysCronCommand,
|
||||
isEnabled: isSigningKeyAutoRotationEnabled,
|
||||
},
|
||||
{
|
||||
name: 'StaleRegistrationCleanup',
|
||||
@@ -172,8 +180,15 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
let failureCount = 0;
|
||||
const failures: string[] = [];
|
||||
const successes: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const { name, command, isEnabled = true } of allCommands) {
|
||||
if (!isEnabled) {
|
||||
this.logger.log(`Skipping ${name} cron job (disabled by config)`);
|
||||
skipped.push(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const { name, command } of allCommands) {
|
||||
try {
|
||||
this.logger.log(`Registering ${name} cron job...`);
|
||||
await command.run();
|
||||
@@ -188,7 +203,7 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Cron job registration completed: ${successCount} successful, ${failureCount} failed`,
|
||||
`Cron job registration completed: ${successCount} successful, ${failureCount} failed, ${skipped.length} skipped`,
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
@@ -198,5 +213,9 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
if (successCount > 0) {
|
||||
this.logger.log(`Successful commands: ${successes.join(', ')}`);
|
||||
}
|
||||
|
||||
if (skipped.length > 0) {
|
||||
this.logger.log(`Skipped commands: ${skipped.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,9 +1185,9 @@ export class ConfigVariables {
|
||||
FALLBACK_ENCRYPTION_KEY: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.SERVER_CONFIG,
|
||||
group: ConfigVariablesGroup.ADVANCED_SETTINGS,
|
||||
description:
|
||||
'Number of days after which the Enterprise auto-rotation cron issues a new current JWT signing key. When unset, the cron is a no-op. Previous keys remain in the database to keep verifying tokens they signed; revocation stays a manual admin action.',
|
||||
'Days the current JWT signing key stays valid before the rotation cron issues a new one. Leave unset to disable auto-rotation.',
|
||||
type: ConfigVariableType.NUMBER,
|
||||
})
|
||||
@CastToPositiveNumber()
|
||||
|
||||
Reference in New Issue
Block a user