diff --git a/packages/@n8n/config/src/configs/security.config.ts b/packages/@n8n/config/src/configs/security.config.ts new file mode 100644 index 0000000000000..329e84cc433f3 --- /dev/null +++ b/packages/@n8n/config/src/configs/security.config.ts @@ -0,0 +1,27 @@ +import { Config, Env } from '../decorators'; + +@Config +export class SecurityConfig { + /** + * Which directories to limit n8n's access to. Separate multiple dirs with semicolon `;`. + * + * @example N8N_RESTRICT_FILE_ACCESS_TO=/home/user/.n8n;/home/user/n8n-data + */ + @Env('N8N_RESTRICT_FILE_ACCESS_TO') + restrictFileAccessTo: string = ''; + + /** + * Whether to block access to all files at: + * - the ".n8n" directory, + * - the static cache dir at ~/.cache/n8n/public, and + * - user-defined config files. + */ + @Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES') + blockFileAccessToN8nFiles: boolean = true; + + /** + * In a [security audit](https://docs.n8n.io/hosting/securing/security-audit/), how many days for a workflow to be considered abandoned if not executed. + */ + @Env('N8N_SECURITY_AUDIT_DAYS_ABANDONED_WORKFLOW') + daysAbandonedWorkflow: number = 90; +} diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index 9ebfb124651b0..1a2a3127adada 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -13,6 +13,7 @@ import { NodesConfig } from './configs/nodes.config'; import { PublicApiConfig } from './configs/public-api.config'; import { TaskRunnersConfig } from './configs/runners.config'; import { ScalingModeConfig } from './configs/scaling-mode.config'; +import { SecurityConfig } from './configs/security.config'; import { SentryConfig } from './configs/sentry.config'; import { TemplatesConfig } from './configs/templates.config'; import { UserManagementConfig } from './configs/user-management.config'; @@ -22,6 +23,7 @@ import { Config, Env, Nested } from './decorators'; export { Config, Env, Nested } from './decorators'; export { TaskRunnersConfig } from './configs/runners.config'; +export { SecurityConfig } from './configs/security.config'; export { LOG_SCOPES } from './configs/logging.config'; export type { LogScope } from './configs/logging.config'; @@ -106,4 +108,7 @@ export class GlobalConfig { @Nested license: LicenseConfig; + + @Nested + security: SecurityConfig; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index eabcd5c489672..cd5438e248c41 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -265,6 +265,11 @@ describe('GlobalConfig', () => { tenantId: 1, cert: '', }, + security: { + restrictFileAccessTo: '', + blockFileAccessToN8nFiles: true, + daysAbandonedWorkflow: 90, + }, }; it('should use all default values when no env variables are defined', () => { diff --git a/packages/cli/src/commands/audit.ts b/packages/cli/src/commands/audit.ts index e86c8c9ab5e79..e98bb8bce026d 100644 --- a/packages/cli/src/commands/audit.ts +++ b/packages/cli/src/commands/audit.ts @@ -1,8 +1,8 @@ +import { SecurityConfig } from '@n8n/config'; import { Flags } from '@oclif/core'; import { ApplicationError } from 'n8n-workflow'; import { Container } from 'typedi'; -import config from '@/config'; import { RISK_CATEGORIES } from '@/security-audit/constants'; import { SecurityAuditService } from '@/security-audit/security-audit.service'; import type { Risk } from '@/security-audit/types'; @@ -26,7 +26,7 @@ export class SecurityAudit extends BaseCommand { }), 'days-abandoned-workflow': Flags.integer({ - default: config.getEnv('security.audit.daysAbandonedWorkflow'), + default: Container.get(SecurityConfig).daysAbandonedWorkflow, description: 'Days for a workflow to be considered abandoned if not executed', }), }; diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 9b711f9766cc7..8bece9199ab31 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -187,29 +187,6 @@ export const schema = { doc: 'Public URL where the editor is accessible. Also used for emails sent from n8n.', }, - security: { - restrictFileAccessTo: { - doc: 'If set only files in that directories can be accessed. Multiple directories can be separated by semicolon (";").', - format: String, - default: '', - env: 'N8N_RESTRICT_FILE_ACCESS_TO', - }, - blockFileAccessToN8nFiles: { - doc: 'If set to true it will block access to all files in the ".n8n" directory, the static cache dir at ~/.cache/n8n/public, and user defined config files.', - format: Boolean, - default: true, - env: 'N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES', - }, - audit: { - daysAbandonedWorkflow: { - doc: 'Days for a workflow to be considered abandoned if not executed', - format: Number, - default: 90, - env: 'N8N_SECURITY_AUDIT_DAYS_ABANDONED_WORKFLOW', - }, - }, - }, - workflowTagsDisabled: { format: Boolean, default: false, diff --git a/packages/cli/src/security-audit/risk-reporters/credentials-risk-reporter.ts b/packages/cli/src/security-audit/risk-reporters/credentials-risk-reporter.ts index ab7873e80813f..0c8d84211e0c2 100644 --- a/packages/cli/src/security-audit/risk-reporters/credentials-risk-reporter.ts +++ b/packages/cli/src/security-audit/risk-reporters/credentials-risk-reporter.ts @@ -1,7 +1,7 @@ +import { SecurityConfig } from '@n8n/config'; import type { IWorkflowBase } from 'n8n-workflow'; import { Service } from 'typedi'; -import config from '@/config'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { ExecutionDataRepository } from '@/databases/repositories/execution-data.repository'; @@ -15,10 +15,11 @@ export class CredentialsRiskReporter implements RiskReporter { private readonly credentialsRepository: CredentialsRepository, private readonly executionRepository: ExecutionRepository, private readonly executionDataRepository: ExecutionDataRepository, + private readonly securityConfig: SecurityConfig, ) {} async report(workflows: WorkflowEntity[]) { - const days = config.getEnv('security.audit.daysAbandonedWorkflow'); + const days = this.securityConfig.daysAbandonedWorkflow; const allExistingCreds = await this.getAllExistingCreds(); const { credsInAnyUse, credsInActiveUse } = await this.getAllCredsInUse(workflows); diff --git a/packages/cli/src/security-audit/security-audit.service.ts b/packages/cli/src/security-audit/security-audit.service.ts index 19582450c4761..97b5424a19583 100644 --- a/packages/cli/src/security-audit/security-audit.service.ts +++ b/packages/cli/src/security-audit/security-audit.service.ts @@ -1,3 +1,4 @@ +import { SecurityConfig } from '@n8n/config'; import Container, { Service } from 'typedi'; import config from '@/config'; @@ -8,7 +9,10 @@ import { toReportTitle } from '@/security-audit/utils'; @Service() export class SecurityAuditService { - constructor(private readonly workflowRepository: WorkflowRepository) {} + constructor( + private readonly workflowRepository: WorkflowRepository, + private readonly securityConfig: SecurityConfig, + ) {} private reporters: { [name: string]: RiskReporter; @@ -19,7 +23,7 @@ export class SecurityAuditService { await this.initReporters(categories); - const daysFromEnv = config.getEnv('security.audit.daysAbandonedWorkflow'); + const daysFromEnv = this.securityConfig.daysAbandonedWorkflow; if (daysAbandonedWorkflow) { config.set('security.audit.daysAbandonedWorkflow', daysAbandonedWorkflow); diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 144540467f61f..2916f191f3f47 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -1,5 +1,5 @@ import type { FrontendSettings, ITelemetrySettings } from '@n8n/api-types'; -import { GlobalConfig } from '@n8n/config'; +import { GlobalConfig, SecurityConfig } from '@n8n/config'; import { createWriteStream } from 'fs'; import { mkdir } from 'fs/promises'; import uniq from 'lodash/uniq'; @@ -46,6 +46,7 @@ export class FrontendService { private readonly mailer: UserManagementMailer, private readonly instanceSettings: InstanceSettings, private readonly urlService: UrlService, + private readonly securityConfig: SecurityConfig, ) { loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes()); void this.generateTypes(); @@ -225,7 +226,7 @@ export class FrontendService { maxCount: config.getEnv('executions.pruneDataMaxCount'), }, security: { - blockFileAccessToN8nFiles: config.getEnv('security.blockFileAccessToN8nFiles'), + blockFileAccessToN8nFiles: this.securityConfig.blockFileAccessToN8nFiles, }, }; } diff --git a/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts index 4513beb6bb191..b5b4c122df1af 100644 --- a/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts @@ -1,7 +1,8 @@ +import type { SecurityConfig } from '@n8n/config'; +import { mock } from 'jest-mock-extended'; import Container from 'typedi'; import { v4 as uuid } from 'uuid'; -import config from '@/config'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { ExecutionDataRepository } from '@/databases/repositories/execution-data.repository'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; @@ -15,10 +16,15 @@ import * as testDb from '../shared/test-db'; let securityAuditService: SecurityAuditService; +const securityConfig = mock({ daysAbandonedWorkflow: 90 }); + beforeAll(async () => { await testDb.init(); - securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository)); + securityAuditService = new SecurityAuditService( + Container.get(WorkflowRepository), + securityConfig, + ); }); beforeEach(async () => { @@ -154,7 +160,7 @@ test('should report credential in not recently executed workflow', async () => { const workflow = await Container.get(WorkflowRepository).save(workflowDetails); const date = new Date(); - date.setDate(date.getDate() - config.getEnv('security.audit.daysAbandonedWorkflow') - 1); + date.setDate(date.getDate() - securityConfig.daysAbandonedWorkflow - 1); const savedExecution = await Container.get(ExecutionRepository).save({ finished: true, @@ -223,7 +229,7 @@ test('should not report credentials in recently executed workflow', async () => const workflow = await Container.get(WorkflowRepository).save(workflowDetails); const date = new Date(); - date.setDate(date.getDate() - config.getEnv('security.audit.daysAbandonedWorkflow') + 1); + date.setDate(date.getDate() - securityConfig.daysAbandonedWorkflow + 1); const savedExecution = await Container.get(ExecutionRepository).save({ finished: true, diff --git a/packages/cli/test/integration/security-audit/database-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/database-risk-reporter.test.ts index d519f97a238a9..3aef57396b666 100644 --- a/packages/cli/test/integration/security-audit/database-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/database-risk-reporter.test.ts @@ -1,3 +1,4 @@ +import { mock } from 'jest-mock-extended'; import Container from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -18,7 +19,7 @@ let securityAuditService: SecurityAuditService; beforeAll(async () => { await testDb.init(); - securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository)); + securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository), mock()); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/security-audit/filesystem-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/filesystem-risk-reporter.test.ts index 34bcb83b497ba..ceb306935ff5e 100644 --- a/packages/cli/test/integration/security-audit/filesystem-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/filesystem-risk-reporter.test.ts @@ -1,3 +1,4 @@ +import { mock } from 'jest-mock-extended'; import Container from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -13,7 +14,7 @@ let securityAuditService: SecurityAuditService; beforeAll(async () => { await testDb.init(); - securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository)); + securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository), mock()); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/security-audit/instance-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/instance-risk-reporter.test.ts index 4f355cbcbc603..928667b518d77 100644 --- a/packages/cli/test/integration/security-audit/instance-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/instance-risk-reporter.test.ts @@ -1,3 +1,4 @@ +import { mock } from 'jest-mock-extended'; import { NodeConnectionType } from 'n8n-workflow'; import Container from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -23,7 +24,7 @@ let securityAuditService: SecurityAuditService; beforeAll(async () => { await testDb.init(); - securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository)); + securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository), mock()); simulateUpToDateInstance(); }); diff --git a/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts index 133a574d40887..c1fb198b69bb0 100644 --- a/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts @@ -1,3 +1,4 @@ +import { mock } from 'jest-mock-extended'; import { Container } from 'typedi'; import { v4 as uuid } from 'uuid'; @@ -24,7 +25,7 @@ let securityAuditService: SecurityAuditService; beforeAll(async () => { await testDb.init(); - securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository)); + securityAuditService = new SecurityAuditService(Container.get(WorkflowRepository), mock()); }); beforeEach(async () => {