fix(install): probe CA bundle path across Linux distros, backfill on already-installed
This commit is contained in:
parent
de0e5af942
commit
ec459def07
@ -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");
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user