> ## Documentation Index
> Fetch the complete documentation index at: https://bunny.net/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTPS & SSL Certificates

> How Edge Scripts verify TLS certificates when fetching external URLs and Pull Zone origins, and how to fix common certificate errors.

## Overview

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:

```typescript theme={null}
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:

```typescript theme={null}
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 });
}
```

<Info>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](#troubleshooting) section is organized by that message.</Info>

## Handled Automatically

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)](https://www.ccadb.org/), 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.

<Info>These conveniences apply to `fetch()` and origin connections only. The lower-level [`node:tls`](/docs/scripting/node-tls) module performs standard, stricter verification and rejects both cases.</Info>

## Private Certificate Authorities

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`:

```typescript theme={null}
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.

## Client Certificates (mTLS)

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](/docs/scripting/secrets) so they never appear in your source code:

```typescript theme={null}
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.

<Info>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)`.</Info>

## Your Pull Zone Origin

Your pullzone origin fetch has their own configuration in the Pullzone. Those do
not affects `fetch()` calls to other URLs.

## Troubleshooting

<AccordionGroup>
  <Accordion title="UnknownIssuer">
    The origin's certificate does not chain to a public Certificate Authority. The usual cause is a private or internal CA, or an intermediate that is not disclosed in the CCADB. Trust the CA with [`Deno.createHttpClient({ caCerts })`](#private-certificate-authorities). Public origins that merely omit a disclosed intermediate already work automatically.
  </Accordion>

  <Accordion title="NotValidForName">
    The certificate is valid, but not for the hostname you connected to. bunny.net's own edge certificates are accepted automatically.
  </Accordion>

  <Accordion title="Expired">
    The origin's certificate has expired. Renew it on the origin. Expiry cannot be bypassed for external origins.
  </Accordion>

  <Accordion title="421 Misdirected Request">
    An HTTPS origin reached by IP address received no matching server name.
  </Accordion>

  <Accordion title="Works in a browser but fails in a script">
    Browsers hide certificate-chain and hostname problems that a strict client will not. Check the origin's certificate with `openssl`:

    ```bash theme={null}
    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.
  </Accordion>

  <Accordion title="Your own origin uses a self-signed certificate">
    Turn off **Verify Origin SSL** on the Pull Zone. This applies only to your configured origin.
  </Accordion>
</AccordionGroup>

## References

* [`fetch()` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch)
* [`Deno.createHttpClient`](https://docs.deno.com/api/deno/~/Deno.createHttpClient)
* [Common CA Database (CCADB)](https://www.ccadb.org/)
* [Node:TLS](/docs/scripting/node-tls)
