Securing your website
Basic steps to have a more secure website, and achieve an A+ rating on Mozilla Observatory.
I recently took this website from a rather poor 'F' rating, to an 'A+' on Observatory. I wanted to share with you the tweaks I made, as no doubt some of you are going through, or need to go through the same journey!
For context, I use Nginx as a reverse proxy to my blog (nodejs, ghost).
If you do something similar you could actually use my prebuilt docker container which not only allows you to turn these options on and off per host, but also generates LetsEncrypt certificates for you in a totally automated fashion.
Configuration Options
SSL Stapling
SSL Stapling (Otherwise known as OCSP) is an attempt to deal with many of the drawbacks associated with typical CA verification of the SSL certificate presented by the server. In essence, the server will send a stapled response, which includes additional verification of the server certificate. I won't go into too much detail, there is an excellent blog over at mozzila which is worth reading.
ssl_stapling on;
SSL Ciphers
We want to ensure that we only use ciphers which are considered secure, we can also tell Nginx our order of preference for these ciphers too.
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH RSA+AESGCM RSA+AES !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
ssl_prefer_server_ciphers on;
SSL Protocols
We don't want to use older insecure protocols like SSLv3. Even TLSv1 is questionable (have a read of this blog)
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
Now lets take a look at some headers.
HTTP Headers
X-Frame-Options
The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a
<frame>
,<iframe>
or<object>
. This header can be used avoid clickjacking attacks, by ensuring that your site cannot be loaded within an frame in another site.
In my case, I use SAMEORIGIN which means frames from the same domain (karlstoney.com) are allowed.
add_header X-Frame-Options SAMEORIGIN always;
X-Content-Type-Options
The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a way to say that the webmasters knew what they were doing.
This header was introduced by Microsoft in IE 8 as a way to block content sniffing that was happening, which could transform non-executable MIME types into executable MIME types. It's pretty common for this to be set as standard
add_header X-Content-Type-Options nosniff always;
X-XSS-Protection
The HTTP X-XSS-Protection response header is a feature of Internet Explorer, Chrome and Safari that stops pages from loading when they detect reflected cross-site scripting (XSS) attacks. Although these protections are largely unnecessary in modern browsers when sites implement a strong Content-Security-Policy that disables the use of inline JavaScript ('unsafe-inline'), they can still provide protections for users of older web browsers that don't yet support CSP.
So the interesting thing to point out here is that it's unnecessary on modern browsers when we implement a good CSP (which we will do, further down), but it helps with older browsers that don't support CSP yet - for example <= IE10.
add_header X-XSS-Protection "1; mode=block" always;
Strict-Transport-Security
The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) is a security feature that lets a web site tell browsers that it should only be communicated with using HTTPS, instead of using HTTP.
Take the scenario where you are redirecting from http:// to https://, like I do on my blog. This opens up the potential for a man-in-the-middle attack, where the redirect could be exploited to direct a user to a malicious site instead of the secure version of the original page.
Once the client has got this header back from the https:// response, their browser will always use https:// for the domain for the duration specified in "max-age".
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Content-Security-Policy
The HTTP Content-Security-Policy response header allows web site administrators to control resources the user agent is allowed to load for a given page. With a few exceptions, policies mostly involve specifying server origins and script endpoints. This helps guard against cross-site scripting attacks (XSS).
In summary, you can tell the clients browser to only load resources for the exact domains that you specify in this header. Any deviation from this will be blocked. For example; lets say someone manages to inject some javascript into your super secure and robust comments system... That javascript is loading something malicious from another domain. As that domain is not in your CSP, it'll be blocked.
CSP is the hardest part to retrofit on an already existing website, if you're on a greenfield app I recommend starting with a relative strong CSP so that you can address issues as they arise.
This is the example I use on my site, I allow things like amazon s3 for my images, google fonts and google analytics. I apply the same policy to all sources but you can be much more fine grained if you want, I'd suggest having a read of this documentation.
add_header Content-Security-Policy "default-src 'self' wss: https://www.google-analytics.com https://fonts.gstatic.com https://fonts.googleapis.com https://s3-eu-west-1.amazonaws.com" always;
Content-Security-Policy: Inline and nonce
By default, CSP will block any inline scripts or css, this applies to both blocks and element attributes. You can disable this with 'unsafe-eval' and 'unsafe-inline', however there are other, better ways to deal with this, for example - using a nonce. This is enabled by adding the 'nonce-securekey' to your Content-Security-Policy header, and then ensuring that your inline blocks are decorated with the same 'nonce=securekey', in this example 'securekey'.
So my header would look like:
Content-Security-Policy: script-src 'self' 'nonce-securekey';
And my script block would look like:
<script nonce="securekey">...</script>
This enables the browser to confirm that the inline block is not malicious, and you have in essence you have provided some proof to the client that the script should be there.
In cryptography, a nonce is an arbitrary value that may only be used once. Therefore we should be using a sufficiently random value and that value should change for every request. If you go and do a curl against my domain 'karlstoney.com' a few times, you'll see this in action. Implementing it in Nginx was a little bit more difficult.
The route I ended up going down was to have Nginx generate this value using the set_secure_random_alphanum
function available in the set-misc nginx module, and then used sub_filter in the http_sub
module to effectively "find and replace" a static value from my upstream, with this random value on each request. I won't go into detail about how to compile nginx with these modules here, but you can see exactly how I do it by reviewing the Dockerfile in my container.
The nginx config is then relatively simple:
set_secure_random_alphanum $cspNonce 32;
add_header Content-Security-Policy "default-src 'self' wss: 'nonce-$cspNonce'" always;
sub_filter_once off;
sub_filter_types *;
sub_filter *CSP_NONCE* $cspNonce;
And my script block looks like this:
<script nonce="*CSP_NONCE*">...</script>
Nginx will automatically replace that value, with the random value on each request, thus implementing my nonce.
And that's it! A+. The most difficult part was the CSP, which is why I really recommend just having a default policy in place from day one, rather than trying to retrofit.
Another docker plug
Remember, as I said at the start of the post, you can get a bunch of this out of the box with my docker nginx reverse proxy, you can implement all of the above with a simple config.js that looks like this:
module.exports = {
karlstoney: {
fqdn: 'karlstoney.com',
redirectInsecure: true,
useHsts: true,
useCsp: true,
csp: "default-src 'self' wss: https://www.google-analytics.com https://fonts.gstatic.com https://fonts.googleapis.com https://s3-eu-west-1.amazonaws.com 'nonce-$cspNonce'",
default: true,
upstreams: {
root: 'app:2368'
},
paths: {
'/': 'root'
}
},
www: {
fqdn: 'www.karlstoney.com',
redirect: 'https://karlstoney.com'
}
};
And a docker-compose.yml file that looks like this:
version: '2'
services:
app:
image: stono/ghost:latest
restart: always
environment:
GHOST_URL: https://karlstoney.com
volumes:
- ./ghost:/data
nginx:
image: stono/docker-nginx-letsencrypt:1.11.10-12
restart: always
environment:
- [email protected]
- LETSENCRYPT=true
volumes:
- ./letsencrypt:/etc/letsencrypt
- ./config.js:/config/config.js
ports:
- 443:443
- 80:80
As always I welcome any feedback or changes, perhaps send me a pull request on Github?
Thanks
Karl