Skip to content

Build1 publisher3 min readPublished

GitHub Pages cannot send a single security header, so every Pages site starts at zero

A reader's scan found six response headers missing on a Pages-hosted site. The fix was a Cloudflare transform rule in front of the custom domain, with no change to the deploy workflow.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • A reader named Amit Feldman commented that macless.dev was fast with tidy on-page SEO but was missing HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy.
  • macless.dev is hosted on GitHub Pages, and GitHub Pages does not let you set custom response headers at all.
  • The fix was to keep the site on GitHub Pages, put Cloudflare in front of the custom domain, and add the header set with a Response Header Transform Rule.
  • HSTS is a toggle under Cloudflare's Edge Certificates settings.
  • None of the header work touches the deploy workflow.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A reader named Amit Feldman ran a header scan against macless.dev and came back with six specifics rather than a warning: HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy were all absent [1]. The cause is architectural rather than careless: the site is hosted on GitHub Pages, and Pages does not let you set custom response headers at all [2].

That is worth sitting with if you ship anything static. Any site served directly by Pages fails those six checks by construction, no matter how careful the HTML is, because there is no place in the platform to put a header. The scan reports what arrives at the browser, and nothing in a Pages build can change what arrives.

The fix, once named, required no change to the site or the pipeline. Keep the site on Pages, put Cloudflare in front of the custom domain, and add the header set with a Response Header Transform Rule [3]. HSTS is a toggle under Edge Certificates rather than a rule [4]. None of it touches the deploy workflow [5]. This is the useful shape of the problem: the headers are an edge concern, so they get solved at the edge, and the repository stays as it was.

The Content-Security-Policy was set to `default-src 'self'; script-src 'none'`, which is viable because the page ships zero JavaScript and so there is nothing to break [6]. Feldman re-scanned twice as the work landed, first the five simpler headers and then the tighter CSP [7], and the site now reports 16 passed, 0 warnings, 0 failures [8]. Six of those sixteen checks were the missing headers, which puts the pre-proxy ceiling at ten [9].

The more durable point in the exchange was Feldman's follow-up: a header scan reads what a browser receives and says nothing about what a CI pipeline does with signing certificates [10]. Macless is a signing pipeline, built on GitHub Actions with code-signing certificates plus App Store Connect and Google Play credentials [11]. A scan cannot tell you whether a certificate is sitting in the repository, whether a workflow echoes a secret into its log, or whether a token is scoped wider than it needs to be [12].

So the author audited that second layer directly: a search of the full commit history of both the appledev source and the Macless product template, plus a line-by-line read of every workflow file [13]. No certificate, key or keystore has ever been committed to either repository at any point [14]. Secrets are passed through `env:` blocks rather than interpolated into shell strings, and nothing sets `-x` [15]. Every workflow triggers only on `push` or `workflow_dispatch`, so there is no `pull_request_target` path for a fork PR to exfiltrate credentials [16]. Decoded certificate and keystore material lands only in the runner's ephemeral temp directory, and artifact uploads grab specific screenshot files rather than whole build directories [17]. The documented credential roles, App Store Connect "App Manager" and Play Console "Release manager", were already narrower than admin [18].

Two real gaps surfaced. There was no explicit `permissions:` block, so `GITHUB_TOKEN` inherited whatever the repository or organisation default was, despite none of the workflows needing write access; that was fixed by adding `permissions: contents: read` to every workflow [19]. And there was no `.gitignore` entry for signing material, even though SIGNING.md and ANDROID.md walk the user through generating a real certificate or keystore with `openssl` and `keytool` [20].

Watch for the before/after case study Feldman is writing up [21], and check your own static hosts: if the platform has no header layer, a clean scan is only ever evidence about the proxy.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories