Observe TLS in Node.js itself
Use Node's TLS socket to observe a connection made by that runtime. Running the system's OpenSSL command can assess an endpoint, but cannot establish what your Node process negotiated.
Before you start
Use your application's supported Node release and executable, a hostname you are authorized to test, and the appropriate trust configuration. This example uses built-in modules only; it installs nothing. The API reference is the Node 24 TLS documentation. Check the release support schedule before selecting a production runtime.
Validation scope: the diagnostic uses documented APIs. Some older patch levels do not expose a hybrid group's name through getEphemeralKeyInfo(); the script preserves that as unknown. Example output is illustrative, not a guarantee for every Node release.
1. Identify the running runtime
node -p "JSON.stringify({node: process.version, openssl: process.versions.openssl, executable: process.execPath})"Run the inventory inside the same container or service environment as the application. Compare it with the application's startup executable. Also inventory its HTTP agents, explicit group settings, proxy, service mesh, and destination: a standalone direct connection cannot reproduce these automatically.
2. Make one verified native TLS connection
Save this as tls-evidence.mjs, review it, and run node tls-evidence.mjs example.com with your target hostname. It sends no HTTP request, uses the runtime's group policy, limits the handshake to TLS 1.3, and closes after recording metadata.
// Save as tls-evidence.mjs. Makes one TLS connection; sends no application data.
import tls from 'node:tls';
const hostname = process.argv[2];
if (!hostname || hostname.length > 253 || !/^[a-z0-9.-]+$/i.test(hostname)) {
console.error('Usage: node tls-evidence.mjs hostname.example');
process.exit(1);
}
const socket = tls.connect({
host: hostname,
port: 443,
servername: hostname,
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
rejectUnauthorized: true,
enableTrace: process.env.PQC_TRACE === '1',
});
const deadline = setTimeout(() => {
socket.destroy(new Error('TLS handshake exceeded 10 seconds'));
}, 10_000);
socket.once('close', () => clearTimeout(deadline));
socket.once('error', (error) => {
console.error(JSON.stringify({ result: 'inconclusive', error: error.message }));
process.exitCode = 1;
});
socket.once('secureConnect', () => {
if (!socket.authorized) {
socket.destroy(new Error('Certificate verification failed'));
return;
}
const keyInfo = socket.getEphemeralKeyInfo();
const group = typeof keyInfo?.name === 'string' ? keyInfo.name : null;
console.log(JSON.stringify({
observedAt: new Date().toISOString(),
target: hostname,
node: process.version,
openssl: process.versions.openssl,
protocol: socket.getProtocol(),
certificateVerified: socket.authorized,
cipher: socket.getCipher().standardName,
keyInfo,
reportedGroup: group,
evidence: group ? 'named key agreement reported' : 'group unknown',
}, null, 2));
socket.destroy();
});getProtocol() records the negotiated TLS version. getCipher() records a cipher suite, which is separate from the key exchange. Current Node 24 documentation describes TLSGroup results from getEphemeralKeyInfo() when a conventional ephemeral key object is unavailable. See the method's documented semantics.
3. Interpret named and missing evidence
Illustrative excerpt from a build that exposes the group:
{
"protocol": "TLSv1.3",
"certificateVerified": true,
"cipher": "TLS_AES_256_GCM_SHA384",
"keyInfo": { "type": "TLSGroup", "name": "X25519MLKEM768" },
"reportedGroup": "X25519MLKEM768",
"evidence": "named key agreement reported"
}A verified TLS 1.3 connection reporting X25519MLKEM768 provides hybrid key exchange evidence for this diagnostic. X25519 or prime256v1 indicates classical agreement. An unfamiliar name requires checking the standards reference; an empty object, null, or missing name remains unknown. Neither a large key size nor an AES cipher name fills the gap.
4. Investigate an unknown or different result
When the high-level API lacks a group name, you can run the same diagnostic with PQC_TRACE=1 in its environment. The documented TLS trace goes to stderr. Inspect the server's selected key share in the completed handshake; a client offer alone is not selection. Node warns that the trace format can change, so this is manual diagnostic evidence, not a stable format to scrape.
Alternatively, make a controlled request from your actual application to an endpoint you operate and correlate the server's negotiated-group telemetry. That report describes the connection received by that server; a TLS-terminating proxy can make it the proxy-to-server hop. Keep trace output private and avoid enabling key logging or tracing production user traffic.
- Trust error: correct the certificate chain or use the application's approved CA configuration. Keep
rejectUnauthorizedenabled. - Timeout/DNS failure: diagnose network reachability and the configured destination before assigning a crypto result.
- Diagnostic hybrid, application classical: compare the deployed executable, agent options, proxy, group restrictions, and connection reuse.
- Classical or unknown on an older runtime: verify that release's library support and documented diagnostics before deciding on an upgrade.
5. Upgrade the application and retest its path
Use a supported stable Node release and your existing application deployment process. Do not replace system OpenSSL and assume Node changed. Review explicit TLS options that might pin an old group list; preserve policy and compatibility rather than forcing one group fleet-wide.
Retain the previous supported application image/configuration, deploy to staging, and repeat both this diagnostic and a normal application request through the real path. Verify certificates, selected group evidence, latency, and ordinary functionality. Roll back the image/configuration if the change regresses behavior, and keep an owner/retest date in the readiness worksheet.
Documentation reviewed 25 September 2026. Refer to the TLS documentation for your precise deployed Node release; a documented capability is not an execution result.