62 lines
4.9 KiB
JavaScript
Executable File
62 lines
4.9 KiB
JavaScript
Executable File
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.');
|
|
}
|