Adding HTTP Security Headers to a Hugo Site on Netlify

Hardening the security headers to defend against cross-site scripting and clickjacking

* This page contains promotional content

Three years are about to pass since this site moved to Hugo.

I configured the HTTP security headers for the site, which had been left alone for a while.

They are directives used by web applications to configure security measures in the web browser, and they are used to defend against things like cross-site scripting and clickjacking

This article covers the configuration for the Hugo + Netlify environment that this site uses
Also, I will leave out explanations of the configuration terminology

Configuring it the usual way, such as with .htaccess

Normally, in an environment where .htaccess can be used, such as under Apache, adding settings like the ones below should be enough
This does assume that the mod_headers module is enabled

# Security Headers
Header always set Content-Security-Policy "upgrade-insecure-requests"
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy: "no-referrer-when-downgrade"
Header always set Expect-CT "max-age=7776000, enforce"
Header always append X-Frame-Options "SAMEORIGIN"
Header always set Permissions-Policy "geolocation=(); midi=();notifications=();push=();sync-xhr=();accelerometer=(); gyroscope=(); magnetometer=(); payment=(); camera=(); microphone=();usb=(); xr=();speaker=(self);vibrate=();fullscreen=(self);" 
Header always set Strict-Transport-Security: "max-age=31536000"
# End Security Headers
 

Deliberately putting a blank line on the last line is what matters!
This configuration also takes effect on a WordPress server just by appending it

Note that .htaccess takes effect per directory or through inheritance, so to apply it to the whole system you configure it in httpd.conf on Apache or default.conf on Nginx

Security headers with Hugo + Netlify

Now, in the Hugo and Netlify environment of this site, Netlify cannot use .htaccess, so a bit of ingenuity was needed

_headers

Create the file /static/_headers with the content below
Change the settings to match your own environment

/**
Content-Security-Policy = "upgrade-insecure-requests"
Expect-CT = "max-age=7776000, enforce"
Referrer-Policy = "strict-origin-when-cross-origin"
Strict-Transport-Security = "max-age=31536000"
X-Content-Type-Options = "nosniff"
X-Frame-Options = "SAMEORIGIN"
X-XSS-Protection = "1; mode=block"
Permissions-Policy = "geolocation=(); midi=();notifications=();push=();sync-xhr=();accelerometer=(); gyroscope=(); magnetometer=(); payment=(); camera=(); microphone=();usb=(); xr=();speaker=(self);vibrate=();fullscreen=(self);"

netlify.toml

Enter the contents of the _headers above into Netlify Playground and press the Test rules button; it checks the syntax and shows the Netlify format at the bottom, so add that content to netlify.toml

Add the following to netlify.toml

[[headers]]
for = "/**"
[headers.values]
Content-Security-Policy = "upgrade-insecure-requests"
Expect-CT = "max-age=7776000, enforce"
Permissions-Policy = "geolocation=(); midi=();notifications=();push=();sync-xhr=();accelerometer=(); gyroscope=(); magnetometer=(); payment=(); camera=(); microphone=();usb=(); xr=();speaker=(self);vibrate=();fullscreen=(self);"
Referrer-Policy = "strict-origin-when-cross-origin"
Strict-Transport-Security = "max-age=31536000"
X-Content-Type-Options = "nosniff"
X-Frame-Options = "SAMEORIGIN"
X-XSS-Protection = "1; mode=block"

Then, if you have it connected to Gitlab or Github, update the repository and deploy

Checking

It is easy to check on this site

If you are a curl user, curl --head works too

  • Before the security header work the rating was D

  • After the security header work it became A+, which confirmed that the measures were in place

Reference articles

See also