GitHub Pages makes HTTPS easy. HSTS is where people usually get tripped up.
The short version: if you use the default *.github.io domain, GitHub handles HTTPS and HSTS for you. If you use a custom domain, you need to understand what GitHub controls, what your DNS provider controls, and one annoying limitation: you generally can’t arbitrarily add or tune response headers on GitHub Pages itself.
That limitation matters because HSTS is just an HTTP response header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
If you can’t control headers, you can’t just “turn on HSTS” the way you would on Nginx, Apache, or Cloudflare.
So the real tutorial is this:
- Understand whether HSTS is already being sent.
- Know when GitHub Pages is enough.
- Know when you need a CDN or reverse proxy in front.
- Avoid breaking subdomains with a bad preload decision.
What HSTS actually does
HSTS tells browsers: “For this domain, always use HTTPS for a period of time.”
That protects users from protocol downgrade attacks and cookie leakage over accidental HTTP requests. Once a browser sees the header over HTTPS, it remembers the rule for max-age seconds.
Typical examples:
Strict-Transport-Security: max-age=300
Good for testing.
Strict-Transport-Security: max-age=31536000
A common production setting: one year.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Apply the rule to all subdomains too.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Eligible for browser preload lists if you submit the domain and meet the requirements.
Preload sounds cool. It’s also where people brick weird legacy subdomains they forgot existed.
GitHub Pages and HSTS reality
There are two common setups:
1. username.github.io or project.github.io
GitHub controls the platform and serves the site over HTTPS. In many cases, security headers including HSTS are already present because GitHub manages the edge infrastructure.
You don’t configure the header yourself.
2. Custom domain like docs.example.com or example.com
You point DNS at GitHub Pages, add the custom domain in your repo settings, and GitHub provisions TLS certificates.
HTTPS works, but your ability to control response headers is still extremely limited. GitHub Pages is static hosting, not a configurable web server.
That means if GitHub sends HSTS for your custom domain, great. If it doesn’t send the exact policy you want, you can’t usually fix that from inside the repo.
No _headers file. No Nginx config. No vercel.json-style header rules. No Apache .htaccess.
If you need strict header control, put Cloudflare, Fastly, or another reverse proxy/CDN in front of GitHub Pages.
First, verify what your site is already sending
Before changing DNS or adding another layer, check the current headers.
You can use your browser devtools, curl, or a scanner like HeaderTest if you want a quick read on security headers without digging through raw responses.
With curl:
curl -I https://docs.example.com
Example response:
HTTP/2 200
server: GitHub.com
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31536000
x-github-request-id: 1234:ABCD:5678:EFGH:999999
If you see strict-transport-security, your site already has HSTS.
If you don’t:
HTTP/2 200
server: GitHub.com
content-type: text/html; charset=utf-8
x-github-request-id: 1234:ABCD:5678:EFGH:999999
then GitHub Pages isn’t sending HSTS for that hostname, or something in front of it is stripping headers.
Also check the HTTP version:
curl -I http://docs.example.com
You want a clean redirect to HTTPS:
HTTP/1.1 301 Moved Permanently
Location: https://docs.example.com/
That redirect is not the same thing as HSTS. Redirects help on first contact. HSTS helps after the browser has learned the rule.
Basic GitHub Pages HTTPS setup
If you’re still setting up the site, get the platform basics right first.
For a subdomain custom domain
Use a CNAME record:
docs.example.com. 3600 IN CNAME username.github.io.
For an apex domain
GitHub recommends A/AAAA records to their Pages IPs. Check GitHub’s current docs for the exact values because they can change.
Then in your repository:
- Go to Settings
- Open Pages
- Set your Custom domain
- Enable Enforce HTTPS
That last checkbox matters. If it’s missing or disabled, GitHub hasn’t finished certificate provisioning yet, or your DNS is wrong.
What if you need your own HSTS policy?
This is the part people don’t want to hear: GitHub Pages alone is usually the wrong tool if you need to precisely manage HSTS.
Say you want this exact policy:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
You can’t count on GitHub Pages to let you set it yourself.
The practical fix is to put a proxy in front. Cloudflare is the most common option because setup is simple and free for many sites.
Example architecture
Browser -> Cloudflare -> GitHub Pages
Cloudflare terminates TLS, adds security headers, and fetches your static content from GitHub Pages.
Example: HSTS with Cloudflare in front of GitHub Pages
- Add your domain to Cloudflare.
- Update nameservers at your registrar.
- Keep your GitHub Pages origin working.
- In Cloudflare DNS, point your hostname to GitHub Pages.
- Enable HTTPS and configure HSTS in Cloudflare.
For a subdomain:
Type: CNAME
Name: docs
Target: username.github.io
Proxy status: Proxied
Then in Cloudflare dashboard:
- SSL/TLS → enable HTTPS
- Set SSL mode appropriately, usually Full or Full (strict) if origin cert validation is clean
- Edge Certificates → enable Always Use HTTPS
- Turn on HTTP Strict Transport Security (HSTS)
Cloudflare will then emit the header at the edge.
A typical production policy might be:
Strict-Transport-Security: max-age=31536000; includeSubDomains
If you want to inject headers more explicitly, Cloudflare can also do that with rules, depending on your plan and features.
Start with a small max-age
I’ve cleaned up enough HSTS mistakes to be pretty conservative here.
Don’t start with preload.
Don’t start with two years.
Don’t blindly enable includeSubDomains.
Use a staged rollout:
Phase 1: 5 minutes
Strict-Transport-Security: max-age=300
Phase 2: 1 week
Strict-Transport-Security: max-age=604800
Phase 3: 1 year
Strict-Transport-Security: max-age=31536000
Only after you’re confident every relevant hostname supports HTTPS should you consider:
Strict-Transport-Security: max-age=31536000; includeSubDomains
And preload? Treat that as a separate project, not a checkbox.
The includeSubDomains trap
This flag applies HSTS to every subdomain under the domain that sent it.
If example.com sends:
Strict-Transport-Security: max-age=31536000; includeSubDomains
then all of these need valid HTTPS:
www.example.comdocs.example.comblog.example.comold-admin.example.comanything.example.com
That forgotten Jenkins box from 2019 suddenly matters.
If your GitHub Pages site lives at docs.example.com, setting HSTS there with includeSubDomains affects only subdomains under docs.example.com, not sibling hosts like blog.example.com.
That distinction is useful. It also confuses people constantly.
Preload on GitHub Pages: be careful
To be preload-eligible, a site generally needs:
- HTTPS on the base domain
- Redirect from HTTP to HTTPS
- HSTS with long
max-age includeSubDomainspreload
Example:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
If you’re serving a marketing site on GitHub Pages at www.example.com and your apex, mail-related hosts, or internal subdomains aren’t fully HTTPS-ready, preload is a bad idea.
And since GitHub Pages doesn’t give you fine-grained rollback control over headers, I’d be even more cautious. Browser preload removal is slow and annoying.
Testing with curl and browser tools
A couple of commands I use regularly:
Check HTTPS headers:
curl -I https://example.com
Follow redirects from HTTP:
curl -I -L http://example.com
Show only the HSTS header:
curl -s -D - https://example.com -o /dev/null | grep -i strict-transport-security
In Chrome or Edge, devtools network tab works fine too. Load the page, click the document request, inspect response headers.
If you’re troubleshooting a proxy in front of GitHub Pages, compare direct origin behavior versus edge behavior. Sometimes the CDN is adding the header; sometimes GitHub is; sometimes nobody is.
A sane recommendation
For most GitHub Pages sites:
- Enable custom domain correctly
- Enable Enforce HTTPS
- Check whether HSTS is already present
- If the existing policy is good enough, stop there
If you need custom HSTS behavior:
- Put Cloudflare or another CDN/reverse proxy in front
- Start with low
max-age - Increase gradually
- Don’t use
includeSubDomainsunless you’ve audited subdomains - Don’t preload unless you’re very sure
GitHub Pages is great static hosting. It’s not a full edge security configuration platform. Once you accept that, the setup decisions get a lot easier.