Pointing a new domain at GitHub Pages works on the first try often enough that the greyed-out checkbox comes as a surprise. You add the custom domain, GitHub accepts it, the site loads over plain HTTP, and the Enforce HTTPS option sits there disabled with a note that the certificate has not been provisioned yet. In most cases nothing is wrong. GitHub is still in the middle of issuing the certificate, and the toggle unlocks on its own once that finishes.
This post covers the whole lifecycle: what happens after you save a custom domain, how to watch it progress from the command line, why you should fix mixed content before you enforce HTTPS, and what to check when provisioning genuinely fails.
The provisioning sequence
When you save a custom domain for a GitHub Pages site, GitHub runs a fixed pipeline in the background. The UI gives you one checkbox as feedback, but three distinct things happen.
1. DNS check
GitHub resolves the domain and confirms it points where it should:
- For an apex domain (
example.com),Arecords must point to the Pages IPs, or anALIAS/ANAMErecord must be configured. - For a subdomain (
www.example.com,blog.example.com), aCNAMErecord must point to<user>.github.io(or<org>.github.io).
If DNS does not resolve correctly at this stage, the certificate request never starts. The UI will keep saying the certificate is pending, and it will keep saying that indefinitely. This is the failure mode worth knowing about, because it looks identical to normal waiting.
2. Certificate issuance
Once DNS checks out, GitHub requests a certificate for the domain (and the www variant where applicable) from its CA. This is the part that takes time. Typical latency is 10 to 30 minutes, but it depends on DNS TTLs and CA queueing, so an hour is not unheard of after a domain switch. During this window the site is reachable over HTTP and, in many cases, already over HTTPS with a certificate that browsers may or may not accept yet.
3. Enforce toggle unlocks
Only after the certificate is live does GitHub enable the Enforce HTTPS checkbox. Ticking it makes Pages redirect all HTTP requests to HTTPS with a 301. Until then, the box stays disabled, and there is no override, no query parameter, no API call that forces it early.
The practical takeaway: after a domain switch, expect a wait. Do not start changing DNS records while the certificate is pending, because you will reset the process.
Watching progress from the terminal
The GitHub settings page is a poor progress indicator. It shows a static message until it flips. curl tells you more.
Check what is actually being served
curl -I http://example.com
curl -I https://example.com
Run both. Before the certificate is live, the HTTPS request typically fails at the TLS handshake, which curl reports as:
curl: (60) SSL certificate problem: unable to get local issuer certificate
or a certificate mismatch error. The HTTP request, meanwhile, returns 200 with no redirect. That combination means: DNS is fine, certificate is not ready.
Once provisioning completes, https:// returns 200 and http:// still returns 200 until you tick the box. After enforcing, http:// returns:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Inspect the served certificate
curl -I only tells you whether TLS works. To see which certificate is being served and for which names, use openssl:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
A healthy result looks roughly like:
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R3
notBefore=...
notAfter=...
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
Two things to verify here. First, the SAN list must include every hostname you actually use. If you serve www.example.com but the certificate only covers the apex, browsers will warn on the www host even though the apex is fine. Second, check notBefore and notAfter to rule out a stale or expired certificate being served from an edge cache.
If openssl returns a certificate whose subject is *.github.io or belongs to a different domain, the request is landing on the wrong vhost. That usually means DNS is still pointing at an old host, or a proxy (Cloudflare, for example) is terminating TLS in front of Pages. Cloudflare's "Full" or "Flexible" SSL modes in front of GitHub Pages are a common source of confusing certificate output.
Scripting the wait
If you switch domains often, a small polling script saves a lot of tab-refreshing. This one checks the HTTPS handshake and the redirect status until both are correct, or gives up after a timeout:
import ssl
import socket
import time
import urllib.request
import urllib.error
DOMAIN = "example.com"
TIMEOUT_MINUTES = 45
INTERVAL_SECONDS = 120
def tls_ok(host: str, port: int = 443) -> bool:
ctx = ssl.create_default_context()
try:
with socket.create_connection((host, port), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls:
cert = tls.getpeercert()
sans = [v for k, v in cert.get("subjectAltName", ()) if k == "DNS"]
return host in sans or f"*.{host.split('.', 1)[1]}" in sans
except (ssl.SSLError, socket.error, OSError):
return False
def redirects_to_https(url: str) -> bool:
req = urllib.request.Request(url, method="HEAD")
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.geturl().startswith("https://")
except urllib.error.HTTPError as e:
return e.code in (301, 308) and e.headers.get("Location", "").startswith("https://")
except urllib.error.URLError:
return False
deadline = time.time() + TIMEOUT_MINUTES * 60
while time.time() < deadline:
cert = tls_ok(DOMAIN)
enforced = redirects_to_https(f"http://{DOMAIN}")
print(f"cert={cert} enforced={enforced}")
if cert and enforced:
print("Done.")
break
time.sleep(INTERVAL_SECONDS)
else:
print("Timed out. Check DNS and the Pages settings.")
The tls_ok check is stricter than a plain connection test: it validates that the hostname appears in the SAN list, which is the thing that actually matters for browsers. The redirects_to_https check confirms enforcement is active. Run it from a machine that is not behind the same DNS resolver you just changed, or you may be testing a cached view.
Fix mixed content before you enforce
This is the step people skip, and it causes the most visible breakage. Enforcing HTTPS makes the page itself load over TLS, but it does not rewrite the URLs inside your HTML, CSS, or JavaScript. Any resource still referenced as http:// becomes mixed content. Modern browsers block active mixed content (scripts, stylesheets, iframes) outright and either block or auto-upgrade passive mixed content (images), depending on the browser and the resource type.
The result: the page loads, the layout looks wrong, and the console is full of errors. Users blame the certificate; the real cause is a handful of hard-coded http:// URLs.
Find them before you flip the switch
Crawl your own site over HTTP and grep the rendered HTML for http:// references. A minimal crawler:
import re
from urllib.parse import urljoin, urlparse
from collections import deque
import requests
START = "http://example.com/"
SAME_HOST = urlparse(START).netloc
# Ignore URLs that are fine as http:// (rare) or not resources at all.
IGNORE = re.compile(r"^(mailto:|tel:|javascript:|#)")
seen = set()
queue = deque([START])
findings = []
session = requests.Session()
session.headers["User-Agent"] = "mixed-content-audit/1.0"
while queue:
url = queue.popleft()
if url in seen or urlparse(url).netloc != SAME_HOST:
continue
seen.add(url)
try:
resp = session.get(url, timeout=10)
except requests.RequestException:
continue
if "text/html" not in resp.headers.get("Content-Type", ""):
continue
for match in re.finditer(r'(?:src|href|action)\s*=\s*["\'](http://[^"\']+)', resp.text):
findings.append((url, match.group(1)))
for match in re.finditer(r'<a\s[^>]*href\s*=\s*["\']([^"\'#]+)', resp.text):
link = urljoin(url, match.group(1))
if not IGNORE.match(link):
queue.append(link)
for page, asset in findings:
print(f"{page} -> {asset}")
print(f"\n{len(findings)} http:// references across {len(seen)} pages")
Anything it prints is a candidate for breakage. The fix is mechanical: rewrite to https://, or better, to protocol-relative or root-relative URLs (//cdn.example.com/lib.js or /assets/lib.js) so the scheme follows the page.
Common offenders
- Hard-coded CDN links in templates, often copied from documentation years ago.
- Markdown content with embedded images using
http://URLs. - Analytics or font snippets pasted from a provider dashboard that predates HTTPS-by-default.
-
srcsetand<picture>sources, which are easy to miss because they are not plainsrcattributes. - CSS
url()references to background images or fonts.
The crawler above only covers HTML attributes. For a thorough pass, also fetch your CSS files and search them for http://, and check any client-side JavaScript that builds URLs at runtime. A Content-Security-Policy header with upgrade-insecure-requests is a useful safety net, but it is a mitigation, not a fix. Do the rewrite first.
When provisioning actually fails
If more than a couple of hours pass and curl -I https://example.com still fails the handshake, the pipeline is stuck. Work through these in order.
Verify the CNAME value exactly
For a subdomain, the CNAME record must point to the Pages hostname, not to an IP, not to a proxy, and not to a URL with a path. The value is <user>.github.io. (or <org>.github.io.), with or without the trailing dot depending on your DNS provider. The apex domain is a special case: it cannot be a CNAME, so it needs A records to the documented GitHub Pages IPs, plus a CNAME file in the repository or the custom domain set in settings.
dig +short CNAME www.example.com
dig +short A example.com
dig +short example.com NS
Check the NS output too. If the domain is still delegated to your registrar's parking nameservers because you changed nameservers recently, the records you configured in the new DNS provider are not live yet. That alone will stall provisioning.
Wait out the TTL
If you changed DNS records within the last 48 hours, resolvers across the internet are still serving cached answers with the old target. GitHub's checker may have queried a resolver that still had the old record and concluded DNS was wrong. There is no way to force GitHub's check; it retries on its own schedule. Lowering the TTL before a planned migration (to 300 seconds, a day or two in advance) makes this window much shorter.
To see what different resolvers think, query a public resolver directly:
dig +short CNAME www.example.com @1.1.1.1
dig +short CNAME www.example.com @8.8.8.8
If these disagree, you are still inside the propagation window. Wait.
Remove and re-add the custom domain
If DNS is correct across resolvers, the served certificate is wrong or absent, and several hours have passed, remove the custom domain in the Pages settings, save, and add it back. This cancels the stuck request and starts a fresh one. It is safe, but it resets the clock, so only do it once you are confident DNS is right. Re-adding the domain while DNS is still wrong just reproduces the same stuck state.
Rule out a proxy in front of Pages
If the domain is behind Cloudflare or a similar proxy, the certificate GitHub issues may never be served, because the proxy terminates TLS with its own certificate. That is not necessarily a problem, but it changes where you look: the openssl output will show the proxy's issuer, and the "Enforce HTTPS" toggle in GitHub may stay disabled because GitHub cannot complete its own validation through the proxy. Either set the proxy to DNS-only during provisioning and re-enable it afterwards, or accept that HTTPS is handled at the proxy layer and configure enforcement there instead.
Check the CNAME file in the repo
If you set the custom domain through the repository's CNAME file rather than the settings UI, a mismatch between the file contents and the settings value can cause the domain to flap between configured states. Pick one method and stick to it; the settings UI writes the CNAME file for you.
A short checklist
Before enforcing HTTPS after a domain switch:
- Confirm DNS resolves correctly from multiple public resolvers.
- Wait for the certificate; verify it with
openssl s_clientand check the SAN list covers every hostname. - Crawl the site for
http://asset references and rewrite them. - Tick Enforce HTTPS once the checkbox unlocks.
- Verify the redirect with
curl -I http://example.comand re-run the crawler against the HTTPS origin to confirm nothing regressed.
The greyed-out checkbox is normal. The failures that matter are the ones where it stays greyed out for hours, and those almost always trace back to DNS.
More notes like this ship every week on this site.
Daily Picks
The following pairs are selected from the multi-timeframe trend scanner (Gate.io futures) and are for technical-analysis study only — not investment advice.
Data updated: 2026-10-25 08:56:33
Long
| Pair | Signal | Price | Take Profit | Stop Loss | R/R |
|---|---|---|---|---|---|
| AXIS | 短平快做多 | $0.2484 | $0.2559 | $0.2422 | 1:1.2 |
| ORDER | 短平快做多 | $0.0566 | $0.0583 | $0.0552 | 1:1.2 |
2 picks selected. Scanner runs every 15 minutes.
Top comments (0)