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

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:
jochen
2026-10-13 07:32:19 +02:00
parent 53e5186496
commit fe2040c65e
23 changed files with 632 additions and 13 deletions
+2 -1
View File
@@ -103,7 +103,8 @@
"path": "${dir:mesh-state}/config.json",
"mode": "0600",
"content": "{\n \"password\": \"${secret:admin}\",\n \"host\": \"${bound:route:name}\"\n}\n",
"merge": "json"
"merge": "json",
"protected": ["password"]
}
],
"build": {
+15 -2
View File
@@ -138,6 +138,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 GiteaClient {
readonly baseUrl: string;
private readonly tokens: TokenSource;
@@ -151,7 +162,8 @@ export class GiteaClient {
/**
* Build from the module's resolved environment. The URL comes from MESH_GITEA_URL (the mesh's own
* name), falling back to the bare GITEA_URL and to the forge's loopback port. The token, in order:
* one configured in settings or the environment (MESH_GITEA_TOKEN / GITEA_TOKEN), which wins; else
* one configured in the environment (MESH_GITEA_TOKEN / GITEA_TOKEN), which wins — never a setting, which is
* ignored and said (hq issue 342); else
* one the module mints for itself with the admin account the vault delivered and keeps in its own
* state (token.ts; hq issue 100). Throws only when neither is possible, naming what is missing,
* rather than hand back a client that fails on first use.
@@ -159,7 +171,8 @@ export class GiteaClient {
static fromEnv(env: NodeJS.ProcessEnv = process.env): GiteaClient {
const cfg = meshConfig(env.MESH_GITEA_CONFIG_FILE);
const url = cfg.url ?? env.MESH_GITEA_URL ?? env.GITEA_URL ?? `http://127.0.0.1:${env.GITEA_PORT ?? "3000"}`;
const configured = cfg.token ?? env.MESH_GITEA_TOKEN ?? env.GITEA_TOKEN;
ignoreSecretSetting(cfg, "token");
const configured = env.MESH_GITEA_TOKEN ?? env.GITEA_TOKEN;
if (configured) return new GiteaClient(url, new ConfiguredToken(configured));
return new GiteaClient(url, MintedToken.fromEnv(url, env));
}
+41
View File
@@ -0,0 +1,41 @@
// 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 { GiteaClient } from "../dist/client.js";
import { ConfiguredToken, MintedToken } from "../dist/token.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(), "gitea-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 token 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({ token: SETTING });
const env = { MESH_GITEA_CONFIG_FILE: at.config, MESH_GITEA_URL: "http://127.0.0.1:1", MESH_GITEA_ADMIN_USER: "mesh-admin", MESH_GITEA_ADMIN_PASSWORD_FILE: at.vault, MESH_GITEA_STATE_DIR: at.dir };
const first = GiteaClient.fromEnv(env);
const second = GiteaClient.fromEnv(env);
// No token from the setting: the module mints its own with the vault's admin account.
assert.ok((first as any).tokens instanceof MintedToken);
assert.ok((second as any).tokens instanceof MintedToken);
assert.ok(!((first as any).tokens instanceof ConfiguredToken));
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();
}
});
+2 -1
View File
@@ -141,7 +141,8 @@
"path": "${dir:mesh-state}/config.json",
"mode": "0600",
"content": "{\n \"user\": \"admin\",\n \"password\": \"${secret:admin}\"\n}\n",
"merge": "json"
"merge": "json",
"protected": ["password"]
}
],
"requires": [
+13 -1
View File
@@ -27,6 +27,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`);
}
/** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`);
* absent or unreadable yields undefined so callers fall back rather than crash. */
function readSecret(file?: string): string | undefined {
@@ -53,7 +64,8 @@ export class HomeAssistantClient {
static fromEnv(env: NodeJS.ProcessEnv = process.env): HomeAssistantClient {
const cfg = meshConfig(env.MESH_HOMEASSISTANT_CONFIG_FILE);
const url = cfg.url ?? env.MESH_HOMEASSISTANT_URL ?? `http://127.0.0.1:${env.HOMEASSISTANT_PORT ?? "8123"}`;
const token = cfg.token ?? readSecret(env.MESH_HOMEASSISTANT_TOKEN_FILE) ?? env.MESH_HOMEASSISTANT_TOKEN;
ignoreSecretSetting(cfg, "token");
const token = readSecret(env.MESH_HOMEASSISTANT_TOKEN_FILE) ?? env.MESH_HOMEASSISTANT_TOKEN;
if (!token) throw new Error("no Home Assistant token — set MESH_HOMEASSISTANT_TOKEN");
return new HomeAssistantClient(url, token);
}
+1 -1
View File
@@ -7,7 +7,7 @@
"scripts": {
"build": "tsc client.ts index.ts tools/index.ts provisions/hass.ts provisions/probe.ts provisions/connections.ts provisions/mesh.ts provisions/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist",
"typecheck": "tsc -p tsconfig.json",
"test": "node --test --experimental-strip-types 'test/*.test.ts'"
"test": "npm run build && node --test --experimental-strip-types 'test/*.test.ts'"
},
"dependencies": {
"@novox/mesh-sdk": "^0.1.0"
@@ -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 { HomeAssistantClient } 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(), "home-assistant-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 token 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({ token: SETTING });
const env = { MESH_HOMEASSISTANT_CONFIG_FILE: at.config, MESH_HOMEASSISTANT_TOKEN_FILE: at.vault };
const first = HomeAssistantClient.fromEnv(env);
const second = HomeAssistantClient.fromEnv(env);
assert.equal((first as any).token, VAULT);
assert.equal((second as any).token, VAULT);
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();
}
});
+21 -1
View File
@@ -42,6 +42,24 @@ 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`);
}
/** A secret delivered as a file, trimmed; undefined when there is none. */
function readSecret(path: string | undefined): string | undefined {
if (!path) return undefined;
try { return readFileSync(path, "utf8").trim() || undefined; }
catch { return undefined; }
}
export class IcecastClient {
readonly baseUrl: string;
private readonly authHeader?: string;
@@ -57,7 +75,9 @@ export class IcecastClient {
static fromEnv(env: NodeJS.ProcessEnv = process.env): IcecastClient {
const cfg = meshConfig(env.MESH_ICECAST_CONFIG_FILE);
const url = cfg.url ?? (env.MESH_ICECAST_URL ?? `http://127.0.0.1:${env.ICECAST_PORT ?? "8000"}`);
return new IcecastClient(url, cfg.user ?? env.MESH_ICECAST_ADMIN_USER, cfg.password ?? env.MESH_ICECAST_ADMIN_PASSWORD);
ignoreSecretSetting(cfg, "password");
const password = readSecret(env.MESH_ICECAST_ADMIN_PASSWORD_FILE) ?? env.MESH_ICECAST_ADMIN_PASSWORD;
return new IcecastClient(url, cfg.user ?? env.MESH_ICECAST_ADMIN_USER ?? "admin", password);
}
async getStatus(): Promise<IcecastStatus> {
+2 -1
View File
@@ -120,7 +120,8 @@
],
"env": {
"MESH_ICECAST_URL": "http://127.0.0.1:${port:8000}",
"MESH_ICECAST_CONFIG_FILE": "${dir:mesh-state}/config.json"
"MESH_ICECAST_CONFIG_FILE": "${dir:mesh-state}/config.json",
"MESH_ICECAST_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret"
}
}
]
@@ -0,0 +1,39 @@
// 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 { IcecastClient } 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(), "icecast-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_ICECAST_CONFIG_FILE: at.config, MESH_ICECAST_ADMIN_PASSWORD_FILE: at.vault };
const first = IcecastClient.fromEnv(env);
const second = IcecastClient.fromEnv(env);
const basic = "Basic " + Buffer.from(`admin:${VAULT}`).toString("base64");
assert.equal((first as any).authHeader, basic);
assert.equal((second as any).authHeader, basic);
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();
}
});
+13 -1
View File
@@ -43,6 +43,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`);
}
/** A secret delivered as a file, trimmed; undefined when there is none, so the caller can fall back. */
function tokenFromFile(file?: string): string | undefined {
if (!file) return undefined;
@@ -71,7 +82,8 @@ export class InfluxDBClient {
const url = cfg.url ?? env.MESH_INFLUXDB_URL ?? `http://127.0.0.1:${env.INFLUXDB_PORT ?? "8086"}`;
// The token reaches the process as a file (novox/hq ADR 0086); the environment variable stays
// only for a workstation running the tools by hand.
const token = cfg.token ?? tokenFromFile(env.MESH_INFLUXDB_TOKEN_FILE) ?? env.MESH_INFLUXDB_TOKEN;
ignoreSecretSetting(cfg, "token");
const token = tokenFromFile(env.MESH_INFLUXDB_TOKEN_FILE) ?? env.MESH_INFLUXDB_TOKEN;
if (!token) throw new Error("no InfluxDB token — set MESH_INFLUXDB_TOKEN_FILE");
const org = cfg.org ?? env.MESH_INFLUXDB_ORG ?? "mesh";
return new InfluxDBClient(url, token, org);
@@ -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 { InfluxDBClient } 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(), "influxdb-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 token 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({ token: SETTING });
const env = { MESH_INFLUXDB_CONFIG_FILE: at.config, MESH_INFLUXDB_TOKEN_FILE: at.vault };
const first = InfluxDBClient.fromEnv(env);
const second = InfluxDBClient.fromEnv(env);
assert.equal((first as any).token, VAULT);
assert.equal((second as any).token, VAULT);
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();
}
});
+2 -1
View File
@@ -114,7 +114,8 @@
"path": "${dir:mesh-state}/config.json",
"mode": "0600",
"content": "{\n \"password\": \"${secret:server-password}\"\n}\n",
"merge": "json"
"merge": "json",
"protected": ["password"]
}
],
"build": {
+13 -1
View File
@@ -52,6 +52,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`);
}
/** Read a secret from the file the mesh mounted it at (an own-secret), if the pointing env is set. */
function readSecret(path: string | undefined): string | undefined {
if (!path) return undefined;
@@ -81,7 +92,8 @@ export class MailuClient {
static fromEnv(env: NodeJS.ProcessEnv = process.env): MailuClient {
const cfg = meshConfig(env.MESH_MAILU_CONFIG_FILE);
const url = cfg.url ?? env.MESH_MAILU_URL;
const apiKey = cfg.apiKey ?? readSecret(env.MESH_MAILU_API_KEY_FILE) ?? env.MESH_MAILU_API_KEY;
ignoreSecretSetting(cfg, "apiKey");
const apiKey = readSecret(env.MESH_MAILU_API_KEY_FILE) ?? env.MESH_MAILU_API_KEY;
if (!url || !apiKey) {
throw new Error("Mailu is not configured — set MESH_MAILU_URL and the API token own-secret");
}
+38
View 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 { MailuClient } 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(), "mailu-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 apiKey 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({ apiKey: SETTING });
const env = { MESH_MAILU_CONFIG_FILE: at.config, MESH_MAILU_URL: "http://127.0.0.1:1/api/v1", MESH_MAILU_API_KEY_FILE: at.vault };
const first = MailuClient.fromEnv(env);
const second = MailuClient.fromEnv(env);
assert.equal((first as any).apiKey, VAULT);
assert.equal((second as any).apiKey, VAULT);
assert.deepEqual(said, ["the setting apiKey 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();
}
});
+13 -1
View File
@@ -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();
}
});
+2 -1
View File
@@ -131,7 +131,8 @@
"path": "${dir:mesh-state}/config.json",
"mode": "0600",
"content": "{\n \"site\": \"default\",\n \"password\": \"${secret:controller}\"\n}\n",
"merge": "json"
"merge": "json",
"protected": ["password"]
}
],
"requires": [