In my last post, Grav in Azure part 5 - Optimising Cloudflare for performance and security, I demonstrated how to configure Cloudflare to optimise the performance and security of your grav website (or indeed any largely static website).
In this post we’ll take a look at how to do the same with Grav itself.
What you will need
- A grav website, hosted on a Windows Web App in Azure.
At this point you may be wondering why we would do anything with performance or security in Grav on the origin server (the Azure web app instance) when we’re sending users where possible to Cloudflare (the edge server)?
There are a couple of reasons why:
- In order for items to end up in the Cloudflare cache, they have to be requested from the origin server at least once to be cached - so you still want that to be as quick a transaction as possible.
- If at any point something has to be retrieved from the origin server in Azure because the edge server (Cloudflare) can’t accomodate the request, you want that fallback to be as painless as possible.
- From a security perspective, some things make sense to secure at both the origin and the edge (you dont want a nice secure cloudflare front-end but be vulnerable any time a request hits the back-end). Better yet, if they are HTTP headers, setting them at the origin ensures those headers are also present when being served up by Cloudflare.
Modifying grav configuration files
Before we start, a bit of advice (advice readily given if you read the grav documentation) - avoid modifying anything in the system directory. Anything you change outside of the user directory is prone to being overwritten when you update the base grav software, plugins etc. So use the user config files which overlay the system config files.
You may have to restart the web app after changing the web.config
file, the system.yaml
file or adding an .xdt
file.
user/config/system.yaml - Grav system configuration
Items listed were changed from the default value - make the same changes and keep the rest of this file as you find it.
Many of these were determined as being needed by testing the site against both webpagetest.org and GTMetrix.
These will achieve the following results:
- Enable processing of Twig and Markdown content
- Enable browser caching, with a maximum age of 7 days (604800 seconds). so unless the item in the browser cache (page, images etc) is older than 7 days, use the item in browser cache rather than download again from the server.
- Enable gzip compression of items in cache (to reduce time to download. (cache should already be enabled via
enabled: true
) - Disable twig debug (you should only enable this when debugging issues with twig templates, otherwise its an unnecessary overhead on the web app instance.
- Enable minification of javascript (js) and css (removes whitespace and comments, reducing the size of those resources and speeding up page load)
- Enable pipelining of js and css assets (allows less important assets to load after initial page renders rather than delaying painting of the page to the user)
- Enable rewrite of any CSS relative URLs during pipelining
- Enable caching of all images in the grav cache system
pages:
process:
markdown: true
twig: true
cache-control: 'public, max-age=604800'
expires: 604800
cache:
enabled: true
gzip: true
twig:
cache: true
debug: false
assets:
css_pipeline: true
css_minify: true
css_rewrite: true
js_pipeline: true
js_minify: true
images:
cache_all: true
applicationHost.xdt
Create a file called applicationHost.xdt in the base directory of your repository and add in everything below. this will enable HTTP compression on the origin server using an XML document transform (XDT).
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.webServer>
<httpCompression>
<dynamicTypes>
<add mimeType="application/json;charset=utf-8" enabled="true" xdt:Transform="InsertAfter(/configuration/system.webServer/httpCompression/dynamicTypes/add[(@mimeType='application/json')])" />
</dynamicTypes>
</httpCompression>
</system.webServer>
</configuration>
web.config - adding MIME types for woff/woff2
This tweak was identified by reviewing the results from webpagetest.org, which identified errors caching the files and also significant delay in loading them, which slowed the page load significantly.
Add the following to the web.config file immediately after the </rewrite>
tag and before the </system.webServer>
tag to add mime types for woff/woff2 files so they can be correctly loaded and cached.
<staticContent>
<remove fileExtension=".woff" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/x-woff" />
</staticContent>
Disable built-in CSS on plugins
This avoids unnecessary bloat - stick to using the theme CSS unless you really need to use the plugins own CSS.
Set built_in_css: false
in the .yaml config file for each plugin under the user/plugins/ directory e.g.
- user/plugins/archives/archives.yaml
- user/plugins/breadcrumbs/breadcrumbs.yaml
- user/plugins/form/form.yaml
- user/plugins/login/login.yaml
- user/plugins/pagination/pagination.yaml
- user/plugins/problems/problems.yaml
- user/plugins/simplesearch/simplesearch.yaml
web.config - HTTP Headers
Before the closing </rewrite>
tag, add the following text:
<outboundRules rewriteBeforeCache="true">
<rule name="Remove X-Powered-By HTTP response header">
<match serverVariable="RESPONSE_X-Powered-By" pattern=".+" />
<action type="Rewrite" value="" />
</rule>
</outboundRules>
This will change the content of the X-Powered-By header to blank text, to avoid giving away information about the server/application stack that might be useful to a hacker in trying to attack your site.
After the </staticContent>
tag added earlier, add in the following text and modify as necessary:
<httpProtocol>
<customHeaders>
<clear />
<add name="Content-Security-Policy" value="default-src 'self' disqus.com links.services.disqus.com; script-src 'self' 'unsafe-inline' ajax.cloudflare.com disqus.com referrer.disqus.com cirriustech.disqus.com c.disquscdn.com www.google-analytics.com; style-src 'self' 'unsafe-inline' disqus.com referrer.disqus.com cirriustech.disqus.com *.disquscdn.com; img-src 'self' www.cloudflare.com disqus.com referrer.disqus.com cirriustech.disqus.com *.disquscdn.com www.google-analytics.com; font-src 'self'; form-action 'self'; upgrade-insecure-requests;" />
<add name="X-Frame-Options" value="DENY" />
<add name="X-XSS-Protection" value="1; mode=block" />
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
</customHeaders>
<redirectHeaders>
<clear />
</redirectHeaders>
</httpProtocol>
This will enable the X-Frame-Options
, X-XSS-Protection
, Strict-Transport-Policy
, Referrer-Policy
, and Content-Security-Policy
options per recommendations from Scott Helme, somewhat of an expert on this topic.
WARNING: You will break your site many times in working out the correct CSP for your site. Use the developer tools built into most modern web browsers (Edge, Chrome, Firefox) to help identify what assets are being blocked by CSP and which aspect of the CSP resulted in the block. This is accessed in Edge and Chrome by pressing the
<F12>
key.
My Content-Security-Policy (CSP) settings by default allow the following:
- Allow content to be loaded from my own domain, disqus.com and links.services.disqus.com
- Allow scripts to be loaded from my own domain, and several Disqus, Cloudflare and Google-analytics domains and also allow inline scripts (code in between
<script></script>
tags rather than loaded from a script file). - Allow styles to be loaded from my own domain and several Disqus and Cloudflare domains and also allow inline style definition (between
<style></style
tags). - Allow images to be loaded from my own domain as well as Disqus, Cloudflare and Google-Analytics domains.
- Allow fonts only to be loaded from my domain
- Allow form-actions only to interact with URLs from my own domain
- Where possible upgrade all insecure (HTTP) requests to secure requests (HTTPS).
- Any requests not matching these rules will be blocked by most modern web browsers, which protects against malicious code injected into the web content being served
Now you may notice the word unsafe a few times there… The Quark theme which is the default theme for Grav makes extensive use of inline assets, as do some of the plugins that I use. From a CSP perspective, this is frowned upon as it is possible to inject malicious code into HTML body, which CSP Is designed to prevent by only allowing the loading of content from locations specified by the CSP.
'unsafe—inline'
allows inline resources to be loaded and as a result the best rating possible on SecurityHeaders.io is an A.
For now, I am happy that having a CSP in place even with inline assets allowed is a significantly better position to be in than not to have a CSP. But I will be chasing down solutions for the problem assets so that I can remove the 'unsafe-inline'
declarations from my CSP.
web.config - disabling the Server header
After the closing <\httpProtocol>
tag, add in the following text:
<security>
<requestFiltering removeServerHeader="true" />
</security>
This stops the Server header from being shown, and so hides the Web Server software name being shown - why would you advertise anything that makes a hacker or malicious actors job easier?
web.config - Disable Version header
The default web.config file has an <httpRuntime>
tag which is as follows:
<httpRuntime requestPathInvalidCharacters="<,>,*,%,&,\,?" />
Amend it as follows:
<httpRuntime requestPathInvalidCharacters="<,>,*,%,&,\,?"
enableVersionHeader="false" />
This disables the Version header - again hiding information available by default that doesn’t need to be disclosed.
Now, I’ve not gone into detail on each of these headers, just given you a run-through of what I have set based on advice from a number of sources.
Quite simply, there’s no point in me re-inventing the wheel when others have done a great job on this topic already, so see the links at the end of this post for more information.
Images
Keeping your images on your website small/optimised reduces page load times.
I use either the online app Squoosh or on my raspberry pi (or any other linux based machine), I use the jpegoptim
and optipng
commands with their default settings.
For more information on their installation and use please see here.
Recap
No Optimisation
So, with a basic grav blog skeleton site, on an F1 web app instance, with no cloudflare front-end or security/performance optimisation, we can expect performance as follows (courtesy of gtmetrix.com:
And in terms of security headers (courtesy of SecurityHeaders.io:
Optimised - Cloudflare Edge
Now with all the optimisation, reporting against the Cloudflare edge URL, we have performance as follows:
And security as follows:
All for very little effort and delivering a responsive and reasonably secure blog site (to get an A+ rating on SecurityHeaders.io I’ll need to change the Quark theme to detour it’s love of inline assets, but an A is still better than an F, right?)
Cost
So what has this all cost me?
- 1x D1 Web App instance = £7.30/month + VAT
- 1x Domain Name = £6.16
If I ran this on a Windows VM, even the smallest available, A0, would come in at around £9.79 + VAT per month, then you need to factor in managed disks, storage transactions and it probably wouldn’t be large enough to provide anything like a decent response time (to be fair neither would a D1, without the Cloudflare front-end). And I’d need to manage the infrastructure, not just the application and content.
What about a larger Web App? A basic B1 would come in at around £40 + VAT per month per instance.
If I wanted AzureCDN, Redis Cache, TrafficManager etc, the costs keep coming.
For a small blog, a D1 Web App, running Grav, optimised for performance/security and using Cloudflare, is a great value solution, and responsive/secure to boot!
If I need to scale, I can quickly and easily scale up/out by changing from a D1 web app to one or more, more powerful web app instances. I can add TrafficManager easily enough to front-end and load balance, and/or I could switch to a paid Cloudflare account.
In other words, putting all your eggs in one basket with a single cloud service provider may not always be the right solution for your needs!
In the next post in this series, I’ll take a look at backup options, and areas that may benefit from further automation.
As ever, any comments or questions, use the comments down below!
If you like what I do and appreciate the time and effort and expense that goes into my content you can always
Read More
Securityheaders.io scan of facebook.com
Scott Helme CSP Cheat Sheet
Google Developers CSP Fundamentals
html5rocks.com CSP tutorial