Skip to content

Commit d76097d

Browse files
orgadsclaude
andcommitted
src: hint at OPENSSL_CONF when the OpenSSL config fails to load
OpenSSL is initialized with CONF_MFLAGS_IGNORE_MISSING_FILE, so a missing configuration file does not prevent Node.js from starting. That flag only covers ENOENT and ENOTDIR though, so a file that exists but cannot be opened is still fatal. Running in a container where /etc/ssl is not accessible to the current user aborts startup with an error that gives no way out: OpenSSL configuration error: ...:BIO_new_file:Permission denied:...fopen(/etc/ssl/openssl.cnf, rb) There is a way out: OpenSSL skips config loading entirely when OPENSSL_CONF is set to an empty value. Say so in the error message, along with the options that select a different file, and document the empty value. Refs: #62230 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Orgad Shaneh <orgad.shaneh@audiocodes.com>
1 parent 3d80990 commit d76097d

3 files changed

Lines changed: 56 additions & 2 deletions

File tree

doc/api/cli.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4262,6 +4262,11 @@ added: v6.11.0
42624262
Load an OpenSSL configuration file on startup. The file can be used as part of
42634263
a [FIPS mode][] configuration.
42644264

4265+
If the variable is set to an empty value, no configuration file is loaded. This
4266+
is useful when the default configuration file cannot be read, for example when
4267+
it is not accessible to the user Node.js runs as, which is otherwise a fatal
4268+
error on startup.
4269+
42654270
If the [`--openssl-config`][] command-line option is used, the environment
42664271
variable is ignored.
42674272

src/node.cc

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,8 +1235,16 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12351235
result->exit_code_ =
12361236
static_cast<ExitCode>(ERR_GET_REASON(ERR_peek_error()));
12371237
result->early_return_ = true;
1238-
result->errors_.emplace_back("OpenSSL configuration error:\n" +
1239-
GetOpenSSLErrorString());
1238+
// CONF_MFLAGS_IGNORE_MISSING_FILE above only covers a missing file, so a
1239+
// configuration file that exists but cannot be read, e.g. when /etc/ssl
1240+
// is not accessible to the current user, ends up here. Point at the ways
1241+
// to select another file or skip loading one.
1242+
// Refs: https://github.com/nodejs/node/issues/62230
1243+
result->errors_.emplace_back(
1244+
"OpenSSL configuration error:\n" + GetOpenSSLErrorString() +
1245+
"Use OPENSSL_CONF or --openssl-config to load a different "
1246+
"configuration file, or set OPENSSL_CONF to an empty value to start "
1247+
"without one.");
12401248
return result;
12411249
}
12421250
#else // OPENSSL_VERSION_MAJOR < 3
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
// A default OpenSSL configuration file that cannot be read is fatal, but the
4+
// error says how to get past it, and an empty OPENSSL_CONF does get past it.
5+
// Refs: https://github.com/nodejs/node/issues/62230
6+
7+
const common = require('../common');
8+
const assert = require('node:assert');
9+
const { spawnSync } = require('node:child_process');
10+
11+
if (!common.hasCrypto)
12+
common.skip('missing crypto');
13+
if (!common.isLinux)
14+
common.skip('linux only');
15+
if (process.config.variables.node_shared_openssl)
16+
common.skip('shared openssl may read a different configuration file');
17+
18+
// Replace /etc/ssl with an empty tmpfs in a private mount namespace, where
19+
// openssl.cnf is a symlink loop: opening it then fails with ELOOP instead of
20+
// ENOENT, which OpenSSL ignores on its own. The namespace goes away with the
21+
// process, so the host /etc/ssl is left alone.
22+
const setup = 'mount -t tmpfs tmpfs /etc/ssl && ln -s openssl.cnf /etc/ssl/openssl.cnf';
23+
24+
if (spawnSync('unshare', ['-Urm', 'sh', '-c', setup]).status !== 0)
25+
common.skip('cannot set up an unprivileged user and mount namespace');
26+
27+
function run(env) {
28+
return spawnSync(
29+
'unshare',
30+
['-Urm', 'sh', '-c', `${setup} && exec "$0" -p 42`, process.execPath],
31+
{ encoding: 'utf8', env: { ...process.env, ...env } });
32+
}
33+
34+
const failed = run({});
35+
assert.notStrictEqual(failed.status, 0);
36+
assert.match(failed.stderr, /OpenSSL configuration error/);
37+
assert.match(failed.stderr, /OPENSSL_CONF/);
38+
39+
const skipped = run({ OPENSSL_CONF: '' });
40+
assert.strictEqual(skipped.status, 0);
41+
assert.strictEqual(skipped.stdout.trim(), '42');

0 commit comments

Comments
 (0)