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.
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:
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
Items listed were changed from the default value - make the same changes and keep the rest of this file as you find it.
These will achieve the following results:
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
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>
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>
This avoids unnecessary bloat - stick to using the theme CSS unless you really need to use the plugins own CSS.
built_in_css: false in the .yaml config file for each plugin under the user/plugins/ directory e.g.
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.
</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
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
My Content-Security-Policy (CSP) settings by default allow the following:
<script></script>tags rather than loaded from a script file).
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.
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?
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.
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
optipng commands with their default settings.
For more information on their installation and use please see here.
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:
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?)
So what has this all cost me?
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!