Custom HTTP headers with GitHub pages
September 14, 2018At some point when I was developing my new homepage I ran into an issue with GitHub Pages. What I wanted, was to follow security best practices, and serve OWASP recommended Security Headers. Unfortunatelly, it looks like GitHub Pages does not support setting anything like this, which is kinda dissapointing. But hey! We are re developers, and we love to solve problems!
And sure I did - with a help of a small chunk of third party services.
Prerequisites
To make this work you would need:
- A custom domain name - it is not mandatory, but strongly recommended - otherwise you would end up with
*.herokuapp.com
url to your page. You can get one for free at dot.tk however I strongly encourage you to buy proper one - it's not that expensive, and looks way better. - Cloudflare account - it's only necessary when you want to use a custom domain, otherwise you can skip it. Their free plan is excellent, covers all our needs and as bonus, provides free SSL out of the box.
- Heroku account - this is where all the magic happens.
- UptimeRobot account - or pingdom, or any other service which checks if your site is alive in short time intervals. It's not mandatory, but helps us avoid some heroku limitations.
- GitHub account - obviously.
A guide
Step 1: Create your GitHub page
This is pretty obvious, as this is the whole point of this guide. Create a page and deploy it to GitHub. Don't forget to configure the custom domain on GitHub side. Few things to remember:
- The configured domain should be YOUR domain, not heroku or github.
- Important! This setup needs to use a
CNAME
record set for heroku app, which can't be set on a root domain. Which means, we need GitHub to force redirection to that record. I'm usingwww.
in this example, which means, you should set the domain on GitHub side towww.your-custom-domain.com
. - Don't worry about DNS settings yet.
- If you don't want to use a custom domain, then you need o set it up to
name-of-your-heroku-app.herokuapp.com
.
Step 2: Create the custom HTTP reverse proxy on heroku
This is the place where we add our custom headers. We'll leverage the power of heroku custom buildpacks. What we need, is a nginx reverse proxy with one purpose only - route all traffic back and forth to our GitHub page, and append extra information (which are our headers) to response.
Fortunatelly, I already did most of the work and created a buildpack just for that. All you need to do is add it to your app and properly configure it. Let's do this!
Create a heroku app
You need a heroku CLI installed and configured. If you have never used heroku before, please refer to the documentation on how to do this. On OSX it's as easy as:
brew install heroku/brew/heroku && heroku login
After that you need to create a custom heroku app (the most basic one is free of charge) and attach custom buildpack to it.
The buildpack would do all the job for you, however, to trigger it, some "application" needs to be deployed. It doesn't really matter what you put there, as the buildpack would never reach it. So add a file, push it to heroku and you're good to go.
mkdir my-heroku-app && cd my-heroku-app
git init
heroku apps:create --buildpack https://github.com/ajgon/github-proxy-buildpack <name-of-your-heroku-app>
touch .keep
git add -A && git commit -m 'Initial commit'
heroku git:remote --app name-of-your-heroku-app
git push heroku master
Configure GitHub page URL
First of all, you need to tell the buildpack, where to route the whole traffic. You can do this by setting ROUTE_TO_URL
environment variable. You can even use a path here, so you can have a root domain pointing to custom GitHub project - you don't have to create login.github.io
one!
heroku config:set ROUTE_TO_URL=https://my-github-name.github.io/my-project
or just
heroku config:set ROUTE_TO_URL=https://my-github-name.github.io
Configure headers
The most sane configuration of the headers is already included in the buildpack, however you can customize it any way you like, using the environment variables. The format is HTTP_YOUR_HEADER_UNDERSCORED
. For example: X-Frame-Options
becomes HTTP_X_FRAME_OPTIONS
. You can set it, using the following command:
heroku config:set HTTP_X_FRAME_OPTIONS=deny --app <name-of-your-heroku-app>
Configure CSP nonce
There is also a CSP nonce rewrite support included, which is based on this excellent article by Scott Helme, however, configuring it is slightly more complicated.
First of all you need to name the "ID" of your nonce. This is a string which will be replaced in your HTML output by the actual nonce generated by nginx.
heroku config:set CSP_NONCE_ID="**CSP_NONCE**"
The actual CSP nonce will be held by nginx in $cspNonce
variable. Which means, you need to use this variable, when you set a CSP header:
heroku config:set HTTP_CONTENT_SECURITY_POLICY="default-src 'self'; script-src 'self' 'nonce-$cspNonce';"
Lastly, you need add the "ID" string as a nonce to all the <script />
tags in your HTML:
<script nonce="**CSP_NONCE**">alert('Hello World!!!');</script>
Sprinkle on top
As a bonus, you can tell the buildpack, to completely hide the custom GitHub headers in response, to completely disguise your site. Nice!
heroku config:set HIDE_GITHUB_HEADERS=1 --app <name-of-your-heroku-app>
Attaching domain to heroku
Last thing you need to do, is tell heroku load balancer to attach your appllication to your domain, by invoking:
heroku domains:add your-custom-domain.com --app name-of-your-heroku-app
This feature is completely free, however heroku requires you to confirm your credit card (without charging it). You can do this in heroku billing section. The good news is, the card information doesn't have to be there all the time. Which means, you can verify your card, add a domain and then remove it from the system.
Testing the setup
After setting all necessary data, test your setup to check the headers and response:
curl -I https://name-of-your-heroku-app.herokuapp.com/ # to check headers
curl https://name-of-your-heroku-app.herokuapp.com/ # to see if page responds correctly
Step 3: Glue heroku with your domain using cloudflare
Technically you can do this with your DNS provider and completely skip this step. However Cloudflare provides SSL certificate, and powerful CDN free of charge. I don't see any reason why you shouldn't use it.
DNS
First, go to DNS section, and set up the records as following:
Type | Name | Value | TTL | Status |
---|---|---|---|---|
A | your-custom-domain.com | 185.199.110.153 | Automatic | pass through CF |
CNAME | www | name-of-your-heroku-app.herokuapp.com | Automatic | pass through CF |
The IP above may change, but you should use one of the GitHub pages IPs for A records.
Crypto
Here you can set up your SSL settings for the certificate provided by Cloudflare. You can configure it as you like, but my recommendation is:
- SSL: Full
- Always use HTTPS: On
- HTTP Strict Transport Security (HSTS): You can leave it disabled and rely on the heroku App, or customize it here (it would take precedence)
- Minimum TLS Version: 1.2 - there is no reason to serve the deprecated ones, unless you need to support some very old browsers.
- Opportunistic Encryption: On
- TLS 1.3: Enabled
Testing the domain
If you did everything correctly, you should be able to visit your domain and see your custom headers:
curl -I https://www.your-custom-domain.com/ # to check the headers
curl https://www.your-custom-domain.com/ # to see if page responds correctly
I strongly recommend visiting securityheaders.com service as well, which can validate your setup and point out any broken or missing parts.
Step 4: Keeping heroku alive
One disadvantage of free heroku applications is they are put to sleep when inactive. However, if you wish to use heroku for this purpose only - you can keep the application alive all the time. To do that, you simply need to ping it at least once in 30 minutes.
This can be achieved by using one of the page monitoring services. Personally I use UptimeRobot, but if you have or like the other one - that's fine as long as you fit this time window.
Conclusion
As you can see, the whole process is somehow cumbersome, but if you manage to follow it, your GitHub page can get any HTTP header you like, along with the secure ones (Access-Control-Allow-Origin
anyone? :-)). I'm personally using this setup for my homepage and never experienced any troubles.
And that's it - enjoy your free, SSL-supported GitHub page with custom domain and all the security headers!