Hardened systemd deployment contract for read-only Monero observer

This commit is contained in:
2026-10-22 10:08:15 -04:00
parent 158981e965
commit e835238d05
12 changed files with 170 additions and 7 deletions
+8 -1
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { assertCustodyDbActionAllowedOnNetwork, parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
import { assertCustodyDbActionAllowedOnNetwork, assertMainnetSchemaMigrationAcknowledged,
parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
test('custody database CLI parses two-argument deposit operations without shifting arguments', () => {
assert.deepEqual(parseCustodyDbArguments(['deposit-prepare-credit', 'a'.repeat(64), `${'b'.repeat(64)}:${'c'.repeat(64)}`]), {
@@ -16,6 +17,12 @@ test('custody database CLI parses two-argument deposit operations without shifti
test('mainnet custody CLI permits only the observation-role grant', () => {
assert.doesNotThrow(() => assertCustodyDbActionAllowedOnNetwork('grant-monero-observer-runtime', true));
assert.doesNotThrow(() => assertCustodyDbActionAllowedOnNetwork('migrate-mainnet-schema', true));
assert.throws(() => assertCustodyDbActionAllowedOnNetwork('migrate-mainnet-schema', false), /requires the mainnet profile/);
assert.throws(() => assertMainnetSchemaMigrationAcknowledged('migrate-mainnet-schema', undefined), /acknowledgement/);
assert.throws(() => assertMainnetSchemaMigrationAcknowledged('migrate-mainnet-schema', 'yes'), /acknowledgement/);
assert.doesNotThrow(() => assertMainnetSchemaMigrationAcknowledged('migrate-mainnet-schema', 'explicit-schema-migration-only'));
assert.doesNotThrow(() => assertMainnetSchemaMigrationAcknowledged('grant-monero-observer-runtime', undefined));
for (const action of ['migrate', 'grant-runtime', 'grant-monero-credit-runtime', 'audit', 'deposit-release'] as const) {
assert.throws(() => assertCustodyDbActionAllowedOnNetwork(action, true), /only the separately scoped/i);
assert.doesNotThrow(() => assertCustodyDbActionAllowedOnNetwork(action, false));
+21
View File
@@ -28,3 +28,24 @@ test('database HBA requires TLS+SCRAM over TCP and provisioning refuses partial
assert.match(setup, /subjectAltName=DNS:database/);
assert.match(setup, /-checkend/);
});
test('standalone Monero observer service is non-root, filesystem-hardened and has no public listener', async () => {
const unit = await read('deploy/systemd/wikideck-monero-observer.service');
const guide = await read('deploy/systemd/README.md');
const observer = JSON.parse(await read('deploy/systemd/wallet-rpc.example.json')) as { network: string; url: string };
const mtls = JSON.parse(await read('deploy/systemd/mtls.example.json')) as { url: string; clientPrivateKeyFile: string };
assert.match(unit, /User=wikideck-observer/);
assert.match(unit, /ProtectSystem=strict/);
assert.match(unit, /ProtectHome=true/);
assert.match(unit, /NoNewPrivileges=true/);
assert.match(unit, /CapabilityBoundingSet=/);
assert.match(unit, /RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX/);
assert.match(unit, /WIKIDECK_MONERO_OBSERVER_WORKER=1/);
assert.doesNotMatch(unit, /^\s*(?:ListenStream|ListenDatagram|SocketUser)=/m);
assert.equal(observer.network, 'mainnet');
assert.equal(observer.url, mtls.url);
assert.equal(mtls.clientPrivateKeyFile, '/etc/wikideck/monero-observer/client.key');
assert.match(guide, /migrate-mainnet-schema/);
assert.match(guide, /does \*\*not\*\* issue customer deposit addresses/);
assert.doesNotMatch(`${unit}\n${guide}`, /(?:password|api[_-]?key)\s*[:=]\s*[^\s<]+/i);
});
+6 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { mkdtemp, writeFile, chmod, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { postgresConfig } from '../server/market/postgres-config.ts';
@@ -18,6 +18,11 @@ test('PostgreSQL verified TLS remains the default and cannot be downgraded throu
for (const mode of ['', 'off', 'disable', 'false', 'mesh', 'TLS']) {
await assert.rejects(postgresConfig({ ...env, WIKIDECK_CUSTODY_PG_TRANSPORT: mode }), /configuration/);
}
if (process.platform !== 'win32') {
await chmod(file, 0o644);
await assert.rejects(postgresConfig(env), /configuration/);
await chmod(file, 0o600);
}
await writeFile(file, 'postgresql://fixture:fixture-only@database.cluster.example/custody?sslmode=disable');
await assert.rejects(postgresConfig(env), /configuration/);
});
+1 -1
View File
@@ -772,7 +772,7 @@ test('configuration requires secret files, verified TLS, literal backend, and lo
await assert.rejects(openCustodyStorage(dir, options, { WIKIDECK_CUSTODY_STORAGE: 'postgres',
WIKIDECK_CUSTODY_CANONICAL_REHEARSAL: 'yes', WIKIDECK_PG_DISPOSABLE_TEST: '1' }), /exactly 0 or 1/);
await assert.rejects(postgresConfig({}), /configuration/);
await writeFile(file, 'postgresql://user:secret@database.example/custody');
await writeFile(file, 'postgresql://user:secret@database.example/custody', { mode: 0o600 });
const secure = await postgresConfig({ WIKIDECK_CUSTODY_PG_URL_FILE: file }); assert.equal((secure.ssl as { rejectUnauthorized: boolean }).rejectUnauthorized, true);
await assert.rejects(postgresConfig({ WIKIDECK_CUSTODY_PG_URL_FILE: file, WIKIDECK_CUSTODY_PG_LOCAL_TEST: '1' }), /configuration/);
await writeFile(file, 'postgresql://user:secret@127.0.0.1/custody?sslmode=disable');