Files
wikideck/deploy/postgres/setup.mjs
T

44 lines
3.7 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 } 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/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 marker = '/secrets/admin/complete';
if (existsSync(marker)) {
if (readFileSync(marker, 'utf8') !== 'wikideck-local-pg-v1' || 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' });
console.log('Existing local sandbox credentials/certificate retained.');
} else {
if (files.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');
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); }
};
// 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`);
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']) 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-v1', 0o644);
console.log('Provisioned fresh local sandbox credentials and verified-TLS certificate; no secrets logged.');
}