Keep every password, token and API key the vault's, never a setting (hq issue 342)
mesh/merge-gate head fe2040c65ea2 merged into trunk 53e51864969f — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…
mesh/repo-check head fe2040c65ea2 merged into trunk 53e51864969f — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery-group group fix/342-a-secret-is-never-a-setting ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head fe2040c65ea2 merged into trunk 53e51864969f — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…
mesh/repo-check head fe2040c65ea2 merged into trunk 53e51864969f — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery-group group fix/342-a-secret-is-never-a-setting ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
A setting outranked the vault: baserow, grafana, letta and unifi merged a layer's
password over the ${secret:...} in their runtime config, and the gitea,
home-assistant, icecast, influxdb, mailu and nextcloud clients read the settings
file before the vault's file. A secret in a setting cannot be rotated and is read
back by the settings verb.
The four files now keep their password protected, so the controller refuses a
setting of it. The six clients ignore a secret in the settings file and say so
once, by key and never with the value; icecast reads its admin password from the
vault's file. checks/secrets refuses a mergeable file whose secret a layer could
replace; searxng's nested server.secret_key is listed as known until the
controller protects a nested key.
This commit is contained in:
@@ -32,6 +32,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 NextcloudClient {
|
||||
constructor(
|
||||
private readonly container: string,
|
||||
@@ -53,7 +64,8 @@ export class NextcloudClient {
|
||||
const ocsUrl = cfg.url ?? env.MESH_NEXTCLOUD_URL ?? `http://127.0.0.1:${env.NEXTCLOUD_PORT ?? "80"}`;
|
||||
const adminUser = cfg.user ?? env.MESH_NEXTCLOUD_ADMIN_USER ?? "admin";
|
||||
const passwordFile = env.MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE;
|
||||
const adminPassword = cfg.password ?? env.MESH_NEXTCLOUD_ADMIN_PASSWORD
|
||||
ignoreSecretSetting(cfg, "password");
|
||||
const adminPassword = env.MESH_NEXTCLOUD_ADMIN_PASSWORD
|
||||
?? (passwordFile ? readFileSync(passwordFile, "utf8").trim() : undefined);
|
||||
if (!adminPassword) {
|
||||
throw new Error("no Nextcloud admin password — set MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE " +
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// A password, a token or an API key is the vault's, never a setting (novox/hq issue 342): a value for it in
|
||||
// the settings file the mesh merges is ignored, the vault's is used, and that is said once, by the key alone.
|
||||
// Run against the compiled module, the way the runtime loads it.
|
||||
|
||||
import { test, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { NextcloudClient } from "../dist/client.js";
|
||||
|
||||
const SETTING = "one-a-setting-carried";
|
||||
const VAULT = "the-vault-made-this";
|
||||
|
||||
function delivered(settings: Record<string, string>): Record<string, string> {
|
||||
const dir = mkdtempSync(join(tmpdir(), "nextcloud-342-"));
|
||||
writeFileSync(join(dir, "config.json"), JSON.stringify(settings));
|
||||
writeFileSync(join(dir, "vault.secret"), VAULT + "\n");
|
||||
return { dir, config: join(dir, "config.json"), vault: join(dir, "vault.secret") };
|
||||
}
|
||||
|
||||
test("the vault's password is used, a setting of it 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 at = delivered({ password: SETTING });
|
||||
const env = { MESH_NEXTCLOUD_CONFIG_FILE: at.config, MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE: at.vault };
|
||||
const first = NextcloudClient.fromEnv(env);
|
||||
const second = NextcloudClient.fromEnv(env);
|
||||
assert.equal((first as any).adminPassword, VAULT);
|
||||
assert.equal((second as any).adminPassword, VAULT);
|
||||
assert.deepEqual(said, ["the setting password 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user