Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/lib/prometheus-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterEach, describe, expect, it } from "bun:test";
import { promAuthHeaders } from "./prometheus";

const KEY = "PROMETHEUS_BASIC_AUTH";
const original = process.env[KEY];

afterEach(() => {
if (original === undefined) delete process.env[KEY];
else process.env[KEY] = original;
});

describe("promAuthHeaders", () => {
it("sends nothing when no credential is configured", () => {
delete process.env[KEY];
expect(promAuthHeaders(new URL("https://prom.openchainbench.com/api/v1/query"))).toEqual({});
});

// The order of rollout depends on this: the code ships first and stays inert,
// the variable is set second, and only then does Caddy start demanding auth.
// If an unset variable produced a header, step one would already 401.
it("stays inert so it can ship before the edge demands auth", () => {
process.env[KEY] = "";
expect(promAuthHeaders(new URL("https://prom.openchainbench.com/api/v1/query"))).toEqual({});
});

it("encodes user:password as a basic credential", () => {
process.env[KEY] = "florent:s3cret";
const h = promAuthHeaders(new URL("https://prom.openchainbench.com/api/v1/query"));
expect(h.authorization).toBe(`Basic ${Buffer.from("florent:s3cret").toString("base64")}`);
// Decodes back to exactly what Caddy's basic_auth compares against.
const decoded = Buffer.from(h.authorization.slice("Basic ".length), "base64").toString("utf8");
expect(decoded).toBe("florent:s3cret");
});

it("trims surrounding whitespace, which a copied env value carries", () => {
process.env[KEY] = " florent:s3cret\n";
const h = promAuthHeaders(new URL("https://prom.openchainbench.com/api/v1/query"));
const decoded = Buffer.from(h.authorization.slice("Basic ".length), "base64").toString("utf8");
expect(decoded).toBe("florent:s3cret");
});

// A value with no colon is a misconfiguration. Sending it would answer 401
// on every bench with nothing to point at; refusing to send it leaves the
// benches working and the missing auth visible.
it("refuses a value that cannot be user:password", () => {
process.env[KEY] = "justatoken";
expect(promAuthHeaders(new URL("https://prom.openchainbench.com/api/v1/query"))).toEqual({});
});

// The worker reaches Prometheus in-network over plain http at ocb-prom:9090,
// which needs no credential. Forwarding one would put it somewhere it never
// has to be.
it("never sends the credential over plain http", () => {
process.env[KEY] = "florent:s3cret";
expect(promAuthHeaders(new URL("http://ocb-prom:9090/api/v1/query"))).toEqual({});
expect(promAuthHeaders(new URL("http://localhost:9090/api/v1/query"))).toEqual({});
});

it("keeps a password containing colons intact", () => {
process.env[KEY] = "florent:pa:ss:word";
const h = promAuthHeaders(new URL("https://prom.openchainbench.com/api/v1/query"));
const decoded = Buffer.from(h.authorization.slice("Basic ".length), "base64").toString("utf8");
expect(decoded).toBe("florent:pa:ss:word");
});
});
35 changes: 35 additions & 0 deletions src/lib/prometheus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ export class Prometheus {
}

private async fetchEnvelope<T>(url: URL, signal?: AbortSignal): Promise<T> {
const authHeaders = promAuthHeaders(url);

// Defense in depth against DNS-rebinding: the schema guards URL
// literals, but a hostname can resolve to a private address at fetch
// time. Refuse if any resolved IP is loopback / RFC1918 / link-local
Expand All @@ -206,6 +208,7 @@ export class Prometheus {
const merged = signal ? mergeSignals(signal, controller.signal) : controller.signal;
const res = await fetch(url, {
signal: merged,
headers: authHeaders,
// Refuse to follow redirects. blocks 3xx into a private host.
redirect: "manual",
// Cache at the platform level. pages call us through ISR.
Expand Down Expand Up @@ -442,6 +445,38 @@ const hostCheckCache = new Map<string, HostCheckEntry>();
// failure storms. First caller does the lookup, the rest await it.
const hostCheckInFlight = new Map<string, Promise<void>>();

/** Basic-auth header for the public Prometheus host, when one is configured.
*
* prom.openchainbench.com answers the internet, because the Vercel site
* queries it directly (PROMETHEUS_URL is set in production). Until something
* authenticates, Prometheus runs there with --web.enable-admin-api and no
* credential, so the destructive routes are blocked at the edge instead.
* This closes the rest.
*
* It has to be a header: Node's fetch refuses a URL that embeds credentials
* outright ("Request cannot be constructed from a URL that includes
* credentials"), so putting user:pass@ in PROMETHEUS_URL would not weaken
* auth, it would break every query.
*
* Absent the variable this adds nothing, which is what makes the edge
* switchable in the only safe order: ship this, set the variable, redeploy
* (env binds at deployment, not at request time), then turn auth on in
* Caddy. Reversed, every bench 401s until the redeploy lands.
*
* Never sent over plain http. The worker's in-network hop to ocb-prom needs
* no credential, and forwarding one would put it somewhere it never has to
* be. */
export function promAuthHeaders(url: URL): Record<string, string> {
if (url.protocol !== "https:") return {};
const raw = process.env.PROMETHEUS_BASIC_AUTH?.trim();
// A value without a colon cannot be user:password; sending it would be a
// silent 401 on every bench rather than a visible misconfiguration.
if (!raw || !raw.includes(":")) return {};
return {
authorization: `Basic ${Buffer.from(raw, "utf8").toString("base64")}`,
};
}

async function assertPublicHost(url: URL): Promise<void> {
// The worker on the VPS reaches Prometheus over the docker bridge by
// its container name (resolves to a private 172.18.0.0/16 address).
Expand Down
Loading