Skip to content

Instantly share code, notes, and snippets.

@plentz
Last active March 10, 2024 12:03
Star You must be signed in to star a gist
Save plentz/6737338 to your computer and use it in GitHub Desktop.
Best nginx configuration for improved security(and performance)
# to generate your dhparam.pem file, run in the terminal
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
# don't send the nginx version number in error pages and Server header
server_tokens off;
# config to don't allow the browser to render the page inside an frame or iframe
# and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking
# if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri
# https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
add_header X-Frame-Options SAMEORIGIN;
# when serving user-supplied content, include a X-Content-Type-Options: nosniff header along with the Content-Type: header,
# to disable content-type sniffing on some browsers.
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
# currently suppoorted in IE > 8 http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx
# http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
# 'soon' on Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=471020
add_header X-Content-Type-Options nosniff;
# This header enables the Cross-site scripting (XSS) filter built into most recent web browsers.
# It's usually enabled by default anyway, so the role of this header is to re-enable the filter for
# this particular website if it was disabled by the user.
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
add_header X-XSS-Protection "1; mode=block";
# with Content Security Policy (CSP) enabled(and a browser that supports it(http://caniuse.com/#feat=contentsecuritypolicy),
# you can tell the browser that it can only download content from the domains you explicitly allow
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
# https://www.owasp.org/index.php/Content_Security_Policy
# I need to change our application code so we can increase security by disabling 'unsafe-inline' 'unsafe-eval'
# directives for css and js(if you have inline css or js, you will need to keep it too).
# more: http://www.html5rocks.com/en/tutorials/security/content-security-policy/#inline-code-considered-harmful
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://ssl.google-analytics.com https://assets.zendesk.com https://connect.facebook.net; img-src 'self' https://ssl.google-analytics.com https://s-static.ak.facebook.com https://assets.zendesk.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://assets.zendesk.com; font-src 'self' https://themes.googleusercontent.com; frame-src https://assets.zendesk.com https://www.facebook.com https://s-static.ak.facebook.com https://tautt.zendesk.com; object-src 'none'";
# redirect all http traffic to https
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name .forgott.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name .forgott.com;
ssl_certificate /etc/nginx/ssl/star_forgott_com.crt;
ssl_certificate_key /etc/nginx/ssl/star_forgott_com.key;
# enable session resumption to improve https performance
# http://vincent.bernat.im/en/blog/2011-ssl-session-reuse-rfc5077.html
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# enables server-side protection from BEAST attacks
# http://blog.ivanristic.com/2013/09/is-beast-still-a-threat.html
ssl_prefer_server_ciphers on;
# disable SSLv3(enabled by default since nginx 0.8.19) since it's less secure then TLS http://en.wikipedia.org/wiki/Secure_Sockets_Layer#SSL_3.0
ssl_protocols TLSv1.2 TLSv1.3;
# ciphers chosen for forward secrecy and compatibility
# http://blog.ivanristic.com/2013/08/configuring-apache-nginx-and-openssl-for-forward-secrecy.html
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
# enable ocsp stapling (mechanism by which a site can convey certificate revocation information to visitors in a privacy-preserving, scalable manner)
# http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox/
resolver 8.8.8.8 8.8.4.4;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/star_forgott_com.crt;
# config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
# to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping
# also https://hstspreload.org/
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
# ... the rest of your configuration
}
@aptalca
Copy link

aptalca commented Feb 6, 2018

An nginx block only inherits add_header directives from the upper level if there are no add_header directives set in that block.

Once you add the add_header directive for the HSTS in the server block, it will ignore the four add_header directives you set in the http block above.

@colinmollenhour
Copy link

The ECDHE-RSA-CHACHA20-POLY1305 cipher is non-compliant with NIST and therefore HIPAA.

@AdrianLThomas
Copy link

A referrer policy should be added in to here too. Without this header, you will still get flagged up on securityheaders.com

https://scotthelme.co.uk/a-new-security-header-referrer-policy/

@neilwashere
Copy link

Thanks for making this available. Very useful resource 👍

@GaProgMan
Copy link

