cloudflare / cloudflare/workerd
🐛 Bug Report — Runtime APIs: node:tls key/cert are documented but ignored
- 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
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