Refuse a throwaway plugin storage secret in bootstrap, before any role is created
CI / full-gate (push) Successful in 2m56s
CI / full-gate (push) Successful in 2m56s
This commit is contained in:
@@ -19,6 +19,17 @@ test("web and bootstrap resolve the same plugin storage secret", () => {
|
||||
assert.match(resolvePluginDbSecret({ PLUGIN_DB_SECRET: "" }), /dev-insecure/); // empty is unset, not a secret
|
||||
});
|
||||
|
||||
// bootstrap writes these passwords into Postgres, so it must refuse the publicly-known throwaway
|
||||
// before creating a role with one — not leave web to notice afterwards.
|
||||
test("bootstrap refuses a missing, empty or throwaway plugin storage secret when hardened", () => {
|
||||
const hardened = { REQUIRE_SECURE_SECRETS: "true" };
|
||||
for (const secret of [undefined, "", "dev-insecure-plugin-db-secret"]) {
|
||||
const env = secret === undefined ? hardened : { ...hardened, PLUGIN_DB_SECRET: secret };
|
||||
assert.throws(() => resolvePluginDbSecret(env), /PLUGIN_DB_SECRET/, `for ${JSON.stringify(secret)}`);
|
||||
}
|
||||
assert.equal(resolvePluginDbSecret({ ...hardened, PLUGIN_DB_SECRET: "a-real-secret" }), "a-real-secret");
|
||||
});
|
||||
|
||||
test("loads dev defaults when the environment is empty", () => {
|
||||
const c = loadConfig({});
|
||||
assert.equal(c.port, 3000);
|
||||
|
||||
+8
-6
@@ -8,11 +8,13 @@ export type LogLevel = (typeof LOG_LEVELS)[number];
|
||||
|
||||
const DEV_PLUGIN_DB_SECRET = "dev-insecure-plugin-db-secret";
|
||||
|
||||
// bootstrap resolves the plugin-storage secret through this, web through loadConfig — and the two
|
||||
// must agree exactly or web connects with a password the role was never given. Compose passes an
|
||||
// unset variable through as "", so empty means throwaway here as it does in readSecret.
|
||||
export function resolvePluginDbSecret(env: Env): string {
|
||||
return env["PLUGIN_DB_SECRET"] || DEV_PLUGIN_DB_SECRET;
|
||||
// The one resolution both processes use — they must agree exactly, or web connects with a password
|
||||
// the role was never given. Compose passes an unset variable through as "", so empty means unset.
|
||||
// `enforce` says whether storage is actually in play: web once PLUGIN_DB_URL is configured,
|
||||
// bootstrap once a plugin declares storage. Enforced, the throwaway is refused — bootstrap is what
|
||||
// writes these passwords into Postgres, so it must refuse *before* creating a role with one.
|
||||
export function resolvePluginDbSecret(env: Env, enforce?: boolean): string {
|
||||
return readSecret(env, "PLUGIN_DB_SECRET", DEV_PLUGIN_DB_SECRET, enforce ?? readBool(env, "REQUIRE_SECURE_SECRETS", false));
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@@ -165,7 +167,7 @@ export function loadConfig(env: Env = process.env): Config {
|
||||
// credentials: the superuser DSN that provisions stays in bootstrap, so a plugin cannot read it
|
||||
// out of web's environment. Unset ⇒ storage is off and a plugin declaring it fails loud at boot,
|
||||
// which is also why the secret is only enforced once a URL is configured.
|
||||
pluginDbSecret: readSecret(env, "PLUGIN_DB_SECRET", DEV_PLUGIN_DB_SECRET, requireSecure && Boolean(env["PLUGIN_DB_URL"])),
|
||||
pluginDbSecret: resolvePluginDbSecret(env, requireSecure && Boolean(env["PLUGIN_DB_URL"])),
|
||||
pluginDbUrl: readOptionalUrl(env, "PLUGIN_DB_URL"),
|
||||
port: readPort(env),
|
||||
// Optional instant-revoke, off by default. When on, an admin deactivate/delete or permission
|
||||
|
||||
Reference in New Issue
Block a user