Skip to content

Instantly share code, notes, and snippets.

@damko
Last active April 16, 2017 15:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save damko/bef0d0831678eb8f3e90862454068b07 to your computer and use it in GitHub Desktop.
Save damko/bef0d0831678eb8f3e90862454068b07 to your computer and use it in GitHub Desktop.

Nginx Notes

Last updated: @2017-04-16

How Nginx works under the hood

At the very moment of starting Nginx, one unique process exists in memory: the Master Process.

It is launched with the current user and group permissions (usually root/root) if the service is launched at boot time by an init script.

The master process itself does not process any client request, instead, it spawns processes that do the Worker Processes, which are affected to a customizable user and group.

From the configuration file, you are able to define the amount of worker processes, the maximum connections per worker process, the user and group the worker processes are running under, and more.

Nginx configuration files

String Values

Nginx makes no difference whether you use single or double quotes.

Ex.:

root '/home/example.com/my web pages';

or

root "/home/example.com/my web pages";

Directives

Nginx's directives are case sensitive => Server_name != server_name

Variables

Nginx modules also provide variables that can be used in the definition of directive values.

Ex.: the Nginx HTTP Core module defines the $nginx_version variable.

log_format main '$pid - $nginx_version - $remote_addr';

Note: variables inserted in strings within quotes will be expanded normally, unless you prefix the $ character with a backslash ( \ ).

What's a server block

An Nginx server block defines the routes and rules applied for a specific domain served by Nginx.

If you are experienced with Apache, a server block is the equivalent of an Apache vhost.

Nginx's tests

This command returns the list of modules enabled at compile time for nginx:

nginx -V

This command parses nginx's config files and checks for syntax errors

nginx -t

How to enable Gzip compression

Reference: how to add the gzip module to nginx

Nginx comes with gzip compression already enabled but just for html files. Binary files should not be compressed but other text files like .css, .js, .json etc. should be.

To extend nginx's gzip functionalities edit /etc/nginx/nginx.conf

##
# `gzip` Settings
#
gzip on;
gzip_disable "msie6";

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;

# this skips compression for very small files
gzip_min_length 256;

# this defines the MIME types for which compression should be enabled
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

To apply

service nginx restart

Test gzip compression

Compression can be tested with the following command:

curl -H "Accept-Encoding: gzip" -I http://localhost/test.html

In response, you should see several HTTP response headers:

Nginx response headers

HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Date: Tue, 19 Jan 2016 20:04:12 GMT
Content-Type: text/html
Last-Modified: Tue, 04 Mar 2014 11:46:45 GMT
Connection: keep-alive
Content-Encoding: gzip

The last line proves that the file has been served compressed.

You can test any other kind of file at the same way.

curl -H "Accept-Encoding: gzip" -I http://localhost/test.css

SSL

To enable SSL encryption in a webserver the first step is to create valid certificates for the domain.

The HTTPS method relies on some certificates released by a Certification Authority (CA). The CA needs to receive a CSR to create valid certificates.

dhparam.pem

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 includes a 1024-bit key for the key-exchange.

Since Letsencrypt and Namecheap are using a 2048-bit certificate, nginx will use a weaker key-exchange therefore it's better to generate a stronger DHE parameter to further increase security:

Use this command:

openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Now create this file /etc/nginx/snippets/ssl-params.conf and add this content:

# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable preloading HSTS for now.  You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
# add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

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

If you have your own DNS server you might want to replace this line

resolver 8.8.8.8 8.8.4.4 valid=300s;

with

resolver your_dns_server_ip 8.8.8.8 valid=300s;

dhparam.pem will used later on. Move on.

SSL using Namecheap.com certificates

In the following examples I will consider a real case with Namecheap.com CA

CSR (Certificate Signing Request) generation using OpenSSL

The first step happens on the host side with the production of certificates uniquely identifying the domain.

A CSR is a file containing your certificate application information, including your Public Key.

To generate a pair of private key and public Certificate Signing Request (CSR) for a webserver use :

cd /root
openssl req -nodes -sha256 -newkey rsa:2048 -keyout example--com_ssl.key -out example--com_ssl.csr

