Files
pieced-portal/src/lib/openbao.ts
2026-04-11 17:21:52 +02:00

102 lines
2.8 KiB
TypeScript

import { readFileSync } from "fs";
const OPENBAO_ADDR =
process.env.OPENBAO_ADDR || "http://openbao.openbao.svc:8200";
const SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
const K8S_AUTH_ROLE = process.env.OPENBAO_K8S_ROLE || "pieced-portal";
const K8S_AUTH_MOUNT = process.env.OPENBAO_K8S_MOUNT || "kubernetes";
let cachedToken: { token: string; expiresAt: number } | null = null;
async function authenticate(): Promise<string> {
if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) {
return cachedToken.token;
}
const jwt = readFileSync(SA_TOKEN_PATH, "utf-8").trim();
const res = await fetch(
`${OPENBAO_ADDR}/v1/auth/${K8S_AUTH_MOUNT}/login`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: K8S_AUTH_ROLE, jwt }),
}
);
if (!res.ok) {
const body = await res.text();
throw new Error(`OpenBao K8s auth failed: ${res.status} ${body}`);
}
const data = await res.json();
const token = data.auth.client_token as string;
const leaseDuration = (data.auth.lease_duration as number) || 3600;
cachedToken = {
token,
expiresAt: Date.now() + leaseDuration * 1000,
};
return token;
}
/**
* Read a KV v2 secret. Path relative to KV mount.
* Returns .data.data object or null if 404.
*/
export async function readSecret(
path: string
): Promise<Record<string, string> | null> {
const token = await authenticate();
const res = await fetch(`${OPENBAO_ADDR}/v1/secret/data/${path}`, {
headers: { "X-Vault-Token": token },
});
if (res.status === 404) return null;
if (!res.ok) {
const body = await res.text();
throw new Error(`OpenBao read failed: ${res.status} ${body}`);
}
const json = await res.json();
return json.data?.data ?? null;
}
export async function writePackageSecrets(
tenantId: string,
packageId: string,
secrets: Record<string, string>
): Promise<void> {
const token = await authenticate();
const path = `secret/data/tenants/${tenantId}/${packageId}`;
const res = await fetch(`${OPENBAO_ADDR}/v1/${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Vault-Token": token,
},
body: JSON.stringify({ data: secrets }),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`OpenBao write failed: ${res.status} ${body}`);
}
}
export async function deletePackageSecrets(
tenantId: string,
packageId: string
): Promise<void> {
const token = await authenticate();
const path = `secret/metadata/tenants/${tenantId}/${packageId}`;
const res = await fetch(`${OPENBAO_ADDR}/v1/${path}`, {
method: "DELETE",
headers: { "X-Vault-Token": token },
});
if (!res.ok && res.status !== 404) {
const body = await res.text();
throw new Error(`OpenBao delete failed: ${res.status} ${body}`);
}
}