fix(runtime): harden smoke and credential boundaries
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const REQUIRED = [
|
||||
'DOMAIN',
|
||||
'ACME_EMAIL',
|
||||
'CORE_REPOSITORY_URL',
|
||||
'POSTGRES_PASSWORD',
|
||||
'REDIS_PASSWORD',
|
||||
'GAME_TOKEN_SECRET',
|
||||
'GATEWAY_BOOTSTRAP_TOKEN',
|
||||
'INITIAL_ADMIN_USERNAME',
|
||||
'INITIAL_ADMIN_PASSWORD',
|
||||
'KAKAO_REST_KEY',
|
||||
];
|
||||
|
||||
const MIN_LENGTH = new Map([
|
||||
['POSTGRES_PASSWORD', 24],
|
||||
['REDIS_PASSWORD', 24],
|
||||
['GAME_TOKEN_SECRET', 32],
|
||||
['GATEWAY_BOOTSTRAP_TOKEN', 32],
|
||||
['INITIAL_ADMIN_PASSWORD', 16],
|
||||
]);
|
||||
|
||||
const isPlaceholder = (value) =>
|
||||
value.startsWith('replace-with-') || value.includes('example.com') || value.includes('your-org');
|
||||
|
||||
const decodeBase64 = (value) => {
|
||||
try {
|
||||
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) return null;
|
||||
return Buffer.from(value, 'base64').toString('utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const validateEnvironment = (env) => {
|
||||
const errors = [];
|
||||
for (const key of REQUIRED) {
|
||||
const value = env[key]?.trim() ?? '';
|
||||
if (!value) errors.push(`${key} is required`);
|
||||
else if (isPlaceholder(value)) errors.push(`${key} still contains an example placeholder`);
|
||||
}
|
||||
|
||||
for (const [key, length] of MIN_LENGTH) {
|
||||
const value = env[key] ?? '';
|
||||
if (value && value.length < length) errors.push(`${key} must contain at least ${length} characters`);
|
||||
}
|
||||
|
||||
const domain = env.DOMAIN?.trim() ?? '';
|
||||
if (domain && !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(domain)) {
|
||||
errors.push('DOMAIN must be a hostname without a scheme, path, or port');
|
||||
}
|
||||
|
||||
const email = env.ACME_EMAIL?.trim() ?? '';
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.push('ACME_EMAIL must be a valid email address');
|
||||
|
||||
const repositoryUrl = env.CORE_REPOSITORY_URL?.trim() ?? '';
|
||||
if (repositoryUrl) {
|
||||
if (/^https?:\/\//i.test(repositoryUrl)) {
|
||||
try {
|
||||
const parsed = new URL(repositoryUrl);
|
||||
if (parsed.username || parsed.password) errors.push('CORE_REPOSITORY_URL must not embed credentials');
|
||||
} catch {
|
||||
errors.push('CORE_REPOSITORY_URL is not a valid HTTP(S) URL');
|
||||
}
|
||||
} else if (!/^(?:ssh:\/\/|git@)[^\s]+/i.test(repositoryUrl)) {
|
||||
errors.push('CORE_REPOSITORY_URL must use HTTP(S) or SSH');
|
||||
}
|
||||
}
|
||||
|
||||
const httpsUser = env.CORE_REPOSITORY_USERNAME?.trim() ?? '';
|
||||
const httpsToken = env.CORE_REPOSITORY_TOKEN?.trim() ?? '';
|
||||
const sshKey = env.CORE_SSH_PRIVATE_KEY_BASE64?.trim() ?? '';
|
||||
const sshHosts = env.CORE_SSH_KNOWN_HOSTS_BASE64?.trim() ?? '';
|
||||
if (Boolean(httpsUser) !== Boolean(httpsToken)) {
|
||||
errors.push('CORE_REPOSITORY_USERNAME and CORE_REPOSITORY_TOKEN must be set together');
|
||||
}
|
||||
if (Boolean(sshKey) !== Boolean(sshHosts)) {
|
||||
errors.push('CORE_SSH_PRIVATE_KEY_BASE64 and CORE_SSH_KNOWN_HOSTS_BASE64 must be set together');
|
||||
}
|
||||
if ((httpsUser || httpsToken) && (sshKey || sshHosts)) errors.push('configure only one Core repository authentication mode');
|
||||
if ((httpsUser || httpsToken) && !/^https?:\/\//i.test(repositoryUrl)) {
|
||||
errors.push('HTTPS repository credentials require an HTTP(S) CORE_REPOSITORY_URL');
|
||||
}
|
||||
if ((sshKey || sshHosts) && !/^(?:ssh:\/\/|git@)/i.test(repositoryUrl)) {
|
||||
errors.push('SSH repository credentials require an SSH CORE_REPOSITORY_URL');
|
||||
}
|
||||
if (sshKey) {
|
||||
const decoded = decodeBase64(sshKey);
|
||||
if (!decoded?.includes('BEGIN OPENSSH PRIVATE KEY')) {
|
||||
errors.push('CORE_SSH_PRIVATE_KEY_BASE64 must encode an OpenSSH private key');
|
||||
}
|
||||
}
|
||||
if (sshHosts && !decodeBase64(sshHosts)?.trim()) {
|
||||
errors.push('CORE_SSH_KNOWN_HOSTS_BASE64 must contain valid base64-encoded known_hosts data');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||
if (isMain) {
|
||||
const errors = validateEnvironment(process.env);
|
||||
if (errors.length) {
|
||||
console.error('Environment validation failed:');
|
||||
for (const error of errors) console.error(`- ${error}`);
|
||||
process.exitCode = 64;
|
||||
} else {
|
||||
console.log('Environment validation passed without printing secret values.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user