import { randomBytes } from 'node:crypto'; import { execFileSync } from 'node:child_process'; import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, chownSync, openSync, fsyncSync, closeSync, renameSync } from 'node:fs'; // Local SANDBOX provisioning only. No secrets pass through logs, command arguments or source control. const roots = ['/secrets/admin', '/secrets/owner', '/secrets/runtime', '/secrets/backup', '/secrets/tls']; for (const root of roots) mkdirSync(root, { recursive: true, mode: 0o755 }); const files = ['/secrets/admin/password', '/secrets/admin/url', '/secrets/owner/url', '/secrets/runtime/url', '/secrets/tls/server.key', '/secrets/tls/server.crt', '/secrets/owner/ca.crt', '/secrets/runtime/ca.crt', '/secrets/admin/ca.crt']; const backupFiles = ['/secrets/backup/url', '/secrets/backup/ca.crt']; const marker = '/secrets/admin/complete'; const put = (file, text, mode = 0o600) => { writeFileSync(file, text, { flag: 'wx', mode }); chownSync(file, 1000, 1000); const fd = openSync(file, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } }; const provisionBackupRole = () => { if (backupFiles.some(existsSync)) throw new Error('Partial backup-role secret initialization; preserve volumes for inspection'); const backup = randomBytes(32).toString('hex'); put('/secrets/backup/url', `postgresql://custody_backup:${backup}@database:5432/wikideck_custody`); put('/secrets/backup/ca.crt', readFileSync('/secrets/admin/ca.crt'), 0o644); }; if (existsSync(marker)) { const version = readFileSync(marker, 'utf8'); if (!['wikideck-local-pg-v1', 'wikideck-local-pg-v2'].includes(version) || files.some(f => !existsSync(f))) { throw new Error('Incomplete secret volumes; restore the matching volumes, never regenerate over an existing database'); } execFileSync('openssl', ['x509', '-checkend', '86400', '-noout', '-in', '/secrets/tls/server.crt'], { stdio: 'ignore' }); if (version === 'wikideck-local-pg-v1') { provisionBackupRole(); const tempMarker = `${marker}.upgrade`; writeFileSync(tempMarker, 'wikideck-local-pg-v2', { flag: 'wx', mode: 0o644 }); renameSync(tempMarker, marker); } else if (backupFiles.some(f => !existsSync(f))) { throw new Error('Backup-role secret volumes are incomplete; preserve existing database and inspect volumes'); } console.log('Existing local sandbox credentials/certificate retained; read-only backup role available.'); } else { if (files.some(existsSync) || backupFiles.some(existsSync)) throw new Error('Partial secret initialization; inspect preserved volumes before retrying'); const admin = randomBytes(32).toString('hex'), owner = randomBytes(32).toString('hex'), runtime = randomBytes(32).toString('hex'), backup = randomBytes(32).toString('hex'); // PostgreSQL's unprivileged entrypoint also reads POSTGRES_PASSWORD_FILE. This separate scoped // admin volume is never mounted into the application; the password file must be readable there. put('/secrets/admin/password', admin, 0o644); put('/secrets/admin/url', `postgresql://postgres:${admin}@database:5432/wikideck_custody`); put('/secrets/owner/url', `postgresql://custody_owner:${owner}@database:5432/wikideck_custody`); put('/secrets/runtime/url', `postgresql://custody_runtime:${runtime}@database:5432/wikideck_custody`); put('/secrets/backup/url', `postgresql://custody_backup:${backup}@database:5432/wikideck_custody`); execFileSync('openssl', ['req', '-x509', '-newkey', 'rsa:3072', '-sha256', '-nodes', '-days', '30', '-subj', '/CN=Wikideck local sandbox CA', '-keyout', '/tmp/ca.key', '-out', '/tmp/ca.crt', '-addext', 'basicConstraints=critical,CA:TRUE', '-addext', 'keyUsage=critical,keyCertSign,cRLSign'], { stdio: 'ignore' }); execFileSync('openssl', ['req', '-new', '-newkey', 'rsa:3072', '-nodes', '-subj', '/CN=database', '-keyout', '/secrets/tls/server.key', '-out', '/tmp/server.csr'], { stdio: 'ignore' }); writeFileSync('/tmp/extensions', 'subjectAltName=DNS:database\nbasicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n'); execFileSync('openssl', ['x509', '-req', '-in', '/tmp/server.csr', '-CA', '/tmp/ca.crt', '-CAkey', '/tmp/ca.key', '-CAcreateserial', '-days', '30', '-sha256', '-extfile', '/tmp/extensions', '-out', '/secrets/tls/server.crt'], { stdio: 'ignore' }); for (const root of ['admin', 'owner', 'runtime', 'backup']) put(`/secrets/${root}/ca.crt`, readFileSync('/tmp/ca.crt'), 0o644); // postgres:17-alpine runs as UID/GID 70. Private key never mounted into Wikideck or migrator. chownSync('/secrets/tls/server.key', 70, 70); chmodSync('/secrets/tls/server.key', 0o600); chmodSync('/secrets/tls/server.crt', 0o644); put(marker, 'wikideck-local-pg-v2', 0o644); console.log('Provisioned fresh local sandbox credentials and verified-TLS certificate; no secrets logged.'); }