This is an absolutely amazing resource. Thank you for creating and sharing it 👍

@veeara282
Copy link

I second what @mhcerri said. It's a gotcha.

@jukbot
Copy link

jukbot commented Aug 17, 2018

Should add prime256v1 to ssl_ecdh_curve too, if you want to connect to app in Android <= 7

Ref: https://stackoverflow.com/questions/47232191/letsencrypt-does-not-work-on-android

@C0nw0nk
Copy link

C0nw0nk commented Sep 24, 2018

Should use a privacy respecting and worlds fastest DNS resolver instead of Google's 8.8.8.8 and 8.8.4.4 IPv4 resolver what is slow and Does not respect privacy or security. Cloudflares resolver also includes both IPv4 IPv6 resolver and alternative fallback DNS resolver alternative resolver for is for IPV4 is 1.0.0.1 and IPv6 2606:4700:4700::1001 as can be found on their website privacy security and speed comes first.

Here is my code for their resolver to support both IPv4 and IPv6 (Can disable IPv6 and will still work universal setup)

#Cloudflare resolver 1dot1dot1dot1.cloudflare-dns.com
resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];

https://1.1.1.1/#explanation
https://www.cloudflare.com/learning/dns/what-is-1.1.1.1/
https://blog.cloudflare.com/announcing-1111/

Fastest DNS resolvers in the world (1.1.1.1 is the fastest most secured and private unlike google)
https://www.dnsperf.com/#!dns-resolvers

@egberts
Copy link

egberts commented Oct 20, 2018

I like this portion better for 301 redirect from http to https, rather than a hardcoded URL value:

return 301 https://$host$request_uri;

@egberts
Copy link

egberts commented Oct 20, 2018

@C0nw0nk, Here is a list of Open DNS servers that can prove that my highly hardened personal website is a secured and “un-hackable”. Basically this test was done using the “dig” tool with ad notation of whether DNSSEC is supported (and a small notation if ca is supported too)
The command used is:

dig +dnssec mywebsiteredacted.net. @# and 'ad' flag appears as authenticated,# and note any absense of 'ca' flag

DNSSEC? IP Provider Name
YES 185.228.168.9 CleanBrowsing DNS
YES 185.228.169.9 CleanBrowsing DNS
YES 156.154.70.1 DNS Advantage
YES 156.154.71.1 DNS Advantage
YES 84.200.69.80 DNS Watch
YES 84.200.70.40 DNS Watch
YES 216.146.35.35 Dyn
YES 216.146.36.36 Dyn
YES 81.218.119.11 GreenTeamDNS
YES 74.82.42.42 HurricaneElectric
YES 156.154.70.1 NeuStar
YES 156.154.71.1 NeuStar
YES 9.9.9.9 Quad9
YES 149.112.112.112 Quad9
If you are using any of the DNS servers below this line, then when you visit your banking website, your transaction will not be safe:
YES 199.85.126.10 Norton ConnectSafe (discontinued)
YES 199.85.126.20 Norton ConnectSafe (discontinued)
YES 199.85.127.10 Norton ConnectSafe (discontinued)
no 1.0.0.1 Cloudflare
no 109.69.8.51 puntCAT
no 1.1.1.1 Cloudflare
no 176.103.130.130 AdGuard
no 176.103.130.131 AdGuard
no 176.103.130.132 AdGuard Family
no 176.103.130.134 AdGuard Family
no 195.46.39.39 SafeDNS
no 195.46.39.40 SafeDNS
no 198.101.242.72 AlternateDNS
no 209.244.0.3 Level 3
no 209.244.0.4 Level 3
no 23.253.163.53 AlternateDNS
no 37.235.1.174 FreeDNS
no 37.235.1.177 FreeDNS
no 77.88.8.1 YandexDNS
no 77.88.8.8 YandexDNS
no 8.8.4.4 Google DNS
no 8.8.8.8 Google DNS
no 89.233.43.71 UncensoredDNS
no 91.239.100.100 UncensoredDNS
no bind.odvr.dns-oarc.net DNS OARC
no NOcd 208.67.220.123 OPENDNS FamilyShield
no NOcd 208.67.220.220 OPENDNS
no NOcd 208.67.222.123 OPENDNS FamilyShield
no NOcd 208.67.222.222 OPENDNS
no NOcd 8.20.247.20 Comodo
no NOcd 8.26.56.26 Comodo

