Use the certs/ fallback in every environment

TLS no longer depends on NODE_ENV: the TLS_* variables win, else certs/
is used whenever it exists (the container's /app/certs mount lands there
too), and TLS_DISABLED=1 is the only way to serve plain HTTP with a
certificate present. The plain-HTTP redirect listener now starts whenever
TLS is on, not only in development, and HTTP_REDIRECT_PORT replaces
DEV_HTTP_PORT. The installer writes TLS_DISABLED=1 for --no-https, and the
health check follows the same rule.
This commit is contained in:
2026-10-20 10:29:37 +01:00
parent 8c20c9a7ed
commit 459eebf73d
10 changed files with 222 additions and 63 deletions
+69
View File
@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import pickTlsFiles from "./pick-tls-files";
const fallback = {
fallback_cert_file: "certs/server.crt",
fallback_key_file: "certs/server.key",
fallback_ca_file: "certs/ca.crt",
};
const exists = (paths: string[]) => (path: string) => paths.includes(path);
const pick = (
options: Partial<Parameters<typeof pickTlsFiles>[0]> = {},
paths: string[] = ["certs/server.crt", "certs/server.key", "certs/ca.crt"],
) =>
pickTlsFiles({
...fallback,
file_exists: exists(paths),
...options,
});
describe("pickTlsFiles", () => {
test("prefers the TLS variables", () => {
const picked = pick({
cert_file: "/etc/cert.pem",
key_file: "/etc/key.pem",
ca_file: "/etc/ca.pem",
});
expect(picked).toEqual({
cert_file: "/etc/cert.pem",
key_file: "/etc/key.pem",
ca_file: "/etc/ca.pem",
source: "explicit",
});
});
test("uses certs/ when the variables are unset, in any environment", () => {
const picked = pick({ cert_file: "", key_file: "" });
expect(picked).toEqual({
cert_file: "certs/server.crt",
key_file: "certs/server.key",
ca_file: "certs/ca.crt",
source: "fallback",
});
});
test("serves HTTP when the certificate is explicitly disabled", () => {
expect(
pick({
cert_file: "/etc/cert.pem",
key_file: "/etc/key.pem",
disabled: "1",
}),
).toEqual({ source: "disabled" });
expect(pick({ disabled: "true" })).toEqual({ source: "disabled" });
expect(pick({ disabled: "no" }).source).toBe("fallback");
});
test("serves HTTP when there is nothing to serve with", () => {
expect(pick({}, []).source).toBe("none");
expect(pick({}, ["certs/server.crt"]).source).toBe("none");
});
test("keeps half a configuration for the resolver to reject", () => {
expect(pick({ cert_file: "/etc/cert.pem" }, []).source).toBe("explicit");
});
});
+83
View File
@@ -0,0 +1,83 @@
type Params = {
/**
* `TLS_CERT_FILE`, when the environment names one.
*/
cert_file?: string;
/**
* `TLS_KEY_FILE`, when the environment names one.
*/
key_file?: string;
/**
* `TLS_CA_FILE`, served for devices to trust.
*/
ca_file?: string;
/**
* `TLS_DISABLED`, the explicit opt-out.
*/
disabled?: string;
/**
* Where a certificate is looked for when the environment names none:
* `certs/` at the project root, which is also where the container's
* `/app/certs` mount lands.
*/
fallback_cert_file: string;
fallback_key_file: string;
fallback_ca_file: string;
file_exists: (path: string) => boolean;
};
export type TlsFiles = {
cert_file?: string;
key_file?: string;
ca_file?: string;
/**
* Where the choice came from, for the startup log: the environment,
* the `certs/` fallback, an explicit disable, or nothing at all.
*/
source: "explicit" | "fallback" | "disabled" | "none";
};
const DisabledValues = new Set(["1", "true", "yes", "on"]);
/**
* Which certificate files to serve with, in every environment: the
* `TLS_*` variables when they are set, else `certs/` when it exists, and
* plain HTTP only when `TLS_DISABLED` says so or there is nothing to serve
* with.
*
* Nothing here is tied to `NODE_ENV`: a production process that has a
* certificate serves HTTPS, and the only way to turn it off is to say so.
*/
export default function pickTlsFiles({
cert_file,
key_file,
ca_file,
disabled,
fallback_cert_file,
fallback_key_file,
fallback_ca_file,
file_exists,
}: Params): TlsFiles {
if (disabled && DisabledValues.has(disabled.trim().toLowerCase())) {
return { source: "disabled" };
}
const cert = cert_file?.trim();
const key = key_file?.trim();
if (cert || key) {
return { cert_file: cert, key_file: key, ca_file: ca_file?.trim(), source: "explicit" };
}
if (file_exists(fallback_cert_file) && file_exists(fallback_key_file)) {
return {
cert_file: fallback_cert_file,
key_file: fallback_key_file,
ca_file: ca_file?.trim() ||
(file_exists(fallback_ca_file) ? fallback_ca_file : undefined),
source: "fallback",
};
}
return { source: "none" };
}