import { execSync, spawnSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { CA_CERT, CERTS_DIR, HOST, SERVER_CERT, SERVER_KEY } from './config.mjs'; const SQL_CA_DIR = '/var/opt/mssql/security/ca-certificates'; export function certHosts(host) { const hosts = new Set([host, '127.0.0.1', 'localhost', 's3backup.local']); for (const iface of Object.values(os.networkInterfaces())) { for (const addr of iface || []) { if (addr.family === 'IPv4' && !addr.internal && !addr.address.startsWith('169.254.')) { hosts.add(addr.address); } } } return [...hosts].sort(); } export function ensureCerts(host = HOST) { fs.mkdirSync(CERTS_DIR, { recursive: true }); const hosts = certHosts(host); const marker = path.join(CERTS_DIR, 'hosts.txt'); const hostList = hosts.join('\n'); if ( fs.existsSync(SERVER_CERT) && fs.existsSync(SERVER_KEY) && fs.existsSync(CA_CERT) && fs.existsSync(marker) && fs.readFileSync(marker, 'utf8') === hostList ) { return; } const cnf = `${CERTS_DIR}/openssl.cnf`; const altNames = hosts .map((h, i) => (/^\d+\./.test(h) ? `IP.${i + 1} = ${h}` : `DNS.${i + 1} = ${h}`)) .join('\n'); fs.writeFileSync( cnf, `[req] distinguished_name = req_distinguished_name x509_extensions = v3_req prompt = no [req_distinguished_name] CN = ${host} [v3_req] subjectAltName = @alt_names basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth [alt_names] ${altNames} ` ); execSync( `openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ -keyout "${CERTS_DIR}/ca-key.pem" -out "${CA_CERT}" \ -subj "/CN=JTL S3 Backup CA/O=JTL/C=DE"`, { stdio: 'pipe' } ); execSync( `openssl req -newkey rsa:2048 -nodes \ -keyout "${SERVER_KEY}" -out "${CERTS_DIR}/server.csr" \ -config "${cnf}"`, { stdio: 'pipe' } ); execSync( `openssl x509 -req -in "${CERTS_DIR}/server.csr" \ -CA "${CA_CERT}" -CAkey "${CERTS_DIR}/ca-key.pem" -CAcreateserial \ -out "${SERVER_CERT}" -days 3650 -extensions v3_req -extfile "${cnf}"`, { stdio: 'pipe' } ); fs.writeFileSync(marker, hostList); } export function caTrustStatus(container = process.env.MSSQL_DOCKER_CONTAINER || 'mssql') { if (!fs.existsSync(CA_CERT)) { return { ok: false, inSync: false, restarted: false }; } const localFp = execSync(`openssl x509 -in "${CA_CERT}" -noout -fingerprint -sha256`, { encoding: 'utf8', }).trim(); const remoteFp = spawnSync( 'docker', [ 'exec', container, 'bash', '-lc', `test -f ${SQL_CA_DIR}/jtlsrv-s3-ca.pem && openssl x509 -in ${SQL_CA_DIR}/jtlsrv-s3-ca.pem -noout -fingerprint -sha256`, ], { encoding: 'utf8' } ); const inSync = remoteFp.status === 0 && remoteFp.stdout.trim() === localFp; return { ok: remoteFp.status === 0, inSync, restarted: false }; } export function installCaTrust(container = process.env.MSSQL_DOCKER_CONTAINER || 'mssql') { if (!fs.existsSync(CA_CERT)) { ensureCerts(); } const localFp = execSync(`openssl x509 -in "${CA_CERT}" -noout -fingerprint -sha256`, { encoding: 'utf8', }).trim(); const remoteFp = spawnSync( 'docker', [ 'exec', container, 'bash', '-lc', `test -f ${SQL_CA_DIR}/jtlsrv-s3-ca.pem && openssl x509 -in ${SQL_CA_DIR}/jtlsrv-s3-ca.pem -noout -fingerprint -sha256`, ], { encoding: 'utf8' } ); if (remoteFp.status === 0 && remoteFp.stdout.trim() === localFp) { spawnSync('docker', [ 'exec', '-u', 'root', container, 'bash', '-lc', 'grep -q s3backup.local /etc/hosts || echo "172.17.0.1 s3backup.local" >> /etc/hosts', ]); return { ok: true, restarted: false }; } spawnSync('docker', ['exec', '-u', 'root', container, 'mkdir', '-p', SQL_CA_DIR], { stdio: 'pipe', }); spawnSync('docker', [ 'exec', '-u', 'root', container, 'bash', '-lc', `rm -f ${SQL_CA_DIR}/*.pem ${SQL_CA_DIR}/*.crt`, ]); spawnSync('docker', [ 'exec', '-u', 'root', container, 'bash', '-lc', 'grep -q s3backup.local /etc/hosts || echo "172.17.0.1 s3backup.local" >> /etc/hosts', ]); try { execSync('grep -q s3backup.local /etc/hosts || echo "172.17.0.1 s3backup.local" >> /etc/hosts', { stdio: 'pipe', }); } catch { // optional on host } const copied = spawnSync( 'docker', ['cp', CA_CERT, `${container}:${SQL_CA_DIR}/jtlsrv-s3-ca.pem`], { stdio: 'pipe' } ); if (copied.status !== 0) { return { ok: false, restarted: false }; } const perms = spawnSync( 'docker', [ 'exec', '-u', 'root', container, 'bash', '-lc', `chown mssql:mssql ${SQL_CA_DIR}/jtlsrv-s3-ca.pem && chmod 644 ${SQL_CA_DIR}/jtlsrv-s3-ca.pem`, ], { stdio: 'pipe' } ); if (perms.status !== 0) { return { ok: false, restarted: false }; } const restarted = spawnSync('docker', ['restart', container], { stdio: 'pipe' }); return { ok: true, restarted: restarted.status === 0 }; }