This command creates a two files in the current folder:

  • example--com_ssl.key which contains a private key; do not disclose this file to anyone and back it up because there is no means to recover it should it be lost.
  • example--com_ssl.csr which contains the Certificate Signing Request

The private key is used as input in the command to generate the Certificate Signing Request (CSR).

After running the command you will be asked to enter details to be entered into your CSR.

For some fields there will a default value and if you enter '.', the field will be left blank.

Country Name (2 letter code) [AU]: GB
State or Province Name (full name) [Some-State]: Yorks
Locality Name (eg, city) []: York
Organization Name (eg, company) [Internet Widgits Pty Ltd]: MyCompany Ltd
Organizational Unit Name (eg, section) []: IT
Common Name (eg, YOUR name) []: **mysubdomain.example.com**
Email Address []:
A challenge password []:
An optional company name []:

Use a fully qualified domain name (FQDN) as Common Name (CN)

For a webserver certificate these fields can be left blank:

  • email address
  • optional company name
  • challenge password

You can check the CSR signature with this command:

openssl req -in example--com_ssl.csr -text -noout

Generation of the certificates through Namecheap

Everything is ready to start the "PositiveSSL" certificate to Namecheap and the full content of the example--com_ssl.csr file will be requested during the registration process through the web interface

Login to Namecheap/Your SSL certificate, click "Activate". You will be asked to select the server type. Select Apache + OpenSSL (even though you are using Nginx). You then are asked to paste the certificate signing request (CSR). On your server, do a

cat example--com_ssl.csr

and copy and paste the everything (including "----BEGIN..." and "----END...") to the Namecheap web page's text-area and click submit. You will then be asked enter additional information, enter them.

You will receive a email from Comodo asking you to open a link and paste some code that's provided in the mail. Do that.

After some time, you will get your certificate in the email. Download the zip file.

It contains 4 files:

  • example--com.crt
  • COMODORSADomainValidationSecureServerCA.crt
  • COMODORSAAddTrustCA.crt
  • AddTrustExternalCARoot.crt

Now you need to combine most of the Comodo crt files into a single site crt file Nginx will use. The AddTrustExternalCARoot.crt file is not needed because the client's browser, like Firefox or Chrome, already has a list of root certs they will trust. When combining the crt files the order is important to Nginx:

cat example--com.crt COMODORSADomainValidationSecureServerCA.crt COMODORSAAddTrustCA.crt > example--com_ssl.crt

You can check the signature of the combined file with this command:

openssl x509 -noout -text -in example--com_ssl.crt

Now move or copy there these 2 files

  • example--com_ssl.crt
  • example--com_ssl.key

to /etc/ssl/certs:

tree -d /etc/ssl/

Output:

/etc/ssl/
├── certs
└── private

And add fill /etc/nginx/snippets/ssl-example--com.conf with this content:

echo "ssl_certificate /etc/ssl/certs/example--com_ssl.crt;" > /etc/nginx/snippets/ssl-example--com.conf
echo "ssl_certificate_key /etc/ssl/certs/example--com_ssl.key;" >> /etc/nginx/snippets/ssl-example--com.conf

SSL using Letsencrypt.org certificates

Letsencrypt is a free, automated and open Certificate Authority. This is enough to understand how awesome it is.

