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:

  1. Understand whether HSTS is already being sent.
  2. Know when GitHub Pages is enough.
  3. Know when you need a CDN or reverse proxy in front.
  4. 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

  1. Add your domain to Cloudflare.
  2. Update nameservers at your registrar.
  3. Keep your GitHub Pages origin working.
  4. In Cloudflare DNS, point your hostname to GitHub Pages.
  5. 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.com
  • docs.example.com
  • blog.example.com
  • old-admin.example.com
  • anything.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
  • includeSubDomains
  • preload

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 includeSubDomains unless 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.