Hardened systemd deployment contract for read-only Monero observer
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user