Make grafana prefer the vault's password, and let the secrets list only shrink (hq issue 342)
mesh/merge-gate head eb4280637906 merged into trunk 53e51864969f — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…
mesh/repo-check head eb4280637906 merged into trunk 53e51864969f — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
mesh/delivery-group group fix/342-a-secret-is-never-a-setting delivered: every member is delivered

grafana's client logged in with a token setting ahead of the vault's admin password,
and its runtime config let a layer set token. The token setting is now ignored and
said, token is protected, and the vault's password comes first.

checks/secrets/known could gain a line hiding a newly unprotected key. merge-check.sh
now judges it against the trunk's copy at the merge base, and refuses an added line.
This commit is contained in:
jochen
2026-10-13 07:43:08 +02:00
parent fe2040c65e
commit eb42806379
7 changed files with 161 additions and 16 deletions
+20 -6
View File
@@ -44,6 +44,17 @@ function meshConfig(file?: string): Record<string, string> {
catch { return {}; }
}
/**
* A password, token or API key is the vault's, never a setting (novox/hq issue 342): a value for one of
* them in the settings file is ignored, and that is said once — by its key, never with its value.
*/
const saidIgnored = new Set<string>();
function ignoreSecretSetting(cfg: Record<string, unknown>, key: string): void {
if (cfg[key] === undefined || saidIgnored.has(key)) return;
saidIgnored.add(key);
console.error(`the setting ${key} is ignored: it comes from the vault`);
}
export class GrafanaClient {
readonly baseUrl: string;
private readonly authHeader: string;
@@ -54,21 +65,24 @@ export class GrafanaClient {
}
/**
* Build from the module's resolved environment. Auth is a service-account/API token
* (MESH_GRAFANA_TOKEN, sent as Bearer) when present, else HTTP basic with the admin password the
* module keeps as its own secret (MESH_GRAFANA_PASSWORD, user MESH_GRAFANA_USER, default admin).
* Throws when neither is configured — the module then contributes nothing rather than failing.
* Build from the module's resolved environment. Auth is HTTP basic with the vault's admin password,
* which the runtime config carries as `password` (kept protected, so no setting replaces it) or
* MESH_GRAFANA_PASSWORD (user MESH_GRAFANA_USER, default admin); else a service-account/API token from
* the environment (MESH_GRAFANA_TOKEN, sent as Bearer). A `token` in the settings file is never used: a
* credential is the vault's, never a setting (hq issue 342). Throws when neither is configured — the
* module then contributes nothing rather than failing.
*/
static fromEnv(env: NodeJS.ProcessEnv = process.env): GrafanaClient {
const cfg = meshConfig(env.MESH_GRAFANA_CONFIG_FILE);
const url = cfg.url ?? env.MESH_GRAFANA_URL ?? `http://127.0.0.1:${env.GRAFANA_PORT ?? "3000"}`;
const token = cfg.token ?? env.MESH_GRAFANA_TOKEN;
if (token) return new GrafanaClient(url, `Bearer ${token}`);
ignoreSecretSetting(cfg, "token");
const password = cfg.password ?? env.MESH_GRAFANA_PASSWORD;
if (password) {
const user = cfg.user ?? env.MESH_GRAFANA_USER ?? "admin";
return new GrafanaClient(url, `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`);
}
const token = env.MESH_GRAFANA_TOKEN;
if (token) return new GrafanaClient(url, `Bearer ${token}`);
throw new Error("no Grafana auth — set MESH_GRAFANA_TOKEN or MESH_GRAFANA_PASSWORD");
}
+1 -1
View File
@@ -142,7 +142,7 @@
"mode": "0600",
"content": "{\n \"user\": \"admin\",\n \"password\": \"${secret:admin}\"\n}\n",
"merge": "json",
"protected": ["password"]
"protected": ["password", "token"]
}
],
"requires": [
@@ -0,0 +1,40 @@
// A password, a token or an API key is the vault's, never a setting (novox/hq issue 342): grafana logs in
// with the vault's admin password, a `token` in the settings file is ignored, and that is said once, by the
// key alone. The runtime config keeps both keys protected, so the controller refuses a layer setting either.
import { test, mock } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { GrafanaClient } from "../dist/client.js";
const SETTING = "one-a-setting-carried";
const VAULT = "the-vault-made-this";
test("the vault's admin password is used, a token setting is ignored and said once, without its value", () => {
const said: string[] = [];
const err = mock.method(console, "error", (...args: unknown[]) => { said.push(args.join(" ")); });
try {
const dir = mkdtempSync(join(tmpdir(), "grafana-342-"));
// What the runtime config holds once the vault filled it, with a token a layer carried beside it.
writeFileSync(join(dir, "config.json"), JSON.stringify({ user: "admin", password: VAULT, token: SETTING }));
const env = { MESH_GRAFANA_CONFIG_FILE: join(dir, "config.json") };
const basic = "Basic " + Buffer.from(`admin:${VAULT}`).toString("base64");
assert.equal((GrafanaClient.fromEnv(env) as any).authHeader, basic);
assert.equal((GrafanaClient.fromEnv(env) as any).authHeader, basic);
assert.deepEqual(said, ["the setting token is ignored: it comes from the vault"]);
assert.ok(!said.join("\n").includes(SETTING), "the setting's value is never said");
} finally {
err.mock.restore();
}
});
test("the runtime config keeps password and token protected", () => {
const manifest = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8")) as {
resources: { id: string; protected?: string[] }[];
};
const runtime = manifest.resources.find((r) => r.id === "runtime-config");
assert.deepEqual(runtime?.protected, ["password", "token"]);
});