fix(install): probe CA bundle path across Linux distros, backfill on already-installed

This commit is contained in:
GodsBoy 2026-03-17 17:24:20 +02:00
parent de0e5af942
commit ec459def07
3 changed files with 50 additions and 7 deletions

View File

@ -8,7 +8,7 @@ import {
import { resolveGatewayInstallToken } from "../../commands/gateway-install-token.js";
import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js";
import { resolveGatewayStateDir } from "../../daemon/paths.js";
import { isNvmNode } from "../../daemon/service-env.js";
import { isNvmNode, resolveLinuxSystemCaBundle } from "../../daemon/service-env.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { isNonFatalSystemdInstallProbeError } from "../../daemon/systemd.js";
import { defaultRuntime } from "../../runtime.js";
@ -58,6 +58,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
}
if (loaded) {
if (!opts.force) {
// Backfill NODE_EXTRA_CA_CERTS even when service is already installed,
// so users upgrading to this fix don't need --force to get the .env entry.
ensureNvmCaCertsInDotEnv({ env: process.env, json, warnings });
emit({
ok: true,
result: "already-installed",
@ -148,13 +151,17 @@ function ensureNvmCaCertsInDotEnv(params: {
}
try {
const caBundle = resolveLinuxSystemCaBundle();
if (!caBundle) {
return;
}
const stateDir = resolveGatewayStateDir(params.env);
const envFile = path.join(stateDir, ".env");
const existing = fs.existsSync(envFile) ? fs.readFileSync(envFile, "utf8") : "";
if (existing.includes("NODE_EXTRA_CA_CERTS")) {
return;
}
const line = "NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt\n";
const line = `NODE_EXTRA_CA_CERTS=${caBundle}\n`;
const content = existing.endsWith("\n") || !existing ? existing + line : `${existing}\n${line}`;
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(envFile, content, "utf8");

View File

@ -9,6 +9,7 @@ import {
getMinimalServicePathParts,
getMinimalServicePathPartsFromEnv,
isNvmNode,
resolveLinuxSystemCaBundle,
} from "./service-env.js";
describe("getMinimalServicePathParts - Linux user directories", () => {
@ -523,6 +524,17 @@ describe("isNvmNode", () => {
});
});
describe("resolveLinuxSystemCaBundle", () => {
it("returns a known CA bundle path when one exists", () => {
const result = resolveLinuxSystemCaBundle();
if (process.platform === "linux") {
// On a real Linux host, at least one standard CA bundle should exist.
expect(result).toMatch(/\.(crt|pem)$/);
}
// On non-Linux CI or minimal containers, result may be undefined.
});
});
describe("shared Node TLS env — Linux nvm detection", () => {
const builders = [
{
@ -537,11 +549,14 @@ describe("shared Node TLS env — Linux nvm detection", () => {
},
] as const;
// The expected CA path depends on what the host actually has on disk.
const expectedCaBundle = resolveLinuxSystemCaBundle();
it.each(builders)(
"$name defaults NODE_EXTRA_CA_CERTS on Linux when NVM_DIR is set",
({ build }) => {
const env = build({ HOME: "/home/user", NVM_DIR: "/home/user/.nvm" }, "linux");
expect(env.NODE_EXTRA_CA_CERTS).toBe("/etc/ssl/certs/ca-certificates.crt");
expect(env.NODE_EXTRA_CA_CERTS).toBe(expectedCaBundle);
},
);

View File

@ -1,3 +1,4 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { VERSION } from "../version.js";
@ -15,8 +16,28 @@ import {
resolveNodeWindowsTaskName,
} from "./constants.js";
/** Standard system CA bundle path on Debian/Ubuntu/Alpine Linux. */
const LINUX_SYSTEM_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
/** Known system CA bundle paths across common Linux distros. */
const LINUX_CA_BUNDLE_PATHS = [
"/etc/ssl/certs/ca-certificates.crt", // Debian, Ubuntu, Alpine
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL, Fedora, CentOS
"/etc/ssl/ca-bundle.pem", // openSUSE
] as const;
/**
* Find the system CA bundle on this Linux host.
* Returns the first existing path, or `undefined` if none is found.
*/
export function resolveLinuxSystemCaBundle(): string | undefined {
for (const candidate of LINUX_CA_BUNDLE_PATHS) {
try {
fs.accessSync(candidate, fs.constants.R_OK);
return candidate;
} catch {
continue;
}
}
return undefined;
}
/**
* Detect if Node.js was installed via nvm.
@ -348,13 +369,13 @@ function resolveSharedServiceEnvironmentFields(
// cannot locate the system CA bundle. Default to /etc/ssl/cert.pem so TLS verification
// works correctly when running as a LaunchAgent without extra user configuration.
// On Linux, nvm-installed Node uses a bundled CA store that is missing modern root CAs.
// Default to the system CA bundle when nvm is detected so TLS works out of the box.
// Default to the system CA bundle when nvm is detected and the bundle exists on disk.
const nodeCaCerts =
env.NODE_EXTRA_CA_CERTS ??
(platform === "darwin"
? "/etc/ssl/cert.pem"
: platform === "linux" && isNvmNode(env)
? LINUX_SYSTEM_CA_BUNDLE
? resolveLinuxSystemCaBundle()
: undefined);
const nodeUseSystemCa = env.NODE_USE_SYSTEM_CA ?? (platform === "darwin" ? "1" : undefined);
return {