| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- import assert from 'node:assert/strict';
- import {
- chmodSync,
- mkdirSync,
- mkdtempSync,
- readFileSync,
- readdirSync,
- rmSync,
- statSync,
- writeFileSync
- } from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { spawnSync } from 'node:child_process';
- import { test } from 'node:test';
- const scriptPath = fileURLToPath(new URL('../scripts/sync-tls-certificate.sh', import.meta.url));
- const opensslLookup = process.platform === 'win32'
- ? { status: 1, stdout: '' }
- : spawnSync('sh', ['-c', 'command -v openssl'], { encoding: 'utf8' });
- const opensslPath = opensslLookup.status === 0 ? opensslLookup.stdout.trim() : '';
- const canRun = process.platform !== 'win32' && Boolean(opensslPath);
- test('atomically synchronizes a valid wildcard certificate and preserves safe permissions', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- generateCertificate(fixture.sourceDir, 'example.test');
- const first = runSync(fixture);
- assert.equal(first.status, 0, first.stderr);
- assert.match(first.stdout, /MailHub TLS certificate synchronized for mail\.example\.test\./);
- assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
- assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
- assert.equal(fileMode(fixture.targetCert), 0o644);
- assert.equal(fileMode(fixture.targetKey), 0o640);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- const previousCert = readFileSync(fixture.targetCert);
- const previousKey = readFileSync(fixture.targetKey);
- chmodSync(fixture.targetCert, 0o640);
- chmodSync(fixture.targetKey, 0o600);
- generateCertificate(fixture.sourceDir, 'example.test');
- const replacement = runSync(fixture);
- assert.equal(replacement.status, 0, replacement.stderr);
- assert.deepEqual(readFileSync(`${fixture.targetCert}.previous`), previousCert);
- assert.deepEqual(readFileSync(`${fixture.targetKey}.previous`), previousKey);
- assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
- assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
- assert.equal(fileMode(fixture.targetCert), 0o644);
- assert.equal(fileMode(fixture.targetKey), 0o640);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- test('is idempotent when the synchronized certificate is already current', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- generateCertificate(fixture.sourceDir, 'example.test');
- const first = runSync(fixture);
- assert.equal(first.status, 0, first.stderr);
- const certBefore = statSnapshot(fixture.targetCert);
- const keyBefore = statSnapshot(fixture.targetKey);
- const second = runSync(fixture);
- assert.equal(second.status, 0, second.stderr);
- assert.match(second.stdout, /MailHub TLS certificate is already up to date\./);
- assert.deepEqual(statSnapshot(fixture.targetCert), certBefore);
- assert.deepEqual(statSnapshot(fixture.targetKey), keyBefore);
- assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- test('rejects a certificate that does not cover MAIL_HOSTNAME without touching the targets', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- generateCertificate(fixture.sourceDir, 'other.test');
- mkdirSync(fixture.certsDir, { recursive: true });
- writeFileSync(fixture.targetCert, 'existing certificate\n');
- writeFileSync(fixture.targetKey, 'existing private key\n');
- chmodSync(fixture.targetCert, 0o640);
- chmodSync(fixture.targetKey, 0o600);
- const certBefore = readFileSync(fixture.targetCert);
- const keyBefore = readFileSync(fixture.targetKey);
- const result = runSync(fixture);
- assert.notEqual(result.status, 0);
- assert.match(result.stderr, /source certificate does not cover mail\.example\.test\./);
- assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
- assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
- assert.equal(fileMode(fixture.targetCert), 0o640);
- assert.equal(fileMode(fixture.targetKey), 0o600);
- assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- test('rejects certificate and private key paths that resolve to the same target', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- generateCertificate(fixture.sourceDir, 'example.test');
- mkdirSync(fixture.certsDir, { recursive: true });
- writeEnvironment(fixture, {
- SUBMISSION_TLS_KEY: '/certs/fullchain.pem'
- });
- const result = runSync(fixture);
- assert.notEqual(result.status, 0);
- assert.equal(readdirSync(fixture.certsDir).some((name) => name === 'fullchain.pem' || name === 'privkey.pem'), false);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- test('rejects a mismatched certificate and private key without touching the targets', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- const unrelatedDir = path.join(fixture.root, 'unrelated');
- mkdirSync(unrelatedDir, { recursive: true });
- generateCertificate(fixture.sourceDir, 'example.test');
- generateCertificate(unrelatedDir, 'example.test');
- writeFileSync(fixture.sourceKey, readFileSync(path.join(unrelatedDir, 'privkey.pem')));
- mkdirSync(fixture.certsDir, { recursive: true });
- writeFileSync(fixture.targetCert, 'existing certificate\n');
- writeFileSync(fixture.targetKey, 'existing private key\n');
- const certBefore = readFileSync(fixture.targetCert);
- const keyBefore = readFileSync(fixture.targetKey);
- const result = runSync(fixture);
- assert.notEqual(result.status, 0);
- assert.match(result.stderr, /certificate and private key do not match/i);
- assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
- assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
- assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- test('tightens an overly permissive existing private key to mode 0640', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- generateCertificate(fixture.sourceDir, 'example.test');
- const first = runSync(fixture);
- assert.equal(first.status, 0, first.stderr);
- chmodSync(fixture.targetKey, 0o666);
- generateCertificate(fixture.sourceDir, 'example.test');
- const replacement = runSync(fixture);
- assert.equal(replacement.status, 0, replacement.stderr);
- assert.equal(fileMode(fixture.targetKey), 0o640);
- assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
- });
- test('validates and installs a staged source snapshot when managed files change mid-run', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- const replacementDir = path.join(fixture.root, 'replacement');
- mkdirSync(replacementDir, { recursive: true });
- generateCertificate(fixture.sourceDir, 'example.test');
- generateCertificate(replacementDir, 'example.test');
- const expectedCert = readFileSync(fixture.sourceCert);
- const expectedKey = readFileSync(fixture.sourceKey);
- const marker = path.join(fixture.root, 'source-mutated');
- const mutationPath = createOpenSslMutationPath(fixture.root, fixture.path);
- const result = runSync(fixture, {
- env: {
- PATH: mutationPath,
- MAILHUB_TEST_REAL_OPENSSL: opensslPath,
- MAILHUB_TEST_MUTATION_MARKER: marker,
- MAILHUB_TEST_SOURCE_CERT: fixture.sourceCert,
- MAILHUB_TEST_SOURCE_KEY: fixture.sourceKey,
- MAILHUB_TEST_REPLACEMENT_CERT: path.join(replacementDir, 'fullchain.pem'),
- MAILHUB_TEST_REPLACEMENT_KEY: path.join(replacementDir, 'privkey.pem')
- }
- });
- assert.equal(result.status, 0, result.stderr);
- assert.equal(readFileSync(fixture.targetCert).equals(expectedCert), true, 'installed certificate must use the staged snapshot');
- assert.equal(readFileSync(fixture.targetKey).equals(expectedKey), true, 'installed private key must use the staged snapshot');
- assert.notDeepEqual(readFileSync(fixture.sourceCert), expectedCert);
- assert.notDeepEqual(readFileSync(fixture.sourceKey), expectedKey);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- test('rolls back both certificate files when post-restart health validation fails', { skip: !canRun }, (t) => {
- const fixture = createFixture(t);
- generateCertificate(fixture.sourceDir, 'example.test');
- const first = runSync(fixture);
- assert.equal(first.status, 0, first.stderr);
- const certBefore = readFileSync(fixture.targetCert);
- const keyBefore = readFileSync(fixture.targetKey);
- const certModeBefore = fileMode(fixture.targetCert);
- const keyModeBefore = fileMode(fixture.targetKey);
- generateCertificate(fixture.sourceDir, 'example.test');
- const failurePath = createFailedHealthPath(fixture.root, fixture.path);
- const result = runSync(fixture, {
- restart: '1',
- env: { PATH: failurePath }
- });
- assert.notEqual(result.status, 0);
- assert.match(result.stderr, /did not become healthy/i);
- assert.equal(readFileSync(fixture.targetCert).equals(certBefore), true, 'certificate must roll back after failed health validation');
- assert.equal(readFileSync(fixture.targetKey).equals(keyBefore), true, 'private key must roll back after failed health validation');
- assert.equal(fileMode(fixture.targetCert), certModeBefore);
- assert.equal(fileMode(fixture.targetKey), keyModeBefore);
- assert.deepEqual(stagingFiles(fixture.certsDir), []);
- });
- function createFixture(t) {
- const root = mkdtempSync(path.join(os.tmpdir(), 'mailhub-cert-sync-'));
- const projectDir = path.join(root, 'project');
- const sourceDir = path.join(root, 'source');
- const certsDir = path.join(projectDir, 'certs');
- const envFile = path.join(projectDir, '.env');
- mkdirSync(projectDir, { recursive: true });
- mkdirSync(sourceDir, { recursive: true });
- writeFileSync(envFile, [
- 'MAIL_HOSTNAME=mail.example.test',
- 'SUBMISSION_TLS_CERT=/certs/fullchain.pem',
- 'SUBMISSION_TLS_KEY=/certs/privkey.pem',
- ''
- ].join('\n'));
- t.after(() => rmSync(root, { recursive: true, force: true }));
- return {
- root,
- projectDir,
- sourceDir,
- certsDir,
- envFile,
- sourceCert: path.join(sourceDir, 'fullchain.pem'),
- sourceKey: path.join(sourceDir, 'privkey.pem'),
- targetCert: path.join(certsDir, 'fullchain.pem'),
- targetKey: path.join(certsDir, 'privkey.pem'),
- path: createCompatibilityPath(root)
- };
- }
- function writeEnvironment(fixture, overrides = {}) {
- const values = {
- MAIL_HOSTNAME: 'mail.example.test',
- SUBMISSION_TLS_CERT: '/certs/fullchain.pem',
- SUBMISSION_TLS_KEY: '/certs/privkey.pem',
- ...overrides
- };
- writeFileSync(fixture.envFile, [
- `MAIL_HOSTNAME=${values.MAIL_HOSTNAME}`,
- `SUBMISSION_TLS_CERT=${values.SUBMISSION_TLS_CERT}`,
- `SUBMISSION_TLS_KEY=${values.SUBMISSION_TLS_KEY}`,
- ''
- ].join('\n'));
- }
- function generateCertificate(sourceDir, domain) {
- const result = spawnSync('openssl', [
- 'req',
- '-x509',
- '-newkey', 'rsa:2048',
- '-nodes',
- '-sha256',
- '-days', '30',
- '-subj', `/CN=*.${domain}`,
- '-addext', `subjectAltName=DNS:*.${domain},DNS:${domain}`,
- '-keyout', path.join(sourceDir, 'privkey.pem'),
- '-out', path.join(sourceDir, 'fullchain.pem')
- ], { encoding: 'utf8' });
- assert.equal(result.status, 0, result.stderr);
- }
- function runSync(fixture, { restart = '0', env = {} } = {}) {
- const rootOwnershipEnv = typeof process.getuid === 'function' && process.getuid() === 0
- ? { MAILHUB_CERT_READER_GID: String(process.getgid()) }
- : {};
- return spawnSync('bash', [scriptPath], {
- cwd: fixture.projectDir,
- encoding: 'utf8',
- env: {
- ...process.env,
- PATH: fixture.path,
- MAILHUB_CERT_PROJECT_DIR: fixture.projectDir,
- MAILHUB_CERT_ENV_FILE: fixture.envFile,
- MAILHUB_CERT_SOURCE_DIR: fixture.sourceDir,
- MAILHUB_CERT_RESTART: restart,
- ...rootOwnershipEnv,
- ...env
- }
- });
- }
- function statSnapshot(file) {
- const stat = statSync(file);
- return {
- ino: stat.ino,
- mode: stat.mode & 0o777,
- size: stat.size,
- mtimeMs: stat.mtimeMs
- };
- }
- function fileMode(file) {
- return statSync(file).mode & 0o777;
- }
- function stagingFiles(directory) {
- return readdirSync(directory).filter((name) => name.includes('.tmp.') || name.startsWith('.cert-sync.'));
- }
- function createCompatibilityPath(root) {
- if (process.platform !== 'darwin') return process.env.PATH || '';
- const binDir = path.join(root, 'bin');
- mkdirSync(binDir, { recursive: true });
- writeExecutable(path.join(binDir, 'cp'), stripDoubleDashWrapper('/bin/cp'));
- writeExecutable(path.join(binDir, 'mv'), stripDoubleDashWrapper('/bin/mv'));
- writeExecutable(path.join(binDir, 'rm'), stripDoubleDashWrapper('/bin/rm'));
- writeExecutable(path.join(binDir, 'chmod'), `#!/usr/bin/env bash
- set -euo pipefail
- if [[ "\${1:-}" == --reference=* ]]; then
- reference="\${1#*=}"
- shift
- mode="$(/usr/bin/stat -f '%Lp' "\${reference}")"
- exec /bin/chmod "\${mode}" "$@"
- fi
- exec /bin/chmod "$@"
- `);
- writeExecutable(path.join(binDir, 'chown'), `#!/usr/bin/env bash
- set -euo pipefail
- if [[ "\${1:-}" == --reference=* ]]; then
- reference="\${1#*=}"
- shift
- owner="$(/usr/bin/stat -f '%u:%g' "\${reference}")"
- exec /usr/sbin/chown "\${owner}" "$@"
- fi
- exec /usr/sbin/chown "$@"
- `);
- writeExecutable(path.join(binDir, 'sha256sum'), `#!/usr/bin/env bash
- exec /usr/bin/shasum -a 256 "$@"
- `);
- return `${binDir}${path.delimiter}${process.env.PATH || ''}`;
- }
- function createOpenSslMutationPath(root, basePath) {
- const binDir = path.join(root, 'mutating-openssl-bin');
- mkdirSync(binDir, { recursive: true });
- writeExecutable(path.join(binDir, 'openssl'), `#!/usr/bin/env bash
- set -euo pipefail
- if [[ ! -e "\${MAILHUB_TEST_MUTATION_MARKER}" ]]; then
- : > "\${MAILHUB_TEST_MUTATION_MARKER}"
- cp "\${MAILHUB_TEST_REPLACEMENT_CERT}" "\${MAILHUB_TEST_SOURCE_CERT}"
- cp "\${MAILHUB_TEST_REPLACEMENT_KEY}" "\${MAILHUB_TEST_SOURCE_KEY}"
- fi
- exec "\${MAILHUB_TEST_REAL_OPENSSL}" "$@"
- `);
- return `${binDir}${path.delimiter}${basePath}`;
- }
- function createFailedHealthPath(root, basePath) {
- const binDir = path.join(root, 'failed-health-bin');
- mkdirSync(binDir, { recursive: true });
- writeExecutable(path.join(binDir, 'docker'), `#!/usr/bin/env bash
- set -euo pipefail
- if [[ "$*" == *"tls.createSecureContext"* ]]; then
- exit 0
- fi
- if [[ "\${1:-}" == "compose" && "\${2:-}" == "restart" ]]; then
- exit 0
- fi
- exit 1
- `);
- writeExecutable(path.join(binDir, 'sleep'), `#!/usr/bin/env bash
- exit 0
- `);
- return `${binDir}${path.delimiter}${basePath}`;
- }
- function stripDoubleDashWrapper(command) {
- return `#!/usr/bin/env bash
- set -euo pipefail
- args=()
- for argument in "$@"; do
- [[ "\${argument}" == "--" ]] || args+=("\${argument}")
- done
- exec ${command} "\${args[@]}"
- `;
- }
- function writeExecutable(file, content) {
- writeFileSync(file, content);
- chmodSync(file, 0o755);
- }
|