Security minded developer available for hire.

Secure NGINX Webserver


This post is not exhaustive and focuses on providing you with the terminology necessary to learn more. You will still need to configure these to suit your web server specifically.

Before you begin

This post is specific to Nginx, but many of the principles can be applied to apache or other popular web servers with a few tweaks to the configuration statements.

To compare your security settings before and after, Mozilla will scan your website and give you a grade for your level of security at You can also install wapiti to scan your site for vulnerabilities outside of the scope of this post.

View the report for at

Important files:

  • Nginx main config file
    • /etc/nginx/nginx.conf
  • Diffie-Hellman key file
    • You can chose where to put this, but I recommend here for organization.
    • /etc/nginx/cert/
  • PHP configuration file
    • /etc/php.ini (default)


SSL/TLS could probably merit an entire post, so I’ll try to keep it sort. LetsEncrypt will do basically everything for free, and all you gotta do is run the installer. Here’s how: download certbot for your setup at, run it, and you can now use HTTPS.

From here I recommend upgrading your Diffie-Hellman key. Default key lengths are quite short, so we need to generate a new one. You will need Git if you have not already installed it.

Create the folder /etc/nginx/cert/ if it does not already exist, then run the following command. This could take a while. It took a full episode of Your Lie in April (~20 minutes) for me before it finally finished.

openssl dhparam 4096 -out /etc/nginx/cert/dhparam.pem

When it finishes, add this to your nginx config file right around where the ssl options added by certbot appear.

server {
    ssl_dhparam /etc/nginx/cert/dhparam.pem;

Last but not least, if yo do not see the following in the config file, add it to ensure that your server prefers to use at least generally acceptable ciphers.

http {
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

Content Security Policy (CSP)

This will be the hardest section.

Content security policy is one of the most important aspects of securing your visitors aside from SSL/TLS, but is not supported by all browsers. The CSP itself is sent via HTTP headers. It limits what a browser will display by whitelisting or blacklisting specific content from third party sites. For example, if any external scripts run on your site e.g. Google analytics.

The command to add your own strict content security policy is

    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; img-src 'self'; style-src 'self'; font-src 'self'; frame-src 'none'; object-src 'none'; form-src 'self'";

This command, as written will disable nearly all content from external sites. In order to relax this a bit we need to look at each directive. It should be added outside of any sections to apply it to the whole server, as opposed to a subset.


Each directive is added to the list separated by semicolons in the header. Notable values are ‘none’, ‘self’, and ‘unsafe-inline’. Using ‘none’ will disallow all sources of the content controlled by the directive. Using ‘self’ is just a shorter version of typing your own url, so for this site it would translate to ‘’, and would allow content from myself. Last but not least, ‘unsafe-inline’ is used mainly in script-src and style-src allowing scripts or css written in line with HTML. I do not recommend using it because it is quite easy to add lines to HTML in transit, meaning that your visitors may execute code injected by a man in the middle.

This table is an overview of common directives. An extended list can be found at

Directive Usage Description
script-src 'self' Determines allowed sources of client side scripts.
default-src 'none' This directive is the default for all loaded content: JavaScript, css, fonts, frames, ...etc. Most directives, if not manually set, will use this value. As such, the most secure option is to use 'none' and override the default by allowing other settings.
style-src 'self' Determines allowed sources of CSS.
img-src 'self' Determines allowed sources of images.
form-src 'self' Determines allowed sources of HTML forms. Usually this is just self.
frame-src 'self' Determines allowed sources of iframes. This includes things like embedded youtube videos.

Example to add Paypal donate button to website with strict content security policy

These are the only directives needed to add a PayPal donate button to your site. Using a strict content security policy, browsers will not display the donate button unless you add PayPal as an acceptable source for images and forms.

    img-src 'self';

    form-action 'self';

Redirect traffic to HTTPS

Strict Transport Security (HSTS)

HSTS tells the client that they should use HTTPS, and remember it for the future.

It’s just an extra line in the config.

    add_header Strict-Transport-Security "max-age=31536000" always;

Max age is the time that the browser should remember that it should use HTTPS on your site. You can append ‘includeSubDomains’ to well… include sub domains.

Double check to be sure the redirect is working

Having the certificates and setup for HTTPS is great and all, but if no one uses it, then it is worthless.

This one is pretty easy. Just add this line that checks for http connections and redirects them to a corresponding https url.

if ($scheme = http) { return 301 https://$host$request_uri; }

Using this method is effective, but as of writing this, Mozilla’s observatory does not support detection for this method.

Prevent Information Disclosure

Server information

If you check the headers on a default install of nginx, you will find that it likes to give visitors a lot of information about your server that they don’t need. You can check the headers using curl -I http://localhost. Look for the ‘Server’ header and look at the information sent. It should look something like this.

$ curl -I http://localhost
HTTP/1.1 200 OK

Server: nginx/1.13.10 (Ubuntu)

Knowing the version number and operating system gives attackers a lot of information about your system for free. I’m not super into that so let’s turn it off. Add this line to an http block in your config file.

http {
    server_tokens off;

PHP Information

Oh boy here we go again. If you’ve got PHP, you’re probably giving out info for free. PHP wants everyone to know what version it is. Now we’re back to checking out your headers. Again, the header we’re looking for will look similar to this.

$ curl -I http://localhost
HTTP/1.1 200 OK

X-Powered-By: PHP/5.4.0

To prevent this we need to change some PHP settings in /etc/php.ini. Inside the file there is a line that will say expose_php, change it to off.

expose_php = Off

Clickjack prevention

The X-Frame-Options header can be set to inform a browser what content will be allowed to render a page in a frame or object. This prevents third parties from embedding their content in your site.

There are three options for this header

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM

  • DENY obviously denies all pages.
  • SAMEORIGIN will allow pages from the same origin as the current site. Different browsers interpret origin to mean one of the following: top level, parent, or the whole chain.
  • ALLOW-FROM will allow a specific site to embed content. A common use for this is to add YouTube as a valid source to show YouTube videos on your site.

The command to actually add this to your server is

server {
    add_header X-Frame-Options SAMEORIGIN always;

Content Type Check

If you are a lemonade salesman, and people ask you for steak, that’s gonna confuse you. The same goes for web servers and MIME type. The X-Content-Type-Options header with the nosniff option will block a request unless the type is “style” or “script” and the MIME type does not match “text/css” or a JavaScript mime type.

Again, this one is pretty simple to add.

server {
    X-Content-Type-Options nosniff always;

Detect reflected cross site scripting attacks

If you disable in line JavaScript via content security policy, then this header is only useful on browsers that do not support content security policies. The X-XSS-Protection header tells the browser to stop loading a page if it detects reflected cross site scripting. Reflected cross site scripting is when an attacker injects code into a request. It is not saved on the server and will only affect users that request it. An example would be posting JavaScript code in a forum post that does not sanitize user input.

You can see all the options for this header at Otherwise, just add this.

server {
    add_header X-XSS-Protection "1; mode=block" always;


There is much more to do, but this is a solid foundation. If you have any questions or corrections, send them to