From the Nginx perspective, there is no difference between Namecheap and Letsencrypt certificates but from the configuration point of view there are differences, so I prefer to write all the steps separately and indipendently from those exposed above (i.e. you don't need to read them, you can just focus on this paragraph).

I took most of the instructions from this awesome article but I've adjusted it accordingly to my experience.

Configuring the system for Letsencrypt

Letsencrypt's certfication is automated: what does it mean? A few things:

  • the creation and installation of certificates happens through a specific tool called certbot
  • letsencrypt certificates expire after 3 months but certbot can automatically renew them until necessary

If you don't have Debian's backports repository configured, add it:

echo 'deb http://ftp.debian.org/debian jessie-backports main' | sudo tee /etc/apt/sources.list.d/backports.list

Then install certbot

apt-get install certbot -t jessie-backports

Get the host ready

The certification process, in a nutshell, works like this:

  • certbot will contact the CA requesting a new certificate for a specific domain
  • the CA will try to reach the specific domain to verify that the domain exists and will look for a specific path to ensure that the request is valid
  • once the verified the CA will send back to certbot the certificate and it will save it on the harddrive
  • the obtained certificate must be included in the server block
  • the system must configured to renew the certificate every 90 days

Therefore the first thing to do is to configure the nginx server block in order to provide access to the specif path which is $webroot/.well-known/acme-challenge

Assuming that /var/www/example--com is the domain's webroot

cd /var/www/example--com

If the webroot is /var/www/example--com:

cd /var/www/example--com/public

Create the folder:

mkdir -p .well-known/acme-challenge
chown www-data:www-data -fR .well-known

Configure the server block in the nginx domain config file:

cd /etc/nginx/sites-enabled
vim example--com.conf

Add this information block

location ^~ /.well-known/acme-challenge/ {
        allow all;
        default_type "text/plain";
}

Reload nginx

service nginx reload

Test it:

curl http://example.com/.well-known/acme-challenge/

You should get a 403 and an output like this one:

<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

Obtain an letsencrypt SSL Certificate

There are several plugins for certbot which are capable to automate a lot the certificate installation but I prefer to use the simple webroot plugin which just triggers the domain validation and the download of the certificate. I will then configure nginx manually.

This command will request a new certificate to Letsencrypt CA for the domain example.com and www.example.com. As far as I understand letsencrypt does not provide wildcard certificates (like *.example.com) therefore you need to specify all the subdomains you want to certify in the command line. Letsencrypt does support though any subdomain level (like something.www.example.com)

certbot certonly --webroot -w /var/www/example--com/public/ -d example.com -d www.example.com

If you are running certbot for the first time on the system, it will ask you to provide an email address that will be used for notifications and in case of recovery.

If everything goes well this will be the output

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for example.com
http-01 challenge for www.example.com
Using the webroot path /var/www/example--com/public for all
unmatched domains.
Waiting for verification...
Cleaning up challenges
Generating key (2048 bits):
/etc/letsencrypt/keys/0002_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0002_csr-certbot.pem

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/example.com/fullchain.pem. Your cert will
   expire on 2017-07-15. To obtain a new or tweaked version of this
   certificate in the future, simply run certbot again. To
   non-interactively renew *all* of your certificates, run "certbot
   renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Yes, please, consider donating to this awesome project.

Be aware that after a few failed attempts in a row, letsencrypt will block the certification of such a domain for a limited amount of time (maybe an hour, something like that)

You can now see the certificates for example.com in your filesystem:

tree ls -l /etc/letsencrypt/live/example.com/

ls [error opening dir]
/etc/letsencrypt/live/example.com/
├── cert.pem -> ../../archive/example.com/cert1.pem
├── chain.pem -> ../../archive/example.com/chain1.pem
├── fullchain.pem -> ../../archive/example.com/fullchain1.pem
└── privkey.pem -> ../../archive/example.com/privkey1.pem

Now add fill /etc/nginx/snippets/ssl-example--com.conf with this content:

echo "ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;" > /etc/nginx/snippets/ssl-example--com.conf
echo "ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;" >> /etc/nginx/snippets/ssl-example--com.conf

Renewal

Edit /etc/crontab and add this line

17 3    * * 1   root    certbot renew >> /var/log/le-renew.log

Every Monday at 3:17 am your system will check if certificates require renewal and, in that case, will contact letsencrypt for automatic renewal. Be aware that, if the renewal doesn't happen in the next 90 days from the creation time your certificates will expire and you will have to recreate them again. The output of the command will be logged in /var/log/le-renew.log

Enable SSL for the Nginx server block

It's all set for configuring nginx to use our certificates, doesn't matter if we are using Namecheap's or Letsencrypt's ones.

Before starting you need to understand which strategy you want nginx to perform:

  • strategy 1: to answer only to port 443 and completely drop the support for non-encrypted traffic. This means that any request like http://example.com will produce a 404 error
  • strategy 2to answer on port 80 and redirect all the traffic on port 443 enforcing encryption. This means that any request like http://example.com will be redirected to https://example.com
  • strategy 3support both encrypted and not encrypted communication letting the client chose the kind of connection. This means that any request like http://example.com will be answered unencrypted while any connection established with https will be encrypted.

strategy 1

The nginx server block inside the /etc/nginx/sites-available/example--com.conf file will look like this one:

server {
  listen 443 ssl;
  server_name example.com www.example.com;
  root /var/www/example--com/public;

  ssl on;
  include /etc/nginx/snippets/ssl-example--com.conf;
  include /etc/nginx/snippets/ssl-params.conf;

  # ...
}

strategy 2

The nginx server block inside the /etc/nginx/sites-available/example--com.conf file will look like this one:

server { listen 80; server_name example.com www.example.com ; #redirects all the traffic on https return 301 https://$server_name$request_uri; }

server { # SSL configuration

    listen 111.222.1.111:443 ssl;
    #server_name     example.com www.example.com ;

    include snippets/ssl-malishor--com.conf;
    include snippets/ssl-params.conf;

    # ...

}

strategy 3

The nginx server block inside the /etc/nginx/sites-available/example--com.conf file will look like this one:

server {
  listen 80;
  listen 443 ssl;
  server_name example.com www.example.com;
  root /var/www/example--com/public;

  ssl on;
  include /etc/nginx/snippets/ssl-example--com.conf;
  include /etc/nginx/snippets/ssl-params.conf;

  # ...
}

and enable it by linking it in the sites-enabled folder

cd /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/example--com.conf 

Run it

Test if the system is able to resolv the domain:

ping example.com

If the domain is unreachable or it points to a wrong ip address, edit the /etc/hosts file or change your dns entries accordingly

Then restart nginx.

nginx -t && service nginx restart

Test the SSL support

To test if SSL is working correctly, run telnet from your client computer (preferably)

telnet example.com 443

When the connection is established type:

GET / HTTP/1.0

and press Enter twice

This would be the output for a non encrypted request (strategy 3)

telnet example.com 80
Trying 111.222.1.111...
Connected to example.com.
Escape character is '^]'.
GET / HTTP/1.0

double enter here

Output:

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 16 Apr 2017 14:50:07 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 11
Last-Modified: Sun, 16 Apr 2017 12:51:10 GMT
Connection: close
ETag: "58f368be-b"
Accept-Ranges: bytes

<html>
<head><title>Example.com home page</title></head>
<body bgcolor="white">
<p>this the content of index.html at example.com</p>
</body>
</html>
Connection closed by foreign host.

This would be the output for an encrypted request (strategy 2)

telnet example.com 443
Trying 111.222.1.111...
Connected to example.com.
Escape character is '^]'.
GET / HTTP/1.0

double enter here

Output:

HTTP/1.1 400 Bad Request
Server: nginx
Date: Sun, 16 Apr 2017 14:53:43 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 264
Connection: close

<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
Connection closed by foreign host.

How to change Nginx's signature

For additional security you might want to disguise your webserver signature to confuse a potential attacker.

One comfortable way for changing nginx's signature is to install the extras package

apt-get install nginx-extras

and add these two lines to /etc/nginx/nginx.conf

http{
         more_set_headers "Server: Apache/2.4.10 (Ubuntu)";
         server_tokens off; 
}

Restart nginx

nginx -t && service nginx restart

Test it

telnet example.com 443
Trying 111.222.1.111...
Connected to example.com.
Escape character is '^]'.
GET / HTTP/1.0

HTTP/1.1 400 Bad Request
Date: Sun, 16 Apr 2017 15:10:26 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 264
Connection: close
**Server: Apache/2.4.10 (Ubuntu)**

<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
Connection closed by foreign host.

If you look carefully, the signature is changed but the content of the 400 error page still shows nginx

Be aware of that and customize Nginx's error pages accordingly. See this for more information.

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