Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Best nginx configuration for improved security(and performance). Complete blog post here
# to generate your dhparam.pem file, run in the terminal
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
# read more here
# 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
# if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri
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.
# currently suppoorted in IE > 8
# 'soon' on Firefox
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.
add_header X-XSS-Protection "1; mode=block";
# with Content Security Policy (CSP) enabled(and a browser that supports it(,
# you can tell the browser that it can only download content from the domains you explicitly allow
# 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:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src; object-src 'none'";
# redirect all http traffic to https
server {
listen 80 default_server;
listen [::]:80 default_server;
return 301 https://$host$request_uri;
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
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
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
ssl_prefer_server_ciphers on;
# disable SSLv3(enabled by default since nginx 0.8.19) since it's less secure then TLS
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ciphers chosen for forward secrecy and compatibility
# enable ocsp stapling (mechanism by which a site can convey certificate revocation information to visitors in a privacy-preserving, scalable manner)
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)
# to avoid ssl stripping
# also
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
# ... the rest of your configuration

The Strict-Transport-Security header needs to be moved inside the http block with the ssl listen statement or you risk sending Strict-Transport-Security headers over HTTP sites you may also have configured on the server.

Additionally, the rewrite for the http server block should be a return 301 instead. See this Nginx documentation for more details:


plentz commented Jan 11, 2014

@ryankearney updated, thanks :)

ssl_session_timeout 5m; <- Should this appear twice in the config?

tzuryby commented Feb 12, 2014

@barrybingo that is probably a typo. once is just enough.

As you're already redirecting all HTTP traffic to HTTPS, have you looked into using SPDY to increase performance?

nginx 1.5.10 (mainline) has SPDY 3.1 support.

Also if you're running nginx as a proxy have you considered turning off the X-Powered-By header by adding the following line to the server config section.

proxy_hide_header X-Powered-By;

How to force redirect all request to www. version?
Here's my current config:

