mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
The probe decided on the eight oldest values alone. An install reinstalled under a fresh key long ago holds old rows sealed under a key that is gone and all current data under the key it runs with; it works today, and the check would refuse it with 503 on every request. When every oldest value fails, the newest values of the same columns are now sampled in reverse order. One that opens makes the verdict inconclusive (mixed): the process serves, warns and probes again at the next start. Only old and newest values that all fail are a mismatch. A newest-only open never seals; sealing stays with the oldest-first rule. Every probe query now runs in its own transaction under a transaction-local statement_timeout (2 s), so a large or locked table ends that column with an error and the probe as incomplete instead of holding up the boot. The newest order is the exact mirror of the oldest, so an index on the timestamp serves both directions. The module comment lists the three conditions that still yield a mismatch.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
/**
|
|
* Runs the encryption key check once per process at boot and records the
|
|
* verdict for the request path (`./key-mismatch-state.ts`).
|
|
*
|
|
* Returns true when the process must refuse: the caller then starts neither
|
|
* the queue producer nor the worker, because a process holding the wrong key
|
|
* must never write. The process keeps running so `/api/health` and the
|
|
* explanation page can say why; a crash loop would hide the reason in a log
|
|
* that the people most likely to hit this (a NAS app catalog install) never
|
|
* open.
|
|
*/
|
|
import {
|
|
checkEncryptionKeyCanaries,
|
|
keyMismatchLogBlock,
|
|
type CanaryClient,
|
|
type InconclusiveReason,
|
|
} from "@/lib/crypto/canary";
|
|
import {
|
|
getKeyCheckMode,
|
|
setKeyMismatchState,
|
|
setKeyMismatchWarning,
|
|
} from "./key-mismatch-state";
|
|
|
|
const INCONCLUSIVE_EXPLANATION: Record<InconclusiveReason, string> = {
|
|
"single-value": "the only stored value found under that id did not open.",
|
|
mixed:
|
|
"the oldest stored values under that id do not open, newer ones do. " +
|
|
"Either the old rows were sealed under a key that is gone, or a wrong " +
|
|
"key served for a while and wrote the newer ones; check that this is " +
|
|
"the key the database was first written with.",
|
|
unsampled:
|
|
"the values stored under that id are too large to probe at start-up.",
|
|
incomplete:
|
|
"the probe of the stored data did not finish in time or a table could " +
|
|
"not be read.",
|
|
};
|
|
|
|
export async function runBootKeyCheck(client: CanaryClient): Promise<boolean> {
|
|
const mode = getKeyCheckMode();
|
|
const outcome = await checkEncryptionKeyCanaries(client, { mode });
|
|
setKeyMismatchWarning(null);
|
|
if (outcome.state === "mismatch") {
|
|
const state = {
|
|
keyIds: outcome.keyIds,
|
|
detectedAt: new Date().toISOString(),
|
|
};
|
|
console.error(keyMismatchLogBlock(outcome.keyIds));
|
|
if (mode === "warn") {
|
|
// The operator turned the refusal off. Say so next to the block, keep
|
|
// the finding for /api/health, and serve.
|
|
setKeyMismatchState(null);
|
|
setKeyMismatchWarning(state);
|
|
console.error(
|
|
"[boot] ENCRYPTION_KEY_CHECK=warn: serving anyway. No key is recorded " +
|
|
"while the check does not pass. Remove the setting once the key is " +
|
|
"confirmed.",
|
|
);
|
|
return false;
|
|
}
|
|
setKeyMismatchState(state);
|
|
return true;
|
|
}
|
|
setKeyMismatchState(null);
|
|
if (outcome.state === "error") {
|
|
// The check could not run. The loaders and the database each have their
|
|
// own loud signal for that; serving stays on.
|
|
console.warn(
|
|
`[boot] Encryption key check skipped: ${outcome.message.slice(0, 300)}`,
|
|
);
|
|
} else {
|
|
for (const { keyId, reason } of outcome.inconclusive) {
|
|
console.warn(
|
|
`[boot] Encryption key check inconclusive for key id ${keyId}: ` +
|
|
`${INCONCLUSIVE_EXPLANATION[reason]} Serving; no key recorded; ` +
|
|
"the check runs again at the next start.",
|
|
);
|
|
}
|
|
if (mode === "warn" && outcome.inconclusive.length > 0) {
|
|
console.warn(
|
|
"[boot] ENCRYPTION_KEY_CHECK=warn: no key recorded on this start " +
|
|
"because the check did not pass for every key id.",
|
|
);
|
|
}
|
|
if (outcome.written.length > 0) {
|
|
console.info(
|
|
`[boot] Encryption key check: recorded key id(s) ${outcome.written.join(", ")}`,
|
|
);
|
|
}
|
|
}
|
|
return false;
|
|
}
|