fix: honor runtime db ssl mode in postgres adapter

This commit is contained in:
Code Queue Review
2026-05-23 04:23:39 +00:00
parent 82b1f9491e
commit a729f19f78
6 changed files with 152 additions and 14 deletions
+6 -3
View File
@@ -48,9 +48,12 @@ non-secret env flags:
The Postgres adapter uses `HWLAB_CLOUD_DB_URL` from
`hwlab-cloud-api-dev-db/database-url`; source records only the Secret reference
and non-secret DNS contract. The adapter reports `runtime.durable: true` only
after the configured adapter proves all durable gates through read-only runtime
checks:
and non-secret DNS contract. `HWLAB_CLOUD_DB_SSL_MODE` is the runtime authority
for DB TLS mode; if it is `disable`, the adapter strips URL `sslmode` query
parameters before opening the Postgres pool so stale Secret URL parameters
cannot override the DEV SSL contract. The adapter reports `runtime.durable: true`
only after the configured adapter proves all durable gates through read-only
runtime checks:
- auth/query access to the configured DB path;
- required runtime table and column schema;
+5
View File
@@ -176,6 +176,11 @@ Postgres adapter may report `runtime.durableRequested=true`, but it still stays
non-durable until the schema, migration ledger, and read readiness gates are
proven through the configured adapter.
`HWLAB_CLOUD_DB_SSL_MODE` is the non-secret runtime authority for Postgres TLS
mode. When DEV sets it to `disable`, the runtime strips stale URL `sslmode`,
`ssl`, and certificate query parameters before constructing the `pg` pool; a
Secret URL query parameter must not silently override the manifest SSL contract.
`/health` and `/health/live` expose this split in `readiness.durability`.
`dbLiveEvidenceObserved=true` with
`dbLiveEvidenceIsDurabilityEvidence=false` means the DB TCP/live layer is
+51 -5
View File
@@ -68,6 +68,15 @@ export function createConfiguredCloudRuntimeStore(options = {}) {
return createCloudRuntimeStore(options);
}
export function buildPostgresPoolConfig({ dbUrl, sslMode = "require", timeoutMs } = {}) {
const normalizedSslMode = normalizePostgresSslMode(sslMode);
return {
connectionString: normalizePostgresConnectionStringForSslMode(dbUrl, normalizedSslMode),
ssl: postgresSslOption(normalizedSslMode),
connectionTimeoutMillis: normalizeRuntimeTimeoutMs(timeoutMs)
};
}
export class CloudRuntimeStore {
constructor({ now = () => new Date().toISOString() } = {}) {
this.now = now;
@@ -1139,11 +1148,11 @@ export class PostgresCloudRuntimeStore {
throw driverError;
}
this.pool = new Pool({
connectionString: this.dbUrl,
ssl: this.sslMode === "require" ? { rejectUnauthorized: false } : false,
connectionTimeoutMillis: normalizeRuntimeTimeoutMs(this.env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS)
});
this.pool = new Pool(buildPostgresPoolConfig({
dbUrl: this.dbUrl,
sslMode: this.sslMode,
timeoutMs: this.env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS
}));
return this.pool;
}
@@ -1310,6 +1319,43 @@ function normalizeRuntimeAdapter(value) {
return RUNTIME_STORE_KIND;
}
function normalizePostgresSslMode(value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (["disable", "false", "0", "off", "no"].includes(normalized)) {
return "disable";
}
return "require";
}
function postgresSslOption(sslMode) {
return sslMode === "disable" ? false : { rejectUnauthorized: false };
}
function normalizePostgresConnectionStringForSslMode(dbUrl, sslMode) {
if (typeof dbUrl !== "string" || dbUrl.trim() === "") {
return dbUrl;
}
let url;
try {
url = new URL(dbUrl);
} catch {
return dbUrl;
}
if (!["postgres:", "postgresql:"].includes(url.protocol)) {
return dbUrl;
}
for (const key of ["ssl", "sslmode", "sslcert", "sslkey", "sslrootcert"]) {
url.searchParams.delete(key);
}
if (sslMode !== "disable") {
url.searchParams.set("sslmode", "require");
}
return url.toString();
}
function postgresAdapterContract() {
return {
required: "Postgres-backed durable runtime adapter using HWLAB_CLOUD_DB_URL",
+58
View File
@@ -11,6 +11,7 @@ import {
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_STORE_KIND_POSTGRES,
buildPostgresPoolConfig,
createCloudRuntimeStore,
createConfiguredCloudRuntimeStore
} from "./runtime-store.mjs";
@@ -132,6 +133,63 @@ test("configured postgres runtime classifies SSL negotiation blocker separately
assert.equal(JSON.stringify(readiness).includes("The server does not support SSL connections"), false);
});
test("postgres pool config makes HWLAB_CLOUD_DB_SSL_MODE authoritative over URL sslmode", () => {
const disabled = buildPostgresPoolConfig({
dbUrl: "postgres://hwlab_user:fixture-pass@db.example.test:5432/hwlab?sslmode=require&application_name=hwlab",
sslMode: "disable",
timeoutMs: "2500"
});
assert.equal(disabled.ssl, false);
assert.equal(disabled.connectionTimeoutMillis, 2500);
const disabledUrl = new URL(disabled.connectionString);
assert.equal(disabledUrl.searchParams.get("sslmode"), null);
assert.equal(disabledUrl.searchParams.get("ssl"), null);
assert.equal(disabledUrl.searchParams.get("application_name"), "hwlab");
const required = buildPostgresPoolConfig({
dbUrl: "postgres://hwlab_user:fixture-pass@db.example.test:5432/hwlab?sslmode=disable",
sslMode: "require"
});
assert.deepEqual(required.ssl, { rejectUnauthorized: false });
assert.equal(new URL(required.connectionString).searchParams.get("sslmode"), "require");
});
test("configured postgres runtime passes normalized pool config to pg", async () => {
const pools = [];
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true",
HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS: "1800"
},
dbUrl: "postgres://hwlab_user:fixture-pass@db.example.test:5432/hwlab?sslmode=require",
sslMode: "disable",
pgModuleLoader: async () => ({
Pool: class FakePool {
constructor(config) {
this.config = config;
pools.push(this);
}
async query() {
return { rows: [] };
}
}
})
});
await store.readiness();
assert.equal(pools.length, 1);
assert.equal(pools[0].config.ssl, false);
assert.equal(pools[0].config.connectionTimeoutMillis, 1800);
assert.equal(new URL(pools[0].config.connectionString).searchParams.get("sslmode"), null);
assert.equal(store.summary().blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
assert.equal(store.summary().durabilityContract.blockedLayer, "schema");
});
test("configured postgres runtime classifies pg_hba no-encryption rejection as SSL before auth", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
+7 -6
View File
@@ -17,7 +17,8 @@ import {
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
buildPostgresPoolConfig
} from "../../internal/db/runtime-store.mjs";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
@@ -345,11 +346,11 @@ async function createPgPool(env, options) {
driverError.code = "HWLAB_PG_DRIVER_MISSING";
throw driverError;
}
return new Pool({
connectionString: env.HWLAB_CLOUD_DB_URL,
ssl: env.HWLAB_CLOUD_DB_SSL_MODE === "disable" ? false : { rejectUnauthorized: false },
connectionTimeoutMillis: normalizeTimeoutMs(env.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS)
});
return new Pool(buildPostgresPoolConfig({
dbUrl: env.HWLAB_CLOUD_DB_URL,
sslMode: env.HWLAB_CLOUD_DB_SSL_MODE,
timeoutMs: normalizeTimeoutMs(env.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS)
}));
}
async function rollbackQuietly(client) {
@@ -191,6 +191,31 @@ test("live dry-run closes pg pool after readiness verification", async () => {
assert.ok(pools[0].calls.some((call) => call.type === "end"));
});
test("live dry-run normalizes pg pool ssl config before readiness verification", async () => {
const backingClient = createFakeQueryClient({ migrationReady: true });
const pools = [];
const report = await buildDevRuntimeMigrationReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: {
HWLAB_CLOUD_DB_URL: "postgresql://hwlab:fixture-pass@db.example.test:5432/hwlab?sslmode=require",
HWLAB_CLOUD_DB_SSL_MODE: "disable",
HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS: "1600"
},
pgModuleLoader: async () => createFakePgModule({ backingClient, pools }),
now: () => "2026-05-22T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "ready");
assert.equal(pools.length, 1);
assert.equal(pools[0].config.ssl, false);
assert.equal(pools[0].config.connectionTimeoutMillis, 1600);
assert.equal(new URL(pools[0].config.connectionString).searchParams.get("sslmode"), null);
assert.equal(JSON.stringify(report).includes("fixture-pass"), false);
assert.equal(JSON.stringify(report).includes("db.example.test"), false);
});
test("apply mode uses a dedicated pg client transaction", async () => {
const backingClient = createFakeQueryClient({ migrationReady: false });
const pools = [];