@jan-swiecki
Copy link

jan-swiecki commented Dec 20, 2018

For letsencrypt:

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
(...)
ssl_stapling on;
ssl_stapling_verify on;
#ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

Edit: changed ssl_certificate from cert.pem into fullchain.pem. This solves "B rating" on https://www.ssllabs.com. Also, according to https://community.letsencrypt.org/t/incomplete-chain-when-using-nginx-1-8-0-and-1-9-9/8500 ssl_trusted_certificate is not needed after we use fullchain.pem in ssl_certificate field. That's why I'm commenting out ssl_trusted_certificate field.

@NTMS2017
Copy link

Thanks for the config. I try to implement using nginx 1.16.0 stable.
My redirect point to proxy_pass. if user types " https://my.domain.com" or " https://my.domain.com/anything" I need to redirect.
Rest of it must show a error page. I reconfigure to point all the error page must shows the index.html.

What is wrong with my config (etc/nginx/conf.d/default.conf) file? I am keep getting nginx default 404. I can not override error 400 and 500 to points the my index.html file.

Any idea how to correct the issue.

server_tokens off;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";


# redirect all http traffic to https
server {
  charset UTF-8;
  listen 80 default_server;
  listen [::]:80 ipv6only=on default_server;
  server_name my.domain.com;

  # define error pages
  error_page 401 403 404  /index.html;
  location = /index.html {
      root /usr/share/nginx/html;
      internal;
  }

  # redirect server error pages
  error_page   500 502 503 504  /index.html;
  location = / {
      root   /usr/share/nginx/html;
      internal;
  }

  # deny access to .htaccess files
  location ~ /\.ht {
      deny  all;
  }

  # Deny access to hidden files (beginning with a period)
  location ~ /\. {
       deny all;
  }

  if ($host = my.domain.com) {
      return 301 https://my.domain.com$request_uri;
  }
  return 404;

}

server {
    charset UTF-8;
    listen 443;
    listen [::]:443 ipv6only=on; 
    server_name my.domain.com;

    error_log  /var/log/nginx/host.error.log;
    access_log  /var/log/nginx/host.access.log  main;

    # define error pages
    error_page 401 403 404  /index.html;
    location = /index.html {
        root /usr/share/nginx/html;
        internal;
    }

    # redirect server error pages
    error_page   500 502 503 504  /index.html;
    location = / {
        root   /usr/share/nginx/html;
        internal;
    }   
    location / {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass "http://127.0.0.1:8888/";
        try_files $uri $uri/ =404;        
    }

    # Only allow these request methods, Do not accept DELETE, SEARCH and other methods
     if ($request_method !~ ^(GET|HEAD|POST)$ ) {
         return 404;
     }
   
}

@dvershinin
Copy link

The X-Content-Type-Options nosniff should only be applied for Javascript and CSS files.
For anything else, it's just an extraneous useless HTTP header transferred with every request.

To avoid such pitfalls, use ngx_security_headers module which knows when to set respective header and also accounts for things like not sending headers during conditional GET requests.

security_headers on;

@luuquangvu
Copy link

Hi @plentz,
When use add_header on multiple blocks, nginx only get the last one and ignore all parents blocks. Let check again plz

@guptarohit
Copy link

I compiled few optimization hacks to increase requests/second.
Optimizations: Tuning Nginx for better RPS of an HTTP API
☮️ 🍰 ✨

@nsgoyat
Copy link

nsgoyat commented Dec 20, 2019

The X-Content-Type-Options nosniff should only be applied for Javascript and CSS files.
For anything else, it's just an extraneous useless HTTP header transferred with every request.

To avoid such pitfalls, use ngx_security_headers module which knows when to set respective header and also accounts for things like not sending headers during conditional GET requests.

security_headers on;

