From 9aaec4f98aabc3d45f2f2a4389f0f3ab6f8e8661 Mon Sep 17 00:00:00 2001 From: Juan Sugg Date: Wed, 15 Jul 2026 04:17:44 -0300 Subject: [PATCH] fix(config): require TLS credentials only when TLS_ENABLED is not false Production required TLS_KEY and TLS_CERT whenever NODE_ENV=production, with no TLS_ENABLED condition, even though startApplicationRuntime already skips certificate loading and the HTTPS listener entirely when TLS terminates at the platform edge. The service therefore had to carry two real certificates that are never read, purely to satisfy a validator that should not have been asking for them. Add TLS_ENABLED to the schema and gate the certificate requirement on it, mirroring config/index.js:84 semantics where any value other than 'false' enables TLS. The runtime already implemented the intended behavior; only the validator disagreed. before: NODE_ENV=production TLS_ENABLED=false, no certs -> Config validation error: "TLS_KEY" is required after: -> boots TLS enabled without certificates still rejects, in production and nowhere else. Both contracts are pinned by unit tests so they cannot regress. --- src/utils/validateEnvVars.js | 18 +++++- tests/unit/utils/validateEnvVars.test.js | 80 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/utils/validateEnvVars.js b/src/utils/validateEnvVars.js index bf25214f2c..3f5e62164d 100644 --- a/src/utils/validateEnvVars.js +++ b/src/utils/validateEnvVars.js @@ -50,15 +50,27 @@ const envVarsSchema = Joi.object({ CLUSTER_SHUTDOWN_TIMEOUT_MS: Joi.number().integer().min(1).optional(), REDIS_URL: Joi.string().pattern(/^rediss?:\/\//).optional(), - // TLS certs are required in production + // Mirrors config/index.js: any value other than 'false' enables TLS. + TLS_ENABLED: Joi.string().valid('true', 'false').optional(), + + // TLS certs are required in production unless TLS terminates at the platform + // edge (TLS_ENABLED=false), where startApplicationRuntime never loads them. TLS_KEY: Joi.string().when('NODE_ENV', { is: 'production', - then: Joi.required(), + then: Joi.when('TLS_ENABLED', { + is: 'false', + then: Joi.optional(), + otherwise: Joi.required(), + }), otherwise: Joi.optional(), }), TLS_CERT: Joi.string().when('NODE_ENV', { is: 'production', - then: Joi.required(), + then: Joi.when('TLS_ENABLED', { + is: 'false', + then: Joi.optional(), + otherwise: Joi.required(), + }), otherwise: Joi.optional(), }), OUTBOUND_CA_BUNDLE_FILE: Joi.string().optional(), diff --git a/tests/unit/utils/validateEnvVars.test.js b/tests/unit/utils/validateEnvVars.test.js index 6c10f405d0..6b2d831010 100644 --- a/tests/unit/utils/validateEnvVars.test.js +++ b/tests/unit/utils/validateEnvVars.test.js @@ -174,12 +174,92 @@ describe('Unit | Utils | Validate Env Vars', () => { NODE_ENV: 'production', REPLICATE_API_TOKEN: 'test-token', }, + remove: ['TLS_KEY', 'TLS_CERT', 'TLS_ENABLED'], + }); + + expect(() => validateEnvVars()).toThrow(/TLS_KEY/); + }); + + it('does not require production TLS credentials when TLS_ENABLED=false', () => { + const validateEnvVars = loadValidator({ + overrides: { + NODE_ENV: 'production', + TLS_ENABLED: 'false', + REPLICATE_API_TOKEN: 'test-token', + }, + remove: ['TLS_KEY', 'TLS_CERT'], + }); + + expect(() => validateEnvVars()).not.toThrow(); + }); + + it('requires production TLS credentials when TLS_ENABLED=true', () => { + const validateEnvVars = loadValidator({ + overrides: { + NODE_ENV: 'production', + TLS_ENABLED: 'true', + REPLICATE_API_TOKEN: 'test-token', + }, remove: ['TLS_KEY', 'TLS_CERT'], }); expect(() => validateEnvVars()).toThrow(/TLS_KEY/); }); + it('requires TLS_CERT in production when only TLS_KEY is supplied', () => { + const validateEnvVars = loadValidator({ + overrides: { + NODE_ENV: 'production', + REPLICATE_API_TOKEN: 'test-token', + TLS_KEY: 'tls-key', + }, + remove: ['TLS_CERT', 'TLS_ENABLED'], + }); + + expect(() => validateEnvVars()).toThrow(/TLS_CERT/); + }); + + it('accepts TLS_ENABLED=false in production alongside unused TLS credentials', () => { + const validateEnvVars = loadValidator({ + overrides: { + NODE_ENV: 'production', + TLS_ENABLED: 'false', + REPLICATE_API_TOKEN: 'test-token', + TLS_KEY: 'tls-key', + TLS_CERT: 'tls-cert', + }, + }); + + expect(() => validateEnvVars()).not.toThrow(); + }); + + it('rejects a TLS_ENABLED value other than true or false', () => { + const validateEnvVars = loadValidator({ + overrides: { + NODE_ENV: 'production', + TLS_ENABLED: 'disabled', + REPLICATE_API_TOKEN: 'test-token', + TLS_KEY: 'tls-key', + TLS_CERT: 'tls-cert', + }, + }); + + expect(() => validateEnvVars()).toThrow(/TLS_ENABLED/); + }); + + it('leaves TLS credentials optional outside production when TLS_ENABLED=true', () => { + const validateEnvVars = loadValidator({ + overrides: { + NODE_ENV: 'development', + TLS_ENABLED: 'true', + REPLICATE_API_TOKEN: 'test-token', + }, + remove: ['TLS_KEY', 'TLS_CERT'], + }); + + expect(() => validateEnvVars()).not.toThrow(); + }); + it('accepts a non-negative TRUST_PROXY_HOPS override', () => { const validateEnvVars = loadValidator({ overrides: {