Infrastructure Series -- HTTP(S) Security Headers! You should use them! [NGINX]

table of contents

Introduction

Obligatory shill of blog stream post: Phaselockedloopable- PLL’s continued exploration of networking, self-hosting and decoupling from big tech

As always check for updates in the second post!

So we’re all familiar with what a reverse proxy is I’m not really going to get too much into that however, most people just set one up and forget it and configure their confs and don’t really care about the security of their reverse proxy save enabling HTTPS and a HTTP redirect. Well additionally we have headers we can configure after the steps detailed in my previous instantiation here:

Infrastructure Series -- NGINX Reverse Proxy and Hardening SSL

Disclaimer 1:

This thread is dedicated to showing that im lazy is the worst excuse for being insecure. Its unacceptable. Im not an expert but this is low hanging fruit.

Disclaimer 2:

I have a strong disdain for web developers who do not provide a base CSP template and permissions template when they create NGINX or other proxy forwards. Seriously you develop the software you should have a base template since you should know BETTER than anyone. Its unacceptable IMHO. Especially when you have entire sets of hardening documentation.

Turning off Server Version Reporting:

NGINX:
server_tokens off; # This will turn off the version by default
(you can set this per block or globally in the master conf http block)
NGINX/fastcgi:
In each config remove anything with /*$version* of some kind
NGINX/scgi:
In each config remove anything with /*$version* of some kind
NGINX/uwsgi:
In each config remove anything with /*$version* of some kind

Security Headers

What are security headers?

So basically, HTTP security headers are a subset of HTTP headers (since every page has a header) and are exchanged between a client and a server to specify the security-related details of the connection. Some HTTP headers that are indirectly related to privacy and security can also be considered HTTP security headers like one I detail below about clearing content once connection is terminated. By enabling suitable security headers in web applications and web server settings, you can improve the resilience of your services against many common attacks.

Why do we care?

So HTTP security headers will provide us an extra layer of security by restricting behaviors that the browser or other connection mechanisms and the server allow once the web application is running at the user end or active in a connection. In many cases, implementing the right headers is a crucial to the security of a service. We know that the big parlor hack came from the API being wide open. How about not becoming the next example of pure laziness?

We want to see this:



:wink:easter-egg:wink:

Headers that I have implemented and so should you:

Server Response Header:
This Server header seems to advertise the software being run on the server but you can remove or change this value.

X-Frame-Options:
X-Frame-Options allows content publishers to prevent their own content from being used in an invisible frame by attackers. The DENY option is the most secure, preventing any use of the current page in a frame. More commonly, SAMEORIGIN is used, as it does enable the use of frames, but limits them to the current domain. X-Frame-Options tells the browser whether you want to allow your site to be framed or not. By preventing a browser from framing your site you can defend against attacks like clickjacking.

I have set this on most of my pages via: add_header X-Frame-Options DENY;

X-Content-Type-Options:
X-Content-Type-Options stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type. The only valid value for this header is “X-Content-Type-Options: nosniff”. This header was introduced by Microsoft in IE 8 as a way for webmasters to block content sniffing that was happening and could transform non-executable MIME types into executable MIME types. Since then, other browsers have introduced it, even if their MIME sniffing algorithms were less aggressive. More information: html - What is "X-Content-Type-Options=nosniff"? - Stack Overflow

I have set this on most of my pages via: add_header X-Content-Type-Options nosniff;

Referrer-Policy:
Referrer Policy is a newer header that allows a site to control how much information the browser includes with navigations away from a document and should be set by all sites. Referrer-Policy is a security header that should be included on communication from your website’s server to a client. The Referrer-Policy tells the web-browser how to handle referrer information that is sent to websites when a user clicks a link that leads to another page or website.

I have set this on most of my pages via: add_header Referrer-Policy "strict-origin";

X-XSS-Protection:
X-XSS-Protection sets the configuration for the XSS Auditor built into older browsers. The recommended value was “X-XSS-Protection: 1; mode=block”. X-XSS-Protection is a HTTP header understood by Internet Explorer 8 (and newer versions). This header lets domains toggle on and off the “XSS Filter” of IE8, which prevents some categories of XSS attacks. IE8 has the filter activated by default, but servers can switch if off by setting. Ideally we have moved onto the CSP (content security policy)

I have set this on most of my pages via: add_header X-XSS-Protection "1; mode=block";

Permissions-Policy:
Permissions Policy is a new header that allows a site to control which features and APIs can be used in the browser. There is a REALLY good write up on github here. webappsec-permissions-policy/permissions-policy-explainer.md at main · w3c/webappsec-permissions-policy · GitHub . This eventually will move onto Feature-Policy. I have not yet moved.

This block changes per service you host:

I have set this on most of my pages via: add_header Permissions-Policy "autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()";
(I havent needed more than that and none of my pages need much more than that so its a safe setting)

Strict-Transport-Security:
HTTP Strict Transport Security is an excellent feature to support on your site and strengthens your implementation of TLS by getting the User Agent to enforce the use of HTTPS. HTTP Strict Transport Security is a policy mechanism that helps to protect websites against man-in-the-middle attacks such as protocol downgrade attacks and cookie hijacking. I have it set to 3 years. A sufficiently long time.

I have set this on most of my pages via: add_header Strict-Transport-Security "max-age=94608000; includeSubDomains; preload" always;
(Dont set this when copying security headers to port 80)

Content-Security-Policy:
Content Security Policy is an effective measure to protect your site from XSS attacks. By whitelisting sources of approved content, you can prevent the browser from loading malicious assets. Content Security Policy became a standard introduced to prevent cross-site scripting, clickjacking and other code injection attacks resulting from execution of malicious content in the trusted web page context. There is a good cheat sheat on this website: CSP Cheat Sheet

This block changes per service you host. It is potentially the most difficult and requires you understand each and every service you host because it does not universally apply without serious breakage.

I have set this on most of my pages via: add_header Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self' data:;" always;

Clear-Site-Data:
When we use a webpage, we can leave various pieces of data in the browser that we’d like to clear out if the user logs out or deletes their account. Clear Site Data gives us a reliable way to do that. Here is the RFC if you want more details Clear Site Data . I decided to enable it globally on all pages via:
add_header Clear-Site-Data "*";

Newer Headers I have not set: (You will face compatibility breakages)

Expect-CT:
Expect-CT allows a site to determine if they are ready for the upcoming Chrome requirements and/or enforce their CT policy. The HTTP Expect-CT header is a response-type header that prevents the usage of wrongly issued certificates for a site and makes sure that they do not go unnoticed and it also allows sites to decide on reporting or enforcement of Certificate Transparency requirements.

Not supported in NGINX 1.18 (my version). Header might eventually look like add_header Expect-CT "$options";
Plan to implement: Yes [As soon as I get to version 1.19]

Cross-Origin-Embedder-Policy:
Cross-Origin Embedder Policy allows a site to prevent assets being loaded that do not grant permission to load them via CORS or CORP.
More information: Cross-Origin-Embedder-Policy - HTTP | MDN

Not supported in NGINX 1.18 (my version). Header might eventually look like add_header Cross-Origin-Embedder-Policy "$options";
Plan to implement: Yes [As soon as I get to version 1.19]

Cross-Origin-Opener-Policy:
Cross-Origin Opener Policy allows a site to opt-in to Cross-Origin Isolation in the browser.
More information: Why you need "cross-origin isolated" for powerful features

Not supported in NGINX 1.18 (my version). Header might eventually look like add_header Cross-Origin-Opener-Policy "$options";
Plan to implement: Yes [As soon as I get to version 1.19]

Cross-Origin-Resource-Policy:
Cross-Origin Resource Policy allows a resource owner to specify who can load the resource.
More information: Cross-Origin Resource Sharing (CORS) - HTTP | MDN and Cross-Origin Resource Policy (CORP) - HTTP | MDN

Not supported in NGINX 1.18 (my version). Header might eventually look like add_header Cross-Origin-Resource-Policy "$options";
Plan to implement: Yes [As soon as I get to version 1.19]

80 (Insecure Server Listen Block)

(these are what I start with if I have no template)
Security Headers: (OMIT HSTS)

add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy "strict-origin";
add_header X-XSS-Protection "1; mode=block";
add_header Permissions-Policy "autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()";
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self' data:;" always;

443 (Secure Server Listen Block)

Security Headers:

add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy "strict-origin";
add_header X-XSS-Protection "1; mode=block";
add_header Permissions-Policy "autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()";
add_header Strict-Transport-Security "max-age=94608000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self' data:;" always;

Testing

You can use curl to see your redirects and headers.

Execute the command: curl -I -L < MY-TLD >.net
My output is as expected

HTTP/2 301 Moved Permanently
Server: nginx
Date: Mon, 26 Apr 2021 03:55:28 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://< MY-TLD >.net/
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin
X-XSS-Protection: 1; mode=block
Permissions-Policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self' data:;

HTTP/2 301 
server: nginx
date: Mon, 26 Apr 2021 03:55:29 GMT
content-type: text/html
content-length: 162
location: https://services.< MY-TLD >.net/
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin
x-xss-protection: 1; mode=block
permissions-policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
strict-transport-security: max-age=94608000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self' data:;

HTTP/2 200 
server: nginx
date: Mon, 26 Apr 2021 03:55:29 GMT
content-type: text/html
content-length: 3037
last-modified: Tue, 09 Mar 2021 08:32:26 GMT
etag: "6047329a-bdd"
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin
x-xss-protection: 1; mode=block
permissions-policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
strict-transport-security: max-age=94608000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; font-src 'self' data:;
accept-ranges: bytes

Score:

Now we can see the headers apply universally and the server tokens are removed from all requests. Now stuff is sufficiently hardened

I have noted a couple difficult pieces of software to write them for below. This is what I have come up with. I am open to suggestions on them. Also if anyone can write one for KIWIX I am open to that as well.

Difficult Subdomains

Cloud Server (Nextcloud)

CSP Line (YOUR WELCOME)

add_header Content-Security-Policy "default-src 'none'; connect-src https:; font-src 'self' data:; frame-src 'self' https://cloud.< MY-TLD >.net; img-src data: http: https:; media-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self';" always;

Score

Curl Response

HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Mon, 26 Apr 2021 07:49:44 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://cloud.< MY-TLD >.net/
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin
X-XSS-Protection: 1; mode=block
Permissions-Policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
Content-Security-Policy: default-src 'none'; connect-src https:; font-src 'self' data:; frame-src 'self' https://cloud.< MY-TLD >.net; img-src data: http: https:; media-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self';

HTTP/2 302 
server: nginx
date: Mon, 26 Apr 2021 07:49:45 GMT
content-type: text/html; charset=UTF-8
referrer-policy: no-referrer
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-robots-tag: none
x-xss-protection: 1; mode=block
x-powered-by: PHP/7.4.16
set-cookie: ocowvairggqw=69f79da228801655377387ff3b78690b; path=/; secure; HttpOnly; SameSite=Lax
expires: Thu, 19 Nov 1981 08:52:00 GMT
cache-control: no-store, no-cache, must-revalidate
pragma: no-cache
set-cookie: oc_sessionPassphrase=R2x0ujg2FBYHumEaVGiWyACf%2F95JwZ1cYb460fstMr%2Bn4Dv6ENaqWL4oYy6dm6EP0clUzFJbl792gSaHM6LiJmQ9qoPdWSh%2FuJWXSGwRNnaHpxHtj7G8UWQROnuEwMPj; path=/; secure; HttpOnly; SameSite=Lax
set-cookie: ocowvairggqw=fd2c6d331248ce51f1de7d8d1b8259c1; path=/; secure; HttpOnly; SameSite=Lax
content-security-policy: default-src 'self'; script-src 'self' 'nonce-dHZiMGEwblROOWdBTzhrNExFd3hPZWhxc3VIcThKUlhJbnZnMjNUdkJiUT06MlorL0p6bTZjcEZ5U3FKQ1N3VmJmWkk4M3EraWsvWWFTRU9VNjF2WVJObz0='; style-src 'self' 'unsafe-inline'; frame-src *; img-src * data: blob:; font-src 'self' data:; media-src *; connect-src *; object-src 'none'; base-uri 'self';
set-cookie: __Host-nc_sameSiteCookielax=true; path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax
set-cookie: __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=strict
location: https://cloud.< MY-TLD >.net/login
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin
x-xss-protection: 1; mode=block
permissions-policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
strict-transport-security: max-age=94608000; includeSubDomains; preload
content-security-policy: default-src 'none'; connect-src https:; font-src 'self' data:; frame-src 'self' https://cloud.< MY-TLD >.net; img-src data: http: https:; media-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self';

HTTP/2 200 
server: nginx
date: Mon, 26 Apr 2021 07:49:45 GMT
content-type: text/html; charset=UTF-8
content-length: 12136
referrer-policy: no-referrer
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-robots-tag: none
x-xss-protection: 1; mode=block
x-powered-by: PHP/7.4.16
set-cookie: ocowvairggqw=3a76027b12f0bfa95f4d9182163f9cff; path=/; secure; HttpOnly; SameSite=Lax
expires: Thu, 19 Nov 1981 08:52:00 GMT
cache-control: no-cache, no-store, must-revalidate
pragma: no-cache
set-cookie: oc_sessionPassphrase=ZBrHXazKEW3ukY4mu3AFLFQ3i2LstJxMQ81DgSyaBCnsBLht9YDLY8akbwnSmg0%2Fi59dN%2FImIaMq58sJyPVOMWPRY253%2BuSr%2BHRvOw2x2kujW%2B2xTq2wL5Kq%2F7mJA2yR; path=/; secure; HttpOnly; SameSite=Lax
set-cookie: ocowvairggqw=c0159a7f4d1cae729218f77beffdc0ec; path=/; secure; HttpOnly; SameSite=Lax
content-security-policy: default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: *;font-src 'self' data:;connect-src 'self' stun.nextcloud.com:443;media-src 'self';frame-src data: prezi.com player.vimeo.com vine.co www.youtube.com 'self';child-src 'self';frame-ancestors 'self';worker-src 'self' blob:;form-action 'self'
set-cookie: __Host-nc_sameSiteCookielax=true; path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax
set-cookie: __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=strict
feature-policy: autoplay 'self';camera 'self';fullscreen 'self';geolocation 'self';microphone 'self';payment 'none'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin
x-xss-protection: 1; mode=block
permissions-policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
strict-transport-security: max-age=94608000; includeSubDomains; preload
content-security-policy: default-src 'none'; connect-src https:; font-src 'self' data:; frame-src 'self' https://cloud.< MY-TLD >.net; img-src data: http: https:; media-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self';

Media/Streaming Server (Jellyfin)

CSP Line

add_header Content-Security-Policy "default-src https: data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'";

Score

Curl Response

HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Mon, 26 Apr 2021 07:50:19 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://jelly.< MY-TLD >.net/
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1;mode=block
Referrer-Policy: no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
Permissions-Policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
Content-Security-Policy: default-src https: data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'

HTTP/2 302 
server: nginx
date: Mon, 26 Apr 2021 07:50:19 GMT
content-type: text/html
content-length: 138
location: https://jelly.< MY-TLD >.net/web/
x-frame-options: SAMEORIGIN
x-xss-protection: 1;mode=block
referrer-policy: no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
x-content-type-options: nosniff
permissions-policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
strict-transport-security: max-age=94608000; includeSubDomains; preload
content-security-policy: default-src https: data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'

HTTP/2 200 
server: nginx
date: Mon, 26 Apr 2021 07:50:19 GMT
content-type: text/html
content-length: 6868
last-modified: Mon, 15 Mar 2021 17:15:25 GMT
accept-ranges: bytes
etag: "1d719bec9bfee54"
x-response-time-ms: 0
x-frame-options: SAMEORIGIN
x-xss-protection: 1;mode=block
referrer-policy: no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
x-content-type-options: nosniff
permissions-policy: autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()
strict-transport-security: max-age=94608000; includeSubDomains; preload
content-security-policy: default-src https: data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'

Conclusion

Your mileage may vary. This thread will be continuously updated as new headers evolve and as I test new things. I may from time to time update what specific pages of mine have CSP changes etc. CSP changes will be your largest source of maintenance. Feel free to comment your experiences on tackling this challenge.

Links to Infrastructure Series and Other Resources

Blog: Phaselockedloopable- PLL’s continued exploration of networking, self-hosting and decoupling from big tech

Phaselockedloopable- PLL’s continued exploration of networking, self-hosting and decoupling from big tech

Series 1: Native Dual Stack IP4+IP6

Infrastructure Series – Native Dual Stack IP4+IP6

Series 2: Wireguard Site to Site Tunnel

Infrastructure Series – Wireguard Site to Site Tunnel

Series 3: Recursive DNS and Adblocking DNS over TLS w/NGINX

Infrastructure Series – Recursive DNS and Adblocking DNS over TLS w/NGINX

Series 4: NGINX Reverse Proxy and Hardening SSL

Infrastructure Series – NGINX Reverse Proxy and Hardening SSL

Series 5: Taking DNS One Step Further - Full DNS Server infrastructure

Infrastructure Series – Taking DNS One Step Further - Full DNS Server infrastructure

Series 6: HTTP(S) Security Headers! You should use them!

Infrastructure Series – HTTP(S) Security Headers! You should use them! [NGINX]

Series 7: Use NGINX to inject CSS themes

Infrastructure Series – Use NGINX to inject CSS themes

ONE KEY TO RULE THEM ALL

Setting up a YubiKey Properly – One Key to rule them ALL!

Series 9: Infrastructure Series: BIND9 Authoritative DNS Guide “Please See Me Edition”

Infrastructure Series: BIND9 Authoritative DNS Guide “Please See Me Edition”

Buy me a crypto-beer

If you found this guide helpful you can donate Monero or Bitcoin to me at the following address in my User Card Profile

7 Likes

Reserved

2 Likes

(Reserved for updating CSPs and differences that happen over time)

2 Likes

Awesome.
Some notes, not a critique in any way:
I’m not sure if it’s useful to hide version information in headers. Not admitting that you’re running outdated software is not the same as not running outdated software.
While the simplest vulnerability scanners will be fooled by this, most won’t.
An advantage to having this kind of reporting public is that the “good guys” can just look up all web servers of the specific versions when a problem arises, and estimate the impact of a vulnerability.

Also wouldn’t setting Clear-Site-Data on every request mean that you effectively can’t use cookies? IMHO it only makes sense on logout pages etc.

2 Likes

To my understanding its only called when the session is fully terminated. It seems to act this way. We will see how browsers implement their response to it in the years coming

True and I don’t run outdated but I just have it in there for the bots to only see nginx. I wish there was a way to spoof the “nginx”

I have no idea why I didn’t see your reply. I stumbled back in sorry for the late response.