| title | HTTPS & SSL Certificates |
|---|---|
| description | How Edge Scripts verify TLS certificates when fetching external URLs and Pull Zone origins, and how to fix common certificate errors. |
When your script calls fetch() on an https:// URL, or when the runtime
fetches your Pull Zone origin, it opens a fully verified TLS connection from the
bunny.net edge to that host. It performs the same checks a browser does: the
certificate must chain to a trusted public Certificate Authority, be valid for
the hostname you connected to, and not be expired.
Most of the time this needs no thought. Fetching an external HTTPS URL works out of the box:
import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.12.0";
BunnySDK.net.http.servePullZone(async (request: Request): Promise<Response> => {
const res = await fetch("https://api.example.com/data.json");
return new Response(await res.text());
});Wrap the call in try/catch so that a certificate problem on the origin
becomes a response you control instead of an unhandled error:
try {
const res = await fetch("https://api.example.com/data.json");
return new Response(await res.text());
} catch (err) {
console.error(err);
return new Response("Upstream unavailable", { status: 502 });
}If a fetch() fails on the certificate, it rejects with a TypeError whose message names the problem, for example invalid peer certificate: UnknownIssuer. The troubleshooting section is organized by that message.
Some origins are misconfigured in ways that every browser tolerates. The runtime handles these for you so that a fetch behaves the way it does in a browser. None of them weaken verification: the certificate chain is still checked against trusted roots on every connection, and a certificate that fails for a real reason (untrusted issuer, expired, wrong hostname) still fails.
- Missing intermediate certificates. Many servers send only their own leaf certificate and omit the intermediate that links it to the root CA. When a certificate fails only for this reason, the runtime completes the chain from a built-in list of publicly disclosed intermediates, sourced from the Common CA Database (CCADB), then verifies the completed chain against the trusted roots.
- bunny.net edge hostnames without their own certificate. A hostname pointed at
the bunny.net edge that has no SSL provisioned for that specific name (a
white-label or alias domain, often reached after a redirect) is answered with
one of bunny.net's own edge certificates, such as
*.b-cdn.net. When verification fails only on the hostname check and the certificate provably belongs to bunny.net, the connection is accepted. A mismatched third-party certificate is never accepted this way. - Older TLS 1.2 origins. Legacy handshakes that some strict clients reject (for example an ECDSA P-256 certificate whose handshake is signed with SHA-384) connect normally.
You do not need to configure anything for these.
These conveniences apply to fetch() and origin connections only. The lower-level node:tls module performs standard, stricter verification and rejects both cases.
The built-in intermediate list only covers certificates disclosed in the CCADB. If your origin uses a private or internal Certificate Authority, trust its certificate for that fetch by passing it to Deno.createHttpClient:
const client = Deno.createHttpClient({
caCerts: [
`-----BEGIN CERTIFICATE-----
...your CA certificate (PEM)...
-----END CERTIFICATE-----`,
],
});
const res = await fetch("https://internal.example.com/", { client });The certificate you supply is added to the trust store for that client only. Hostname and expiry checks still apply: this adds a trust anchor, it does not disable verification.
Some origins require the client to present its own certificate. Pass the PEM-encoded certificate and private key to Deno.createHttpClient and use that client for the fetch. Store both as environment secrets so they never appear in your source code:
import * as BunnySDK from "@bunny.net/edgescript-sdk@0.12.0";
import process from "node:process";
const client = Deno.createHttpClient({
cert: process.env.ClientCert, // certificate presented to the origin (PEM)
key: process.env.ClientKey, // matching private key (PEM)
// caCerts: [ ... ] // add if the origin uses a private CA
});
BunnySDK.net.http.servePullZone(async (request: Request): Promise<Response> => {
try {
const res = await fetch("https://mtls.example.com/status", { client });
return new Response(await res.text(), { status: res.status });
} catch (err) {
return new Response(`mTLS request failed: ${err.message}`, { status: 502 });
}
});The origin's own certificate is still verified as usual.
If you have trouble storing a multi-line PEM value in a secret, store it base64-encoded instead and decode it in the script with atob(process.env.ClientCert).
Your pullzone origin fetch has their own configuration in the Pullzone. Those do
not affects fetch() calls to other URLs.
```bash
openssl s_client -connect your-origin.example.com:443 \
-servername your-origin.example.com
```
`unable to verify the first certificate` means a missing intermediate: fix the origin to send its full chain, or use `caCerts` if the CA is private. `Verify return code: 0 (ok)` means the chain is fine and the problem lies elsewhere.