DEV Community

Merlonix
Merlonix

Posted on Originally published at merlonix.com

GitHub Pages Silently Fails to Provision SSL — and the “Enforce HTTPS” Checkbox Stays Checked Anyway

You point a custom domain at GitHub Pages, tick Enforce HTTPS, wait, and come back to a checkbox that is still ticked and an informational banner that has cleared. So it worked, right?

Not necessarily. That checkbox reflects your intent, not the outcome. GitHub Pages provisions a Let's Encrypt certificate for a custom domain behind the scenes, and when provisioning fails — which it does silently, for a small set of very specific DNS reasons — the settings UI gives you almost nothing to distinguish "issued" from "failed." The banner clears in both cases. The checkbox stays checked in both cases. The only durable signal that something is wrong is a visitor hitting "Your connection is not private" weeks later.

Here is the actual failure surface, and how to watch for it from the outside.

Custom-domain SSL on GitHub Pages, precisely

The default <username>.github.io / <orgname>.github.io URLs are served under GitHub's own wildcard certificate — nothing to provision, nothing to monitor. The moment you attach a custom domain, GitHub has to obtain a certificate for your name, and it does it like this:

  1. It verifies the custom domain's DNS points at GitHub Pages infrastructure.
  2. It runs a Let's Encrypt HTTP-01 challenge against the domain.
  3. If the challenge validates, Let's Encrypt issues a 90-day certificate.
  4. Pages starts serving HTTPS.

Step 2 is the whole game. HTTP-01 means an HTTP request to your domain has to actually reach GitHub's servers. So the DNS has to be exactly right:

  • Apex domain (example.com): four A records, pointing at GitHub's range — 185.199.108.153, 185.199.109.153, 185.199.110.153, 185.199.111.153.
  • Subdomain (www.example.com): a CNAME to <username>.github.io.

GitHub validates all four A records for an apex domain. Miss one and provisioning fails.

The four ways this quietly breaks

Almost every real GitHub Pages SSL failure I have seen traces to DNS, and specifically to one of these:

Only some of the four A records exist. This happens most after a registrar or DNS-provider migration. Someone rebuilds the zone, adds the two or three GitHub IPs they remember, and moves on. Three of four validate fine as far as dig is concerned — but GitHub wants all four, so the cert never issues.

Stale A records from old docs. Search results still surface GitHub's previous IP ranges. Records get added that point at addresses no longer in the validation set. Looks configured; isn't.

A records on the wrong name. Apex A records land on www (or the www CNAME lands on the apex), so one half of an apex-plus-www setup is missing.

Cloudflare's orange cloud is on. This is the sneaky one. If the domain sits behind Cloudflare with the proxy enabled, DNS resolution returns Cloudflare's IPs, not GitHub's. GitHub's HTTP-01 challenge can't reach GitHub through the proxy, and provisioning fails. GitHub Pages needs DNS-only (grey cloud) for the challenge to succeed. And because someone can flip that proxy toggle months after the site is live, a working cert can start failing to renew long after launch.

The renewal gap is where it actually hurts

GitHub certs are Let's Encrypt, 90 days, auto-renewed at roughly 60 days. Renewal uses the same HTTP-01 challenge with the same DNS requirements. So a domain that had all four A records at launch can be missing one at renewal time because someone touched DNS in the intervening weeks — a migration, a Cloudflare change, a "cleanup."

Now do the arithmetic. Renewal is attempted around day 60. The cert is valid through day 90. If the renewal fails and nobody notices, you have ~30 days before the certificate expires and browsers start hard-blocking the site — 30 days in which GitHub's UI is telling you nothing is wrong.

Watching it from outside GitHub

You can't read GitHub's internal provisioning state over an API. What you can do is watch the two things that actually determine success — the DNS records and the served certificate — from outside, and alert on the change at the moment it happens instead of 30 days later:

  • Watch all four apex A records. A DNS-change monitor that knows the four GitHub IPs is the earliest possible signal: the instant one is removed or a proxy flips resolution to Cloudflare's IPs, that's your page — not a browser error a month on.
  • Watch the www CNAME as its own check. The www → <username>.github.io delegation is a separate requirement from the apex A records; a CNAME retarget breaks SSL independently.
  • Watch cert expiry with a ~30-day threshold. That threshold lines up with the 60-day renewal window: if GitHub's renewal attempt fails, the alert fires before expiry, while you still have runway to fix the DNS.
  • Track GitHub Pages as a vendor. Platform-wide provisioning incidents happen; separating "GitHub is having a bad day" from "this one client's DNS is wrong" saves an hour of misdirected debugging.

The honest limit

External monitoring detects the symptom surface — the DNS records changed, the served certificate is close to expiry or already expired, the chain the server presents is broken. It does not read GitHub's provisioning queue or tell you why GitHub declined to issue; it tells you the DNS and cert facts that cause GitHub to decline, which is what you can actually act on. If you want the internal reason, the Pages settings page and the repo's Pages API are still where you look — this just makes sure you look before the outage instead of after.


I write about the quiet, no-error-thrown failure modes of web infrastructure at merlonix.com/blog, where this post first appeared. Merlonix monitors custom-domain SSL expiry and DNS/CNAME change together, so a missing GitHub Pages A record pages you the day it's removed rather than the day the certificate finally expires.

Your Certificate Auto-Renewal Will Fail Silently One Day
A Dangling CNAME Is a Subdomain Takeover Waiting to Happen
Domain Expiry and SSL Expiry Are Two Different Clocks

Top comments (0)