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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ HUB_STATIC_DIR=../web/dist
# Dev/test-only opt-out of the CREDENTIAL_ENCRYPTION_KEY requirement above:
# set to boot without a key, storing those secrets unencrypted with a boot
# warning. Uncommented here so a fresh checkout's `bun run dev` keeps
# working out of the box — never set this for a real deployment.
# working out of the box. Safe to inherit as-is: the hub refuses to boot
# with this set unless BASE_URL is also a loopback address (localhost /
# 127.0.0.1 / ::1), so it can never take effect against a real deployment
# even if this file is copied verbatim.
ALLOW_PLAINTEXT_SECRETS=1

# Opt-out of @workbench/access-policy's email-verification requirement.
Expand Down
34 changes: 33 additions & 1 deletion apps/hub/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ import type { SupportedCredentialProvider } from "@workbench/hub-client";

const HTTP_URL = /^https?:\/\/.+$/;

const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);

/**
* Whether `baseUrl` names a loopback address — the one fact that
* distinguishes a developer's own machine from a real deployment
* without trusting an operator to have overridden anything. Used to
* refuse `ALLOW_PLAINTEXT_SECRETS` (see `readHubConfig`) rather than
* relying on an operator to have removed it from an inherited `.env`.
*/
function isLoopbackBaseUrl(baseUrl: string): boolean {
return LOOPBACK_HOSTNAMES.has(new URL(baseUrl).hostname);
}

const HubEnv = type({
DATABASE_URL: type(/^postgres(ql)?:\/\/.+$/).describe(
"a Postgres connection URL, e.g. postgres://workbench:workbench@localhost:5432/workbench",
Expand Down Expand Up @@ -167,7 +180,7 @@ const HubEnv = type({
"a 64-character hex-encoded 32-byte AES-256 key (openssl rand -hex 32) encrypting secrets at rest through Interchange's CredentialCipher seam — webhook-trigger signing secrets and onboarding's OAuth PKCE connect state; boot fails without it unless ALLOW_PLAINTEXT_SECRETS opts into dev/test's unencrypted fallback",
),
"ALLOW_PLAINTEXT_SECRETS?": type("'1' | 'true'").describe(
"dev/test-only opt-in to boot without CREDENTIAL_ENCRYPTION_KEY, storing secrets at rest unencrypted with a boot warning; never set this for a real deployment",
"dev/test-only opt-in to boot without CREDENTIAL_ENCRYPTION_KEY, storing secrets at rest unencrypted with a boot warning; refused unless BASE_URL is a loopback address, so a real deployment can never inherit it by accident",
),
"ALLOW_UNVERIFIED_EMAILS?": type("'1' | 'true'").describe(
"dev/test-only opt-in to let @workbench/access-policy trust an email that better-auth has not verified — self-signup domain checks and pending-invite redemption normally require emailVerified; never set this for a real deployment",
Expand Down Expand Up @@ -667,5 +680,24 @@ export function readHubConfig(
if (sidecarProvisioners.defaultProvisionerId !== undefined)
hubConfig.defaultSidecarProvisionerId =
sidecarProvisioners.defaultProvisionerId;

if (
hubConfig.allowPlaintextSecrets &&
!isLoopbackBaseUrl(hubConfig.baseUrl)
) {
throw new Error(
[
`ALLOW_PLAINTEXT_SECRETS is set, but BASE_URL (${hubConfig.baseUrl}) is not a loopback address.`,
"Storing secrets unencrypted at rest is a dev/test-only fallback for",
"http://localhost — refused here because this looks like a real",
"deployment. Generate a real key instead and add it to .env:",
"",
" openssl rand -hex 32",
"",
"then set CREDENTIAL_ENCRYPTION_KEY to it and remove ALLOW_PLAINTEXT_SECRETS.",
].join("\n"),
);
}

return hubConfig;
}
36 changes: 36 additions & 0 deletions apps/hub/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,42 @@ describe("readHubConfig", () => {
expect(message).toContain("ALLOW_PLAINTEXT_SECRETS");
});

test("ALLOW_PLAINTEXT_SECRETS boots fine against a loopback BASE_URL", () => {
expect(
readHubConfig({
...validEnv,
BASE_URL: "http://localhost:3000",
ALLOW_PLAINTEXT_SECRETS: "1",
}).allowPlaintextSecrets,
).toBe(true);
expect(
readHubConfig({
...validEnv,
BASE_URL: "http://127.0.0.1:3000",
ALLOW_PLAINTEXT_SECRETS: "1",
}).allowPlaintextSecrets,
).toBe(true);
});

test("ALLOW_PLAINTEXT_SECRETS refuses to boot against a non-loopback BASE_URL", () => {
const message = readExpectingError({
...validEnv,
BASE_URL: "https://workbench.example.com",
ALLOW_PLAINTEXT_SECRETS: "1",
});
expect(message).toContain("ALLOW_PLAINTEXT_SECRETS");
expect(message).toContain("BASE_URL");
});

test("a non-loopback BASE_URL boots fine without ALLOW_PLAINTEXT_SECRETS", () => {
const config = readHubConfig({
...validEnv,
BASE_URL: "https://workbench.example.com",
CREDENTIAL_ENCRYPTION_KEY: "a".repeat(64),
});
expect(config.allowPlaintextSecrets).toBe(false);
});

test("accepts postgresql:// and https:// URL forms", () => {
const config = readHubConfig({
...validEnv,
Expand Down
Loading