cloudflare / cloudflare/workerd

🐛 Bug Report — Runtime APIs: node:tls key/cert are documented but ignored

Open
#7,201 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

### Summary

The Cloudflare Workers documentation for `node:tls` shows `connect(url, { key: env.KEY, cert: env.CERT })` as a way to create a TLS connection with client key material:

https://developers.cloudflare.com/workers/runtime-apis/nodejs/tls/

However, with workerd `1.20260815.1` and on the production edge, both `key` / `cert` passed directly to `tls.connect()` and through `createSecureContext({ key, cert })` are ignored. Even deliberately malformed PEM is accepted by the Workers runtime instead of failing during context creation.

This prevents a multi-tenant Worker from loading a tenant-specific client certificate from R2 or another secret store and presenting it for one outbound mTLS request.

### Documentation inconsistency

The `node:tls` page documents `key`, `cert`, and `createSecureContext()`:

https://developers.cloudflare.com/workers/runtime-apis/nodejs/tls/

But the `node:https` page says that `key`, `cert`, and `pfx` are unsupported and recommends a static mTLS binding:

https://developers.cloudflare.com/workers/runtime-apis/nodejs/https/

### Minimal behavioral check

In native Node.js, this fails immediately as expected:

```js
import { createSecureContext } from "node:tls";

createSecureContext({
key: "-----BEGIN PRIVATE KEY-----\nINVALID\n-----END PRIVATE KEY-----",
cert: "-----BEGIN CERTIFICATE-----\nINVALID\n-----END CERTIFICATE-----",
});
```

In the Workers runtime, the same invalid material does not fail and produces an empty `SecureContext`.

I also ran a sequential A/B test against a public mTLS endpoint using the same raw HTTP/1.1 transport and parser.

Native Node.js:

- valid client certificate passed to `connect()`: HTTP 404 with the expected application error;
- the same certificate through `createSecureContext()`: same HTTP 404;
- no client certificate: HTTP 403;
- malformed PEM: ASN.1 error before the request was written.

Remote Workers edge:

- valid `key` / `cert`;
- valid material through `createSecureContext()`;
- no client certificate;
- malformed PEM;

All four Workers cases were indistinguishable: the request was written, the peer closed the connection, and zero response bytes were received. A public non-mTLS control using the same `node:tls` transport and HTTP parser returned HTTP 301, so the generic socket/parser path was working.

No private key, certificate, tenant identifier, or production payload is included in this report.

### Current implementation

The source for the installed workerd version appears to confirm the behavior:

- `createSecureContext()` does not create a customizable TLS context and sets `context = undefined`:
https://github.com/cloudflare/workerd/blob/v1.20260815.1/src/node/internal/internal_tls_common.ts#L38-L82
- `connect()` does not forward `options.key` or `options.cert`:
https://github.com/cloudflare/workerd/blob/v1.20260815.1/src/node/internal/internal_tls_wrap.ts#L638-L720
- the effective TLS upgrade forwards only `expectedServerHostname` to `startTls()`:
https://github.com/cloudflare/workerd/blob/v1.20260815.1/src/node/internal/internal_tls_wrap.ts#L438-L509
- `setKeyCert()` explicitly reports that changing key/certificate material is unsupported:
https://github.com/cloudflare/workerd/blob/v1.20260815.1/src/node/internal/internal_tls_wrap.ts#L552-L558

### Use case

We operate a multi-tenant invoicing SaaS. Each tenant uploads its own PKCS#12/PFX A1 client certificate, stored encrypted in R2. The application can load and convert the selected PFX to PEM in memory, but it cannot present that PEM in the outbound mTLS handshake.

A static mTLS binding per tenant would require control-plane provisioning and Worker reconfiguration for every onboarding, renewal, and revocation. We need either:

1. the documented `tls.connect({ key, cert })` behavior to be implemented; or
2. another supported per-request API that accepts in-memory client certificate material.

If dynamic client certificates are intentionally unsupported, the `node:tls` documentation should state this explicitly and avoid the current `key` / `cert` example.

### Environment

- Wrangler: `4.124.0`
- Bundled workerd: `1.20260815.1`
- Compatibility date: `2026-08-22`
- Compatibility flag: `nodejs_compat`
- Native control: Node.js `v24.13.0`
- Reproduced with `wrangler dev --remote` on the Cloudflare edge

Contributor guide

Open the contributing guide

Research direction

Read src/node/internal/internal_tls_common.ts and src/node/internal/internal_tls_wrap.ts at v1.20260815.1, then compare their behavior with the node:tls and node:https documentation. Run the malformed-PEM and mTLS checks described in the report. Done means dynamic key/cert handling works as documented, or the unsupported behavior is made explicit and the conflicting documentation is corrected.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
api, backend, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.