This commit is contained in:
2026-10-20 17:09:22 -04:00
parent 447314440b
commit 621611ecd9
4 changed files with 73 additions and 2 deletions
+14
View File
@@ -34,10 +34,24 @@ The following are configuration values, not credentials:
| `WIKIDECK_CUSTODY_STORAGE=postgres` | Selects PostgreSQL; default `sqlite` is retained for existing sandbox users. |
| `WIKIDECK_CUSTODY_PG_URL_FILE` | Absolute path to a private secret file containing a PostgreSQL URL. Never a Vite/browser variable. |
| `WIKIDECK_CUSTODY_PG_CA_FILE` | Optional private file with trusted PEM CA certificate(s); otherwise use system trust. TLS hostname/certificate verification remains enabled. |
| `WIKIDECK_CUSTODY_PG_TRANSPORT` | Default `tls`: verified PostgreSQL TLS. Optional `infrastructure`: the driver uses plaintext and relies on an independently enforced encrypted/authenticated infrastructure path; see conditions below. |
| `WIKIDECK_CUSTODY_PG_INFRASTRUCTURE_ACK` | Required **only** in infrastructure mode, exactly `encrypted-authenticated-path-enforced`. An operator acknowledgement, NOT automatic verification of infrastructure security. |
| `WIKIDECK_CUSTODY_PG_LOCAL_TEST=1` | **Disposable loopback tests only:** disables TLS solely for literal loopback/localhost. Never use for production. |
URL query parameters/fragments are rejected, preventing `sslmode` options from overriding transport policy. Missing/malformed configuration fails with a generic error, not a URL/password dump. Secret files must be regular non-symlink bounded files; mount them read-only with restricted filesystem ACLs. Production network firewall, verified TLS, password/secret rotation and external database logs need an operator review. The automated fixture tests configuration policy, not an actual production certificate renewal/handshake drill.
### Kubernetes/infrastructure-managed transport
Prefer the default verified PostgreSQL TLS, with certificate issuance/renewal provided by the database operator, managed service or cluster infrastructure. **Wikideck does not need a bespoke certificate lifecycle service.** The local Docker certificate generator is a sandbox convenience, not the Kubernetes production architecture.
If an enforced service-mesh mTLS path or equivalent infrastructure authenticates the intended database peer and encrypts the complete inter-workload path, deployments may explicitly select `infrastructure` plus the exact acknowledgement above. This sets the PostgreSQL driver's `ssl` to false: application-to-proxy and proxy-to-database plaintext segments must be local/isolated within the reviewed boundary. A firewall, ordinary Kubernetes networking or NetworkPolicy alone does not satisfy that condition. Ensure sidecar interception cannot be bypassed, plaintext inter-pod access is denied, destination identities are restricted, and policies remain enforced after restarts/failover. Protect migration, audit and recovery clients too—not just the HTTP application.
Infrastructure mode cannot be combined with a CA file or the loopback-test switch; a stray acknowledgement in TLS mode is rejected. URL TLS overrides remain forbidden and file-based password credentials remain required. No insecure “accept any certificate” TLS option is added. Wikideck cannot verify external mesh enforcement; the acknowledgement is not proof and does not authorize live funds. Cluster evidence/review remains a release gate.
Validation for this transport increment: TypeScript/Vite build, **171 offline tests and 16 PostgreSQL integration tests passed**, zero failures/skips. Configuration tests cover secure defaults, exact opt-in, missing acknowledgement/password, invalid mode, conflicting CA/test settings and URL override rejection. No Kubernetes cluster or actual service-mesh enforcement was tested or certified.
Existing Compose stacks remain configured for verified native PostgreSQL TLS. Their TLS-only database HBA and deployment smoke probe intentionally reject infrastructure/plaintext mode: do not change just the client flag on that stack. This option is for a separately configured infrastructure deployment, not automatic TLS fallback. No Kubernetes resources or running database/container settings were changed. Custom certificate renewal implementation is deferred to infrastructure; the local sandbox's documented short-lived certificates still need controlled operational replacement if retained.
The operator CLI is [scripts/custody-db.ts](../scripts/custody-db.ts), available through `npm run custody:db -- ACTION`. Credentials are provided only by the secret-file environment setting, never positional arguments or browser UI:
1. Provision a dedicated database, a migration/schema-owner identity, and a separate existing login role without superuser/create-role/create-db/replication/bypass-RLS privileges. Create passwords privately outside this tool; no credentials are generated or displayed by the CLI.
+1
View File
@@ -126,6 +126,7 @@ Run throughout development; final release gates depend on P2–P5.
- Complete wallet review: same-origin threat model, migration/lock/unlock and recovery across supported browsers, signing review/session invalidation and stale handles; decide isolated/hardware signing support. Close old plaintext copies as a user migration/risk procedure, not a secure-erasure claim.
- Real host/edge verification: HTTPS/cert renewal, forwarded-header trust, firewall, admin access, secrets, egress, CPU/RAM/disk/WAL budgets, time sync, on-call alerts and monitoring of actual peer/tip and payment-worker progress.
- Deployment clarification: production target is behind a firewall in Kubernetes. Database certificate issuance/renewal belongs to the database/cluster infrastructure, not a bespoke application service. Verified PostgreSQL TLS remains the default; explicit [infrastructure-managed transport](postgres-financial-storage.md) is available only with acknowledged externally enforced encryption/peer authentication. A firewall/NetworkPolicy alone is insufficient evidence. Local Docker TLS provisioning remains sandbox-specific; no certificate-rotation feature is being activated.
- Backups/restore with complete financial journals and off-host encrypted copies: measured RPO/RTO and successful cold restore/reconciliation; test losses of host, journal, wallet key and processor reachability. Recovery of customer keys and recovery of merchant funds are different responsibilities.
- Repeat consensus/hashpower studies on intended miner/validator hardware. Quantify production attack and recovery costs; finalize difficulty/retarget/actual genesis after review. One powerful operator server is not multiple independent validators/miners or Monero-level security.
- Pin release artifacts/dependencies/base images, SBOM, secret/dependency/container/native scans, fuzz/sanitizer runs, reproducible/provenance evidence and signed artifacts. Publish the selected project license and respect dependency/Wikipedia licensing.
+13 -2
View File
@@ -19,6 +19,15 @@ export async function postgresConfig(env: NodeJS.ProcessEnv = process.env): Prom
if (!['postgres:', 'postgresql:'].includes(url.protocol) || !url.hostname || url.search || url.hash ||
!/^\/[a-zA-Z0-9_-]+$/.test(url.pathname)) throw new Error('Invalid PostgreSQL URL');
const insecure = env.WIKIDECK_CUSTODY_PG_LOCAL_TEST === '1';
const transport = env.WIKIDECK_CUSTODY_PG_TRANSPORT ?? 'tls';
if (transport !== 'tls' && transport !== 'infrastructure') throw new Error('Invalid PostgreSQL transport');
const infrastructure = transport === 'infrastructure';
const acknowledgement = env.WIKIDECK_CUSTODY_PG_INFRASTRUCTURE_ACK;
if (infrastructure) {
if (acknowledgement !== 'encrypted-authenticated-path-enforced' || insecure || env.WIKIDECK_CUSTODY_PG_CA_FILE) {
throw new Error('Infrastructure transport requires explicit acknowledgement without conflicting TLS/test settings');
}
} else if (acknowledgement !== undefined) throw new Error('Infrastructure acknowledgement without infrastructure transport');
if (env.WIKIDECK_CUSTODY_PG_LOCAL_TEST !== undefined && !insecure) throw new Error('Invalid local-test switch');
if (insecure && !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) throw new Error('Non-TLS test connections must be loopback');
if (!insecure && (!url.username || !url.password)) throw new Error('PostgreSQL credentials required');
@@ -26,9 +35,11 @@ export async function postgresConfig(env: NodeJS.ProcessEnv = process.env): Prom
const ca = env.WIKIDECK_CUSTODY_PG_CA_FILE ? await secretFile(env.WIKIDECK_CUSTODY_PG_CA_FILE) : undefined;
return { host: url.hostname.replace(/^\[|\]$/g, ''), port: url.port ? Number(url.port) : 5432,
database: url.pathname.slice(1), user: decodeURIComponent(url.username), password: decodeURIComponent(url.password),
ssl: insecure ? false : { rejectUnauthorized: true, ...(ca ? { ca } : {}) },
// Infrastructure mode is explicit plaintext on the driver's hop. It does NOT configure or
// verify a service mesh, peer identity, network policy, or encryption outside this process.
ssl: insecure || infrastructure ? false : { rejectUnauthorized: true, ...(ca ? { ca } : {}) },
max: 4, connectionTimeoutMillis: 5000, idleTimeoutMillis: 10000,
statement_timeout: 10000, lock_timeout: 3000, idle_in_transaction_session_timeout: 10000,
application_name: 'wikideck-custody', options: '-c search_path=wk_custody,pg_catalog' };
} catch { throw new Error('Invalid or inaccessible custody PostgreSQL configuration; check secret files and verified TLS settings'); }
} catch { throw new Error('Invalid or inaccessible custody PostgreSQL configuration; check secret files and explicit transport policy'); }
}
+45
View File
@@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { postgresConfig } from '../server/market/postgres-config.ts';
test('PostgreSQL verified TLS remains the default and cannot be downgraded through URL options', async t => {
const dir = await mkdtemp(join(tmpdir(), 'wikideck-transport-'));
t.after(() => rm(dir, { recursive: true, force: true }));
const file = join(dir, 'connection');
// Synthetic configuration fixture, never sent to a database.
await writeFile(file, 'postgresql://fixture:fixture-only@database.cluster.example/custody', { mode: 0o600 });
const env = { WIKIDECK_CUSTODY_PG_URL_FILE: file };
const config = await postgresConfig(env);
assert.equal((config.ssl as { rejectUnauthorized: boolean }).rejectUnauthorized, true);
assert.deepEqual(config.ssl, (await postgresConfig({ ...env, WIKIDECK_CUSTODY_PG_TRANSPORT: 'tls' })).ssl);
for (const mode of ['', 'off', 'disable', 'false', 'mesh', 'TLS']) {
await assert.rejects(postgresConfig({ ...env, WIKIDECK_CUSTODY_PG_TRANSPORT: mode }), /configuration/);
}
await writeFile(file, 'postgresql://fixture:fixture-only@database.cluster.example/custody?sslmode=disable');
await assert.rejects(postgresConfig(env), /configuration/);
});
test('infrastructure-managed transport requires exact opt-in and rejects mixed TLS/test configuration', async t => {
const dir = await mkdtemp(join(tmpdir(), 'wikideck-infrastructure-'));
t.after(() => rm(dir, { recursive: true, force: true }));
const file = join(dir, 'connection');
await writeFile(file, 'postgresql://fixture:fixture-only@database.cluster.example/custody', { mode: 0o600 });
const env = { WIKIDECK_CUSTODY_PG_URL_FILE: file, WIKIDECK_CUSTODY_PG_TRANSPORT: 'infrastructure' };
await assert.rejects(postgresConfig(env), /configuration/);
for (const ack of ['', 'true', 'firewall', 'encrypted-authenticated-path-enforced ']) {
await assert.rejects(postgresConfig({ ...env, WIKIDECK_CUSTODY_PG_INFRASTRUCTURE_ACK: ack }), /configuration/);
}
const approved = { ...env, WIKIDECK_CUSTODY_PG_INFRASTRUCTURE_ACK: 'encrypted-authenticated-path-enforced' };
assert.equal((await postgresConfig(approved)).ssl, false);
for (const conflict of [{ WIKIDECK_CUSTODY_PG_CA_FILE: file }, { WIKIDECK_CUSTODY_PG_LOCAL_TEST: '1' }]) {
await assert.rejects(postgresConfig({ ...approved, ...conflict }), /configuration/);
}
await assert.rejects(postgresConfig({ ...approved, WIKIDECK_CUSTODY_PG_TRANSPORT: 'tls' }), /configuration/);
await writeFile(file, 'postgresql://fixture@database.cluster.example/custody');
await assert.rejects(postgresConfig(approved), /configuration/); // Password auth remains mandatory.
await writeFile(file, 'postgresql://fixture:fixture-only@database.cluster.example/custody?sslmode=require');
await assert.rejects(postgresConfig(approved), /configuration/);
});