DEV Community

Jackson
Jackson

Posted on Originally published at macless.dev

A header scan says 16/16. Here's what it can't see.

A few days after how I built and shipped an iOS app without a Mac went up here, a reader named Amit Feldman left a comment with an actual finding, not a vague warning: macless.dev was fast and the on-page SEO was tidy, but HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy were all missing. Six headers, zero excuses — the site's on GitHub Pages, and Pages doesn't let you set custom response headers at all. That's just how the platform works, and it's an easy thing to never notice if nobody runs the scan.

The fix was straightforward once it was named: keep the site on 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 Edge Certificates. None of it touches the deploy workflow. Amit re-scanned twice more as things landed — first the five easy headers, then a properly tight Content-Security-Policy (default-src 'self'; script-src 'none', since the page ships zero JavaScript, nothing to break) — and macless.dev sits at 16 passed, 0 warnings, 0 failures now. He's writing that before/after up as its own case study, which is a nicer way to end up getting linked than most.

His follow-up point was the more useful one. A header scan reads what a browser receives — it says nothing about what a CI pipeline does with your signing certificates. Macless is a signing pipeline: GitHub Actions, code-signing certificates, App Store Connect and Google Play credentials. A header scan can't tell you whether a cert is sitting in the repo, whether a workflow echoes a secret into its log, or whether a token is scoped wider than it needs to be. That's a fair distinction between two different layers, so rather than take anyone's word for either layer, I went and checked the second one directly.

What "actually checked" means here

Not a policy review. A search of the real git history of both appledev (Citolex's source) and the Macless product template, plus a read-through of every workflow file line by line:

  • Full commit history (not just current files) searched for any certificate, private key, or keystore ever committed and later removed.
  • Every workflow checked for secrets interpolated directly into shell commands versus passed through env: (the safer pattern — GitHub can mask an env-sourced secret in logs; a value pasted straight into a run: string is both a log-exposure and shell-injection risk).
  • Checked for set -x/set -ex, which would dump every command — including expanded secret values — into the public build log.
  • Checked what triggers each workflow. pull_request_target combined with secrets is the classic way a fork PR exfiltrates credentials from a public repo; none of these workflows use it.
  • Traced exactly where decoded certs and keystores get written during a build — ephemeral runner temp storage that dies with the job, or the repo working directory that could get committed or accidentally artifact-uploaded.
  • Checked the actual GitHub Actions secret scoping this template's own docs tell you to set up (App Store Connect API key role, Play Console service account role).

What was already clean

No certificate, key, or keystore file has ever been committed to either repo, at any point in their history. Secrets are passed through env: blocks, not interpolated into shell strings. Nothing sets -x. Every workflow triggers on push or workflow_dispatch only — nothing runs on a fork's pull request, so there's no path for an outside contributor to exfiltrate a secret through a crafted PR. Decoded cert and keystore material only ever lands in the runner's ephemeral temp directory, never the repo itself, and the artifact-upload steps grab specific screenshot files, not whole build directories. The documented credential roles (App Store Connect "App Manager," Play Console "Release manager") were already scoped down from full admin access.

Two real things, both fixed

Not everything was already perfect — two genuine, if minor, gaps:

1. No explicit permissions: block. None of these workflows push commits, open PRs, or otherwise need to write back to the repo — they build, sign, and upload. Without an explicit block, GITHUB_TOKEN inherits whatever the repo or org default is, which can be broader than a workflow actually needs. Fixed by adding this to the top of every workflow:

permissions:
  contents: read
Enter fullscreen mode Exit fullscreen mode

2. No .gitignore for signing material. SIGNING.md and ANDROID.md walk you through generating a real certificate or keystore with openssl/keytool. Nothing in the docs stopped someone from running those commands inside their project folder, and a plain git add . afterward would have committed a real private key. Fixed two ways: a .gitignore now ships with the template covering *.p12 *.key *.cer *.pem *.mobileprovision *.keystore *.jks *_base64.txt, and both docs now say plainly to generate this material from a scratch folder outside the repo, not inside it.

If you're building something similar: both of these are worth checking in any CI pipeline that handles signing material, not just this one. An explicit permissions: block costs three lines and closes off a class of supply-chain risk for free. A .gitignore that anticipates exactly which files your setup docs are about to tell someone to generate is cheap insurance against the single most common way a real credential ends up in a public repo — someone followed the instructions correctly, in the wrong directory.

Where this leaves things

Both CI fixes are live now - in Citolex's own repo and in the Macless product template everyone downloads. The header layer Amit caught is fixed and independently reverified at 16/16, and he wrote the whole before/after up as its own case study. Worth being straightforward about the rest of his message too: alongside the case-study offer, he also pitched his paid audit services - a Launch-Ready Audit ($99) and a Deep Dive ($149) - covering exactly this CI/secrets layer. Given he'd already found three real things for free, that's not an unreasonable thing to charge for; I just happened to be able to check it myself directly, in about twenty minutes, using the repos' own git history rather than a read-only link handed to someone else. Two real, minor issues turned up, same as the header pass did. If you want the deeper, ongoing version of what he did here for free, his audits are worth a look. Thanks are owed either way: the header catch was real, the nudge to check the layer underneath it was a good one, and the case study is a fair trade for both.


SIGNING.md and ANDROID.md now warn you before you generate anything, every workflow ships with a scoped-down permissions: block, and the .gitignore is there as a backstop either way — all part of Macless, a one-time template for shipping an iOS (and now Android) app through GitHub Actions without a Mac.

Top comments (0)