server {
return 301$request_uri;

server {
listen 443 ssl spdy;
server_name; to = Works to = Works to = Not working


plentz commented Feb 20, 2014

@barrybingo not really, thanks!

nice work

RoldanLT to = Works to = Works to = Works
server {
        listen 80;
        rewrite ^$request_uri? permanent;
server {
        listen 443 ssl spdy;
        rewrite ^$request_uri? permanent;

        ssl on;
server {
        listen 443 ssl spdy;

Thanks vincentclee!
I'm using this now and it works :)

server {
listen 80;
return 301$request_uri;

server {
listen 443 ssl spdy;
return 301$request_uri;

    ssl on;
ssl_certificate /ssl-unified.crt;
ssl_certificate_key /net.key;


server {
listen 443 ssl spdy;

The browser will only listen to the Strict-Transport-Security header if the connection was established via HTTPS. The first time the visitor connects to the website using HTTP, the visitor needs to be redirected using a 301 redirect. On subsequent requests, even it the connection uses HTTP, HSTS forces the browser to use HTTPS for connecting to a particular website instead of HTTP. Even if a user types in a http:// URL, the browser will automatically correct the URL and will connect to https://.

It appears the more correct way to implement HSTS in Nginx is as so:

map $scheme $hsts_header {
    https   max-age=31536000;

server {
    listen  80;
    listen  443 ssl;

    add_header Strict-Transport-Security $hsts_header;

Most examples use the following to implement HSTS, which does not follow the RFC.

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

bbigras commented Sep 18, 2014

@toddlahman : Shouldn't we avoid to set the header for http (port 80) ?

An HSTS Host MUST NOT include the STS header field in HTTP responses conveyed over non-secure transport.

tisdall commented Oct 15, 2014

In nginx >= 1.7.5 you should have (note the added "always"):

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

This forces the header to always be added regardless of the http code which is what is wanted for this type of header.

wyrmiyu commented Dec 2, 2014

I'm a bit curious, you seem to use ssl_trusted_certificate with the same wildcard-certificate that you use with ssl_certificate but Nginx documentation[1] specifies that ssl_trusted_certificate

Specifies a file with trusted CA certificates in the PEM format used to verify client certificates and OCSP responses if ssl_stapling is enabled.

The file should be a certificate bundle containing root and intermediate CAs.


chrisnew commented Dec 9, 2014

Use following ciphers to exclude RC4.


i am getting access denied error

paskal commented Jan 14, 2015

From my fork, you may want to include it as well:

# enabling Public Key Pinning Extension for HTTP (HPKP)
# to generate use on of these:
# $ openssl rsa -in my-website.key -outform der -pubout | openssl dgst -sha256 -binary | base64
# $ openssl req -in my-website.csr -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | base64
# $ openssl x509 -in my-website.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | base64
add_header Public-Key-Pins 'pin-sha256="base64+info1="; max-age=31536000; includeSubDomains'; 

@chrisnew thanks for the RC4-less ciphers.


more one question about this

when try access https://myip not redirect to my

how to redirect my ip to domian name?

works http my ip to dimain but https not


mhcerri commented Mar 18, 2015

If you add a single 'add_header' inside the server directive (in your case, for HSTS), it will override all the 'add_header's that you've defined outside.

bryfry commented Mar 24, 2015

EDH+aRSA is now considered weak

I updated my ciphers string to:


maybe you could add keepalive_timeout 70;

tierra commented Jul 3, 2015

@bryfry - They're considered weak in your case because you're still using a common Diffie-Hellman group with your ciphers, not because the cipher itself is weak. You don't need to disable EDH+aRSA ciphers, you just need to generate a new unique DH group, and configure it with your web server:

Why are the unsafe- variants allowed in the CSP? Also, the URL for Google Analytics content is now.

t1gor commented Sep 14, 2015

@oliversalzburg: +1 for google analytic url.

server_tokens off; should probably also go in the host on port 80, although I haven't checked if it actually responds with server tokens on the redirect.

More HSTS updates:
add_header Strict-Transport-Security "max-age=31622400; includeSubDomains; preload";

new requirements from Chromium team on getting in the preload list of browsers (and a core point of HSTS)

nkukard commented Jan 3, 2016

Having TLSv1 enabled fails the latest PCI scan btw.

lvjurz commented Feb 2, 2016


you should not use '$host' variable in redirect, because it will redirect you to anything that user/attacker will ask for '' in this case. Instead make sure you set your server_name correctly (i.e. and then use '$server_name' instead. Alternatively you can use FQDN in redirect and append $request_uri ('return 301$request_uri;').

Below is example of what happens when $host is used.


Connected to localhost.
Escape character is '^]'.
GET /login HTTP/1.0


HTTP/1.1 301 Moved Permanently
Server: nginx/1.4.6 (Ubuntu)
Date: Tue, 02 Feb 2016 12:19:59 GMT
Content-Type: text/html
Content-Length: 193
Connection: close

This param

Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits

ssl_dhparam /etc/nginx/ssl/dhparam.pem;
Set to 2048 is very cpu consumer.
On an octo-core Intel(R) Xeon(R) CPU X3450 @ 2.67GHz, 4 nginx workers all at 100% cpu.
When removing the ssl_dhparam, the cpu is used normally.
I think it could simply be lower than 2048.

It will only consume 100% cpu while generating 'dhparam.pem'.

Also "All versions of nginx as of 1.4.4 rely on OpenSSL for input parameters to Diffie-Hellman (DH). Unfortunately, this means that Ephemeral Diffie-Hellman (DHE) will use OpenSSL's defaults, which include a 1024-bit key for the key-exchange. Since we're using a 2048-bit certificate, DHE clients will use a weaker key-exchange than non-ephemeral DH clients. We need to generate a stronger DHE parameter."

There is a great post explaining various settings indepth

As indicated by @mhcerri, the add_header directives in the parent scope are not effective for HTTPS requests.

Please see my blog post for a further explanation: Nginx add_header configuration pitfall.

This configuration uses very weak protocols and cipher suites. I recommend using this generator to get the latest recommended NGINX configs:

Thanks in a lot!

conradwt commented May 6, 2016

@ryankearney Should all these header statements be added to the sever block because it seems that they are causing issues for other server specific files?

wget commented May 10, 2016

@conradwt I'm experiencing the same issues. When either doing a test either with nginx -t -c /my/nginx.conf/ or by restarting nginx, the latter complains with error message like:

"server_tokens" directive is not allowed here in /my/nginx.conf
"add_header" directive is not allowed here in /my/nginx.conf

dscso commented Jun 10, 2016

You should also listen on IPv6...

listen [::]:443 ssl;

Revokee commented Jun 30, 2016

@fotonobile actually you need to update your OpenSSL to fix that (I had to restart my server as well)

morsik commented Jul 2, 2016

Just included those values inside my nginx 1.11 config.
add_header doesn't works when inside http section (but docs says it should, and nginx doesn't tell me bad things about this, so strange). Moved into server section caused to serve those headers correctly.


sander commented Jul 15, 2016

@morsik did you have additional add_headers in server?

There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.

sgnl commented Oct 30, 2016

Internet Archive for

snoek09 commented Dec 6, 2016


tardis40 commented Dec 26, 2016

why no HPKP? why no dual SSL boot? why no ECDSA certificate with ssl_ecdsa_curve? the set of suites are quite weak, I'm concerned about bulk block mod CBC here: ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA

here's what I have:

`# HTTPS server
server {
listen 443 ssl;

    ssl on;

    ssl_certificate      nginx_rsa.crt;
    ssl_certificate_key  server.key;

    ssl_certificate blablabla.crt;
    ssl_certificate_key ec.key;

    add_header Strict-Transport-Security max-age=63072000;
    add_header Public-Key-Pins 'pin-sha256=\"RyVHLvC/L9OizFGYr+ujdCAdQL2vpmbquWSVwagQOzE=\"; pin-sha256=\"EohwrK1N7rr3bRQphPj4j2cel+B2d0NNbM9PWHNDXpM=\"; max-age=25920; includeSubDomains';
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ecdh_curve secp384r1:secp256k1;

    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;

And here's OCSP stapling, you know that im sure:
#OCSP Stapling

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /root/ssltest/;
resolver; `

this is more like it, imo.
sorry my formatting

petarov commented Jan 11, 2017

dhparam.pem file generation may take a decent amount of time, especially on RPis.

One solution is to use DSA rather than DH parameters, converted to DH format.

openssl dhparam -dsaparam -out /etc/nginx/ssl/dhparam.pem 2048

grandcat commented Jan 23, 2017

Regarding automatic redirect and IPv6:
Someone might want to add listen [::]:80 ipv6only=on; so Nginx correctly forwards its IPv6 visitors.

So, the complete redirect block would look like this:

# redirect all http traffic to https
server {
  listen 80;
  listen [::]:80 ipv6only=on;
  return 301 https://$host$request_uri;

Same applies to the other server block handling the real requests:

server {
  listen 443 ssl default deferred;
  listen [::]:443 ssl ipv6only=on;
  # ...

C0nw0nk commented Mar 1, 2017

Why do you not enable http2

listen 443 ssl http2 default deferred; #IPv4
listen [::]:443 ssl http2 default deferred; #IPv6

fedulovivan commented Mar 7, 2017

+1 for users @mhcerri @gertvdijk @sander who already highlighted above an issue with overriding add_headers directive

jspiro commented Mar 15, 2017

Has anyone built a new version of this with the recommendations above?

C0nw0nk commented Mar 23, 2017

OWASP recommendations are more up to date.

Also your resolver should be more like this.

resolver valid=300s; #Cache resolver

plentz commented Jul 2, 2017

@jeremysears thanks, I will update the protocols and cipher suites. It's been a while since the last time I've updated this gist

kravietz commented Jul 5, 2017

I believe is also quite an up to date resource on Nginx configuration.

Mozilla also provide an SSL config generator:

ks228 commented Aug 24, 2017

I have paid 99 dollars for this year on some expensive ssl from godaddy. I wish I knew this one earlier. Your tutorial along with let's crypt ssl will help me a lot now

ghprod commented Sep 4, 2017

This is great!

Thanks 👍

Your blogpost link is broken! :( I've added this GIST to

@MichaeMimouni try adding the -dsaparam option.

openssl dhparam -dsaparam -out /etc/ssl/private/dhparam.pem 4096

as answered here

1985a commented Nov 3, 2017

Hi there. Thanks you for sharing this. I wish I found this before. I was able to get this by using a configuration like this one.

jult commented Nov 16, 2017

FYI, here's my include for TLS/SSL and nginx:

Rotzbua commented Nov 23, 2017

There is a mistake: includeSubdomains should be includeSubDomains
The header should not be processed case sensitive but to match the RFC it would be good to use that form.

knoxcard commented Nov 29, 2017

Look at this:

add this your nginx.conf file ...

Referrer Policy

add_header Referrer-Policy "no-referrer";

Update nginx configuration: nginx -s reload

Then test here:

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