Harden platform security, reliability, and CI after full audit.
Close deployment IDOR and gate stub payment endpoints, add production secret validation, health probes, Redis-backed build progress, GitHub Actions CI, expanded tests, billing/k8s refactors, and ops runbooks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -84,6 +84,11 @@ export default () => ({
|
||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||
},
|
||||
|
||||
cluster: {
|
||||
/** AES-256-GCM key for encrypting stored kubeconfigs. Required in production. */
|
||||
kubeconfigKey: process.env.CLUSTER_KUBECONFIG_KEY || '',
|
||||
},
|
||||
|
||||
// OTP SMS. Provider selectable via SMS_PROVIDER ('mizbansms' | 'kavenegar').
|
||||
sms: {
|
||||
provider: (process.env.SMS_PROVIDER || 'mizbansms').trim().toLowerCase(),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { validateProductionConfig } from './validate-production-config';
|
||||
|
||||
describe('validateProductionConfig', () => {
|
||||
const env = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...env };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = env;
|
||||
});
|
||||
|
||||
it('does nothing in development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.JWT_SECRET;
|
||||
expect(() => validateProductionConfig()).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws in production when secrets are missing or default', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.JWT_SECRET = 'default-jwt-secret';
|
||||
process.env.JWT_REFRESH_SECRET = 'default-refresh-secret';
|
||||
process.env.DB_PASSWORD = 'cloudhost_secret';
|
||||
|
||||
expect(() => validateProductionConfig()).toThrow(/Production configuration validation failed/);
|
||||
expect(() => validateProductionConfig()).toThrow(/JWT_SECRET/);
|
||||
expect(() => validateProductionConfig()).toThrow(/CLUSTER_KUBECONFIG_KEY/);
|
||||
});
|
||||
|
||||
it('passes in production with strong secrets', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.JWT_SECRET = 'a-very-long-random-production-secret';
|
||||
process.env.JWT_REFRESH_SECRET = 'another-very-long-random-refresh-secret';
|
||||
process.env.DB_PASSWORD = 'strong-db-password-here';
|
||||
process.env.CLUSTER_KUBECONFIG_KEY = '0123456789abcdef0123456789abcdef';
|
||||
|
||||
expect(() => validateProductionConfig()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
const DEFAULT_JWT_SECRET = 'default-jwt-secret';
|
||||
const DEFAULT_REFRESH_SECRET = 'default-refresh-secret';
|
||||
const DEFAULT_DB_PASSWORD = 'cloudhost_secret';
|
||||
|
||||
export function validateProductionConfig(): void {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return;
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
const jwtSecret = process.env.JWT_SECRET || DEFAULT_JWT_SECRET;
|
||||
const refreshSecret = process.env.JWT_REFRESH_SECRET || DEFAULT_REFRESH_SECRET;
|
||||
const dbPassword = process.env.DB_PASSWORD || DEFAULT_DB_PASSWORD;
|
||||
|
||||
if (!process.env.JWT_SECRET || jwtSecret === DEFAULT_JWT_SECRET) {
|
||||
errors.push('JWT_SECRET must be set to a strong random value in production');
|
||||
}
|
||||
if (!process.env.JWT_REFRESH_SECRET || refreshSecret === DEFAULT_REFRESH_SECRET) {
|
||||
errors.push('JWT_REFRESH_SECRET must be set to a strong random value in production');
|
||||
}
|
||||
if (!process.env.DB_PASSWORD || dbPassword === DEFAULT_DB_PASSWORD) {
|
||||
errors.push('DB_PASSWORD must be changed from the default in production');
|
||||
}
|
||||
if (!process.env.CLUSTER_KUBECONFIG_KEY?.trim()) {
|
||||
errors.push('CLUSTER_KUBECONFIG_KEY must be set in production to encrypt stored kubeconfigs');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(
|
||||
`Production configuration validation failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user