102 lines
2.8 KiB
TypeScript
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}`);
|
|
}
|
|
}
|