fix(runtime): harden smoke and credential boundaries

This commit is contained in:
2026-08-04 16:05:06 +00:00
parent 09d849de07
commit c49989631f
16 changed files with 517 additions and 8 deletions
+112
View File
@@ -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.');
}
}