I see you currently have X-Content-Type-Options nosniff applied to your own website's HTML (index page) and all other assets.

@andreiavrammsd
Copy link

TLSv1.3 should be added: ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

@rooch84
Copy link

rooch84 commented Feb 25, 2020

TLSv1 & TLSv1.1 should be removed. Otherwise the HTTPS rating is capped at B. ssl_protocols TLSv1.2 TLSv1.3;

https://blog.qualys.com/ssllabs/2018/11/19/grade-change-for-tls-1-0-and-tls-1-1-protocols

@plentz
Copy link
Author

plentz commented Mar 1, 2020

@rooch84 thanks, I've updated it.

@bms8197
Copy link

bms8197 commented Mar 7, 2020

These are my settings for nginx with LetsEncrypt SSL for an A+ score on Qualys:

ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 5m;
ssl_buffer_size 1400;
ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
ssl_session_tickets off;

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_prefer_server_ciphers on;
ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL;

resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];
resolver_timeout 5s;
ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

For dhparam.pem I've used this:
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096

I would suggest installing haveged and have it running prior of dhparam.pem generation (otherwise it would take quite some long time)

@dohlin
Copy link

dohlin commented Mar 16, 2020

These are my settings for nginx with LetsEncrypt SSL for an A+ score on Qualys:

ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 5m;
ssl_buffer_size 1400;
ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
ssl_session_tickets off;

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_prefer_server_ciphers on;
ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL;

resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];
resolver_timeout 5s;
ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

For dhparam.pem I've used this:
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096

I would suggest installing haveged and have it running prior of dhparam.pem generation (otherwise it would take quite some long time)

Thanks for this! Very useful indeed!

@hongbo-miao
Copy link

hongbo-miao commented Sep 12, 2020

Here is HTTP/3 support
https://www.nginx.com/blog/introducing-technology-preview-nginx-support-for-quic-http-3/

server {
    listen 443 ssl;              # TCP listener for HTTP/1.1
    listen 443 http3 reuseport;  # UDP listener for QUIC+HTTP/3

    ssl_protocols       TLSv1.3; # QUIC requires TLS 1.3
    ssl_certificate     ssl/www.example.com.crt;
    ssl_certificate_key ssl/www.example.com.key;

    add_header Alt-Svc 'quic=":443"'; # Advertise that QUIC is available
    add_header QUIC-Status $quic;     # Sent when QUIC was used
}

@rfl890
Copy link

rfl890 commented Jan 17, 2022

You should probably change the resolver to
1.1.1.1
1.0.0.1
Which are Cloudflare's DNS servers, which are privacy focused and faster than Google

@rooch84
Copy link

rooch84 commented Jan 17, 2022

@rfl890 do you have any supporting evidence? I don't dispute your claims, but I think changes should be made objectively.

@rfl890
Copy link

rfl890 commented Jan 24, 2022

@rooch84 Well if you go to their website (https://1.1.1.1/) and scroll down a bit, it shows a comparison between DNS resolvers and it shows that it is indeed the fastest. They might be lying, but what reason would they have? You don't pay for using the DNS itself, it's free. And in the privacy policy (https://www.cloudflare.com/application/privacypolicy/), in section 2, they specifically say they only collect the minimum amount of data needed and all data is anonymized. I'm not trying to "root for them" or anything, but well, you asked for it

@Peneheals
Copy link

@rooch84 You can check the relevant data about latency/speed here.

And a privacy-focused report, made by a 3rd party audit company (KPMG) here (bottom mid section leads to this pdf).

@rooch84
Copy link

rooch84 commented Feb 2, 2022

Thanks @rfl890 and @Peneheals for the info. Looks like a sensible change to me.

@arch-user-france1
Copy link

Getting an unknown variable "quic". Http/3 works on Chromium but not Firefox, what do I have to do?

@rlSimonLi
Copy link

@Wiilf
Copy link

Wiilf commented Apr 15, 2023

EECDH+AESGCM:EDH+AESGCM:AES128+EECDH:AES128+EDH

This prioritizes cipher suites that support PFS and use ECC, while also using 128-bit encryption for improved performance on older devices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment