feat(config): load one validated secret bundle

This commit is contained in:
2026-07-12 11:06:19 +02:00
parent b539303002
commit 5fe74612fb
9 changed files with 255 additions and 2 deletions
+104
View File
@@ -0,0 +1,104 @@
import {
closeSync, constants, fstatSync, lstatSync, openSync, readFileSync,
type Stats,
} from "node:fs";
/** Keys accepted by the deployment bundle. Keep this list intentionally explicit. */
export const SECRET_BUNDLE_KEYS = Object.freeze([
"THT_MODEL_API_KEY", "THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY",
"THT_CA", "THT_SSL_CA", "THT_VECTOR_BOOTSTRAP_PASSWORD", "THT_VECTOR_MIGRATOR_PASSWORD",
"THT_VECTOR_READER_PASSWORD", "THT_VECTOR_WRITER_PASSWORD", "PI_PROVIDER_API_KEY",
] as const);
const ALLOWED = new Set<string>(SECRET_BUNDLE_KEYS);
const LEGACY_FILES: Readonly<Record<string, string>> = {
THT_MODEL_API_KEY: "THT_MODEL_API_KEY_SECRET_FILE",
THT_DWH_API_KEY: "THT_DWH_API_KEY_SECRET_FILE",
THT_VEC_API_KEY: "THT_VEC_API_KEY_SECRET_FILE",
THT_VEC_WRITE_API_KEY: "THT_VEC_WRITE_API_KEY_SECRET_FILE",
THT_CA: "THT_CA_SECRET_FILE",
THT_SSL_CA: "THT_CA_SECRET_FILE",
THT_VECTOR_BOOTSTRAP_PASSWORD: "THT_VECTOR_BOOTSTRAP_PASSWORD_SECRET_FILE",
THT_VECTOR_MIGRATOR_PASSWORD: "THT_VECTOR_MIGRATOR_PASSWORD_SECRET_FILE",
THT_VECTOR_READER_PASSWORD: "THT_VECTOR_READER_PASSWORD_SECRET_FILE",
THT_VECTOR_WRITER_PASSWORD: "THT_VECTOR_WRITER_PASSWORD_SECRET_FILE",
};
const MAX_BUNDLE_BYTES = 64 * 1024;
const MAX_LINE_BYTES = 16 * 1024;
export interface SecretBundleConfig {
secretsFile?: string;
secretFiles?: Readonly<Record<string, string | undefined>>;
}
function unavailable(): Error { return new Error("secret bundle is unavailable"); }
function secureStat(info: Stats, docker: boolean): boolean {
const mode = info.mode & 0o777;
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > MAX_BUNDLE_BYTES) return false;
if (docker) return info.uid === 0 && mode === 0o444;
return info.uid === (process.getuid?.() ?? info.uid) && (mode === 0o400 || mode === 0o600);
}
function readSecure(file: string): string {
let fd: number | undefined;
try {
if (!file || file.trim() !== file || file.includes("\0")) throw unavailable();
const docker = file.startsWith("/run/secrets/") && !file.slice("/run/secrets/".length).includes("/");
if (file.startsWith("/run/secrets/") && !docker) throw unavailable();
if (docker) {
const parent = lstatSync("/run/secrets");
if (!parent.isDirectory() || parent.uid !== 0 || (parent.mode & 0o022) !== 0) throw unavailable();
}
const before = lstatSync(file);
if (!secureStat(before, docker)) throw unavailable();
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
const opened = fstatSync(fd);
if (!secureStat(opened, docker) || before.dev !== opened.dev || before.ino !== opened.ino) throw unavailable();
return readFileSync(fd, "utf8");
} catch {
throw unavailable();
} finally {
if (fd !== undefined) try { closeSync(fd); } catch { /* sanitized by design */ }
}
}
function parseBundle(text: string): ReadonlyMap<string, string> {
const values = new Map<string, string>();
const lines = text.split("\n");
for (const raw of lines) {
if (raw.length > MAX_LINE_BYTES) throw unavailable();
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
if (!match) throw unavailable();
const [, key, value] = match;
if (!ALLOWED.has(key) || values.has(key) || value.length === 0 || /[\r\n]/.test(value)) {
throw unavailable();
}
values.set(key, value);
}
return values;
}
export function loadSecretBundle(file: string): ReadonlyMap<string, string> {
try { return parseBundle(readSecure(file)); } catch { throw unavailable(); }
}
/** Resolve a value from the bundle, with the pre-bundle *_SECRET_FILE fallback. */
export function secretValue(config: SecretBundleConfig, key: string): string | undefined {
if (config.secretsFile) {
const found = loadSecretBundle(config.secretsFile).get(key);
if (found !== undefined) return found;
}
const legacyName = LEGACY_FILES[key];
const legacyPath = legacyName ? config.secretFiles?.[legacyName] : undefined;
if (!legacyPath) return undefined;
const value = readSecure(legacyPath);
if (!value || /\s/.test(value)) throw unavailable();
return value;
}
export function legacySecretEnvNames(): Readonly<Record<string, string>> { return LEGACY_FILES; }