Field notes
GitHub Pages, custom domain: "Enforce HTTPS" greyed out, certificate stuck, and the www CNAME that causes it
My portfolio sat for weeks with "Enforce HTTPS" greyed out while http:// served the whole site with no redirect. The cause was one DNS record; the diagnosis needed the API rather than the settings page. Here is the checklist I wish I had had.
The symptom
Settings → Pages shows your custom domain with a green check, but Enforce HTTPS is greyed out. Sometimes with "certificate is being provisioned", sometimes with nothing. Meanwhile:
$ curl -sI http://example.me/ | head -1
HTTP/1.1 200 OK # served in clear, no redirect
$ curl -sI https://example.me/ | head -1
# TLS error, or a certificate for the wrong name
Read the real state from the API
The settings page hides the interesting field. The Pages API does not:
gh api repos/OWNER/REPO/pages --jq '{status, cname, https_enforced, cert: .https_certificate}'
https_certificate.state is what you want. The values you will meet:
| state | meaning |
|---|---|
new / authorization_created |
GitHub has asked Let's Encrypt; DNS must already be right |
authorization_pending |
the ACME challenge is waiting — usually DNS not propagated |
authorized / issued |
certificate exists; the checkbox becomes clickable within an hour |
dns_changed |
GitHub saw your DNS change and restarted from scratch |
errored |
check description — CAA record, or a record pointing elsewhere |
dns_changed is the one that looped for me: every time a record changed, the process restarted, and one record kept "changing".
The DNS that actually works
GitHub's documented setup is short, and every deviation costs a certificate cycle:
example.me. A 185.199.108.153
example.me. A 185.199.109.153
example.me. A 185.199.110.153
example.me. A 185.199.111.153
www.example.me. CNAME OWNER.github.io. # NOT example.me, NOT an A record
My mistake: www was a CNAME to the apex (example.me) instead of OWNER.github.io. Both resolve to the same addresses, so the site worked on www — but GitHub's certificate request lists both names, its verifier treats a www that does not point at OWNER.github.io as "changed", and the request restarts. The certificate that finally got issued after the fix covers both names:
$ echo | openssl s_client -connect example.me:443 -servername example.me 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:example.me, DNS:www.example.me
Two more records to check:
- CAA. If your zone has any CAA record, it must include
0 issue "letsencrypt.org", or issuance fails with a politeerrored. No CAA record at all is fine. (Adding one that allows only Let's Encrypt is a cheap hardening step once everything works.) - No AAAA records pointing anywhere else, and no leftover A record from the previous host — the verifier checks all of them.
Verify from outside your own resolver, because your cache will lie to you for the whole TTL:
dig +short example.me A @8.8.8.8
dig +short www.example.me CNAME @8.8.8.8
dig +short example.me CAA @8.8.8.8
Unsticking a certificate that is "being provisioned" forever
If DNS is right and the state has been authorization_pending for more than a day, GitHub's job is stuck, not yours. What worked for me: in Settings → Pages, remove the custom domain, save, add it back, save. That discards the stuck request and starts a fresh one; with correct DNS the state went to issued within an hour. (Doing the same through the API did not help — the UI path seems to reset more.)
Then wait for the checkbox. It becomes clickable shortly after issued, not at the same moment. Tick it, and within about five minutes:
$ curl -sI http://example.me/ | grep -i '^location\|^HTTP'
HTTP/1.1 301 Moved Permanently
location: https://example.me/
Check the whole matrix, not just the apex: http:// and https:// for the apex, www, and OWNER.github.io, plus any sub-pages. The OWNER.github.io redirect in particular kept pointing at http:// until the checkbox was on.
What the checkbox does not give you
- HSTS. Pages does not send
Strict-Transport-Security, so the first request of a new visitor can still go out in clear. If you put Cloudflare in front, enable HSTS there; otherwise accept it. - Security headers in general — no CSP, no
X-Frame-Options. A<meta http-equiv="Content-Security-Policy">in the HTML covers most of the CSP value; clickjacking protection cannot be done from a meta tag. - Renewal visibility. Certificates auto-renew, but nothing tells you if renewal fails. A weekly cron on any machine you own is enough:
echo | openssl s_client -connect example.me:443 -servername example.me 2>/dev/null \
| openssl x509 -noout -enddate
Bonus: "Page build failed" with no reason
Unrelated to HTTPS but found the same week: the legacy Pages build failed five times in a row on a commit that added three PNG icons generated with Python. Valid PNGs, no Jekyll, .nojekyll present. Removing the three files fixed it; the same generator's apple-touch-icon.png built fine. I never found the rule — but I did find that a build can also fail transiently for no reason at all, so before bisecting your commit, re-trigger the build and see if it goes green:
gh api -X POST repos/OWNER/REPO/pages/builds
gh api repos/OWNER/REPO/pages/builds/latest --jq '{status, error: .error.message}'