From 41b4398b3c1dc6fc014887e4581ac1cbc29fcec9 Mon Sep 17 00:00:00 2001 From: CemAyyildiz Date: Mon, 31 Aug 2026 16:50:54 +0300 Subject: [PATCH] fix(keeper): redact upstream errors from health responses (#276) Return stable public health messages while logging detailed upstream failures server-side only, with regression tests for secret-bearing errors. Co-authored-by: Cursor --- services/keeper/src/status-server.test.ts | 32 +++++++++++++++++++++++ services/keeper/src/status-server.ts | 4 ++- services/keeper/src/status.test.ts | 25 ++++++++++++++++++ services/keeper/src/status.ts | 7 +++-- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/services/keeper/src/status-server.test.ts b/services/keeper/src/status-server.test.ts index 5cbd993..0209d92 100644 --- a/services/keeper/src/status-server.test.ts +++ b/services/keeper/src/status-server.test.ts @@ -180,6 +180,38 @@ test("GET /healthz returns 503 when drand is down", async () => { assert.equal(res.status, 503); const body = res.body as Record; assert.equal(body.ok, false); + assert.equal(body.reason, "health check failed"); + assert.doesNotMatch(JSON.stringify(body), /drand down/); + }, + ); +}); + +test("GET /healthz redacts secret-bearing upstream errors from the response body", async () => { + const secretRpc = "https://rpc.example.internal/secret-token-abc123"; + await withServer( + makeSource({ + reader: { + getRound: async () => { + throw new Error(`connection refused to ${secretRpc}`); + }, + getBidState: async () => ({ revealed_value: null }) as never, + }, + drand: { + chain: () => ({ + info: async () => { + throw new Error(`connection refused to ${secretRpc}`); + }, + }), + } as never, + }), + async (server) => { + const res = await get(server, "/healthz"); + assert.equal(res.status, 503); + const body = res.body as Record; + assert.equal(body.reason, "health check failed"); + const serialized = JSON.stringify(body); + assert.doesNotMatch(serialized, /secret-token-abc123/); + assert.doesNotMatch(serialized, /rpc\.example\.internal/); }, ); }); diff --git a/services/keeper/src/status-server.ts b/services/keeper/src/status-server.ts index ea8ae97..7a49231 100644 --- a/services/keeper/src/status-server.ts +++ b/services/keeper/src/status-server.ts @@ -140,11 +140,13 @@ function healthzHandler( }, }; } catch (e) { + const detail = e instanceof Error ? e.message : String(e); + console.error("[keeper-healthz] health check failed:", detail); return { status: 503, body: { ok: false, - reason: e instanceof Error ? e.message : String(e), + reason: "health check failed", now: clock.toISOString(), }, }; diff --git a/services/keeper/src/status.test.ts b/services/keeper/src/status.test.ts index 7596f39..2bf0af8 100644 --- a/services/keeper/src/status.test.ts +++ b/services/keeper/src/status.test.ts @@ -282,6 +282,31 @@ describe("buildKeeperStatus — upstream failure", () => { assert.match(String(health.reason ?? ""), /drand/); }); + it("redacts upstream error details from health reason", async () => { + const secretUrl = "https://rpc.internal.example/secret-key-xyz"; + const reader = { + getRound: async () => { + throw new Error(`connection refused: ${secretUrl}`); + }, + getBidState: async () => ({ revealed_value: null }) as never, + }; + const drand = { + chain: () => ({ + info: async () => { + throw new Error(`timeout contacting ${secretUrl}`); + }, + }), + } as never; + + const health = await checkHealth(reader, drand); + assert.equal(health.rpc, "down"); + assert.equal(health.drand, "down"); + assert.equal(health.reason, "rpc: unavailable; drand: unavailable"); + const serialized = JSON.stringify(health); + assert.doesNotMatch(serialized, /secret-key-xyz/); + assert.doesNotMatch(serialized, /rpc\.internal\.example/); + }); + it("does not crash when a tracked round is missing on-chain", async () => { const source = makeSource({ reader: readerNotFound(), diff --git a/services/keeper/src/status.ts b/services/keeper/src/status.ts index d2aeeff..2c7144e 100644 --- a/services/keeper/src/status.ts +++ b/services/keeper/src/status.ts @@ -293,7 +293,8 @@ export async function checkHealth( // healthy-enough: reachable } else { rpc = "down"; - reasons.push(`rpc: ${msg}`); + console.error("[keeper-health] rpc probe failed:", msg); + reasons.push("rpc: unavailable"); } } @@ -301,7 +302,9 @@ export async function checkHealth( await drand.chain().info(); } catch (e) { drandStatus = "down"; - reasons.push(`drand: ${e instanceof Error ? e.message : String(e)}`); + const msg = e instanceof Error ? e.message : String(e); + console.error("[keeper-health] drand probe failed:", msg); + reasons.push("drand: unavailable"); } const worst = rpc === "down" || drandStatus === "down" ? "down" : "ok";