This tutorial explains how to use LetsEncrypt and acme.sh to create SSL certificates, and how to import them into NGINX virtual host configurations. In addition, snippets for a secure SSL configuration and secure transport headers are provided and explained.

A hybrid solution, using RSA and ECDSA certificates, will be created.

RSA is the reigning champion when it comes to encryption algorithms. The most important parameter which defines the security of RSA is its key length - a key length of at least $4096$ bits is recommended. As the computing power of modern CPUs and GPUs grows, keys with $8192$ bits might become necessary. However, the more bits in a key, the more difficult its generation become.

ECDSA is a modern asymmetric algorithm, based on elliptic curves. One of the perks of ECDSA is that much smaller keys can be used. OpenSSL states that a ECDSA key of $384$ bits is equivalent to an RSA key of $7680$ bits.

NB 1: This article won't deal with the mathematics behind SSL certificates. Articles explaining the mathematical details of ciphers and encryption algorithms will be released in some time in the future.

Installing LetsEncrypt and ACME

To install LetsEncrypt and acme.sh, a new user should be created first

sudo adduser letsencrypt


The user should then be added to the www-data group:

sudo usermod -a -G www-data letsencrypt


The letsencrypt user also needs permissions to restart NGINX without entering the root password. This can be done by editing the visudo file as follows:

sudo visudo



Now that the letsencrypt user exists with the necessary permissions, it can be used to install the acme.sh script. This is a single bash script file that can be downloaded via curl:

su - letsencrypt
curl https://get.acme.sh | sh
exit


Directory Structure

To not create too much chaos when using multiple hosts on the same server, I suggest creating a global .well-known/acme-challenge folder in the web directory of the letsencrypt user. This folder will be used by letsencrypt to verify domains.

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/letsencrypt
sudo chmod -R 775 /var/www/letsencrypt


As explained in the introduction, the goal is to set up a hybrid system using a Rivest–Shamir–Adleman (RSA) certificate alongside an Elliptic Curve Digital Signature Algorithm (ECDSA) certificate. A technical reasoning behind this is that ECDSA is actually faster, but if the client does not support it, a fallback to RSA is still possible as well.

Furthermore, I suggest creating a rsa and ecc folder for each virtual host. The RSA and ECDSA certificates for the host will be stored in the respective directories.

sudo mkdir -p /etc/letsencrypt/yourDomain/rsa
sudo mkdir -p /etc/letsencrypt/yourDomain/ecc
sudo chown -R www-data:www-data /etc/letsencrypt
sudo chmod -R 775 /etc/letsencrypt


NGINX and SSL

To get started, open the /etc/nginx/nginx.conf file, and make sure that the following three parameters are set:

user www-data;
worker_processes auto;
server_tokens off;


Finally disable the default nginx website by removing it from the sites-enabled directory:

sudo rm /etc/nginx/sites-enabled/default


HTTP Gateway

The default NGINX host can be replaced be a so-called HTTP Gateway host, which simply serves as a starting point for all HTTP traffic, and then proxies the traffic to the desired virtual host via HTTPS.

To create the HTTP Gateway, create a new file in the /etc/nginx/sites-available directory, i.e. named HTTPGateway, and configure it as follows:

# define the default server
server {
# listen on IPv4 and IPv6
listen 80 default_server;
listen [::]:80 default_server;

# specify the server names to listen to
server_name yourDomain;

# specify the root www folder
root /var/www;

# SSL and ACME stuff
location ^~ /.well-known/acme-challenge {
default_type text/plain;
root /var/www/letsencrypt;
}

# redirect all HTTP traffic to HTTPS
location / {
return 301 https://$host$request_uri;
}
}


listen

As can be seen from the config host file, the HTTPGateway listens on port $80$ for both IPv4 and IPv6 addresses. It is also set as the default_server. This is necessary, since some old or broken clients do not send the Host HTTP Header in their requests and thus NGINX can not figure out which domain should be accessed.

server_name

Once new virtual hosts are to be added, they should be appended to the server_name parameter. This is a list of all domain names NGINX should listen to.

root

This sets the /var/www directory as the root www folder.

ACME Challenge

To actually allow letsencrypt to verify domains, acme-challenges are configured to be resolved in the previously created /var/www/letsencrypt directory. The default_type parameter defines the default MIME type of a response, in this case plain or text types are used.

Redirect

Finally, return 301 https://$host$request_uri; redirects all HTTP traffic to HTTPS, with the desired host and url.

To activate the gateway, a symlink must be created to the sites-enabled directory:

sudo ln -s /etc/nginx/sites-available/HTTPGateway /etc/nginx/sites-enabled/HTTPGateway


Test the configuration, and if everything is ok, restart nginx:

sudo nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

sudo systemctl restart nginx


Test Access

Note: Before starting LetsEncrypt, it is important to make sure that the server is reachable. If not, LetsEncrypt might run into a loop, trying again and again to verify the domain, and does reaching a rate limit, meaning that new certificates can't be generated for 7 days!

To test if the server is reachable, simply create a text file in the acme-challenge folder created above, i.e.:

echo "Test" >> /var/www/letsencrypt/.well-known/acme-challenge/test.txt


Now try to access the file via a browser, by visiting http://yourDomain/.well-known/acme-challenge/test.txt. If the file is reachable, proceed with generating the SSL certificates.

If not, check the nginx logs and fix the errors.

Note: Remember to delete the test file.

Generating SSL Certificatess

It is now finally time to generate the actual SSL certificates. To do so, we must first switch to the letsencrypt user:

su - letsencrypt


Generating an RSA Certificate

The RSA certifcate can be created by calling acme.sh with the --keylength 4096 parameter, as follows:

acme.sh --issue -d yourDomain --keylength 4096 -w /var/www/letsencrypt --key-file /etc/letsencrypt/yourDomain/rsa/key.pem --ca-file /etc/letsencrypt/yourDomain/rsa/ca.pem --cert-file /etc/letsencrypt/yourDomain/rsa/cert.pem --fullchain-file /etc/letsencrypt/yourDomain/rsa/fullchain.pem --reloadcmd "sudo /bin/systemctl reload nginx.service"


The -d parameter specifies the domain to validate. The -w option specifies the webroot for LetsEncrypt, this is the folder that was created above. The --key-file, --ca-file, --cert-file and --fullchain-file tell acme.sh where to store the created certificate files. Finally, once the script finishes, it restarts NGINX.

Note: I do not suggest using a key smaller than 4096. Unfortunately creating a key with 8192 bits would just take too much computing time.

Generating an ECDSA Certificate

acme.sh supports ECDSA with three key lengths:

• ec-256 (prime256v1, "ECDSA P-256")
• ec-384 (secp384r1, "ECDSA P-384")
• ec-521 (secp521r1, "ECDSA P-521")

Note: At the time of writing of this article, LetsEncrypt did not yet support ec-521 keys.

I thus suggest to create EdCSA certificates by using --keylength ec-384:

acme.sh --issue -d yourDomain --keylength ec-384 -w /var/www/letsencrypt --key-file /etc/letsencrypt/yourDomain/ecc/key.pem --ca-file /etc/letsencrypt/yourDomain/ecc/ca.pem --cert-file /etc/letsencrypt/yourDomain/ecc/cert.pem --fullchain-file /etc/letsencrypt/yourDomain/ecc/fullchain.pem --reloadcmd "sudo /bin/systemctl reload nginx.service"


Automatic Renewal of SSL Certificates

Warning: LetsEncrypt certificates are only valid for $90$ days and must thus be renewed periodically. The acme.sh script should have created cronjobs for the letsencrypt user. This can be verified by just typing crontab -l:

crontab -l
45 0 * * * "/home/letsencrypt/.acme.sh"/acme.sh --cron --home "/home/letsencrypt/.acme.sh" > /dev/null


If done, go back to the normal user:

exit


Diffie-Hellman Parameter

The security of the HTTPS communication can further be increased by using the so-called Diffie-Hellman Key Exchange. The Diffie-Hellman algorithm provides the capability for two communicating parties to agree upon a shared secret between them.

To create a Diffie-Hellman key, create a folder to store it in, and then run openssl dhparam as follows:

sudo mkdir -p /etc/nginx/dhparams
sudo openssl dhparam -out /etc/nginx/dhparams/dhparams.pem 4096


This tells openssl to create a $4096* bit Diffie-Hellman key for us. NGINX Snippets To easily use SSL certificates in future NGINX host configurations, SSL snippets can be created. Those snippets should be stored in the /etc/nginx/snippets directory. If that directory does not yet exist, as it should, then create it: sudo mkdir -p /etc/nginx/snippets  SSL Configuration The first snippet to create is a snippet to hold all SSL related configurations. Create a ssl.conf file as follows: # 4096 Diffie-Hellman Key Exchange ssl_dhparam /etc/nginx/dhparams/dhparams.pem; # do not use the old TLSv1.1 protocol ssl_protocols TLSv1.2 TLSv1.3; # use EdCSA and RSA ciphers ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; # use multiple curves ssl_ecdh_curve secp521r1:secp384r1; # tell the server to determine which cipher to use ssl_prefer_server_ciphers on; # SSL sessions ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; # SSL stapling for self-signed certs ssl_stapling on; ssl_stapling_verify on; # DNS resolver resolver 192.168.178.1;  ssl_dhparam This parameter tells NGINX which Diffie-Hellman key to use. ssl_protocols With parameter specifies which versions of the TLS protocol are allowed to be used. I suggest using the newer TLSv1.2 and TLSv1.3 protocols. ssl_ciphers With this parameter, one can define which ciphers to be used preferably. The ciphers are tried in the order as specified here, i.e. if the client does not support the ECDHE-ECDSA-AES128-GCM-SHA256 cipher, it will try to use the next one in the list until it reaches the end of the list. An updated list of secure ciphers can be generated by Mozilla. ssl_ecdh_curve This parameters specifies the elliptic curve algorithms to use. ssl_prefer_server_ciphers When this boolean is set to on, the web server can control which ciphers are available. When the server supports old TLS versions and ssl_prefer_server_ciphers is set to off, an adversary could interfere with the handshake and force the connection to use weak ciphers. Those weak ciphers have been deprecated in TLS v1.2 and TLS v1.3, which would theoretically remove the need for servers to specify preferred ciphers. Thus, in modern settings, it is suggested to set ssl_prefer_server_ciphers to off, because then the client device can choose the preferred encryption method based on the hardware capabilities of the client device. For some reason I prefer this to be set to off, and to provide a list of secure ciphers with the ssl_ciphers parameter. Session Settings The three session settings basically tell NGINX how long to wait to establish secure communication. SSL Stapling SSL stapling means that a certificate's revocation information are included in the TLS handshake together with the server certificate. If a browser requests to get a stapled Online Certificate Status Protocol (OCSP) response, those settings will prevent a man-in-the-middle from using a revoked certificate for the domain in order to hijack the traffic. Resolver This parameter defines the DNS to use. In this case it is simply set to the router. I would avoid using the standard Google or CloudFare resolvers. Resolvers with a good privacy record are hosted by, for example, Digital Courage, Digitale Gesellschaft, NixNet and Quad9. A mix of the above resolvers could look like this: resolver 9.9.9.9 149.112.112.112 104.244.78.231:853 5.9.164.112 185.95.218.42 185.95.218.43 [2620:fe::fe] [2620:fe::9] [2a05:fc84::42] [2a05:fc84::43] valid=60s;  A complete list is offered by PrivacyTools IO. Transport Header Configuration To easily import a secure transport header configuration in virtual hosts files, another snipped, called header.conf should be created and configured as follows. Check the Mozilla Documentation for a more thorough explanation of each parameter. # security related headers add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload;" always; add_header X-Content-Type-Options "nosniff" always; add_header Content-Security-Policy "default-src https:" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Robots-Tag none always; add_header X-Download-Options noopen always; add_header X-Permitted-Cross-Domain-Policies none always; add_header Referrer-Policy no-referrer always; add_header X-Frame-Options "SAMEORIGIN" always; # remove X-Powered-By, which is nothing but an information leak fastcgi_hide_header X-Powered-By;  Strict-Transport-Security The HTTP Strict-Transport-Security (HSTS) response HTTP Header tells browsers to only access our domains via HTTPS. This is important, as if a website accepts a connection through HTTP and redirects to HTTPS, visitors may initially communicate with the non-encrypted version of the site before being redirected. This creates a small but lucrative opportunity for a man-in-the-middle attack. The redirect could be exploited to direct visitors to a malicious site instead of the secure version of the original site. The max-age parameter defines the time, in seconds, that the browser should remember to only access this domain via HTTPS. The includeSubDomains parameter applies this same rule to all subdomains as well. The preload directive adds the domain to the Google HSTS preload service, which means that browsers will never connect to the domain using an insecure connection. While the service is hosted by Google, all browsers have stated an intent to use (or actually started using) this preload list. X-Content-Type-Options The X-Content-Type-Options response HTTP header indicates that the MIME types are deliberately configured. Setting this to nosniff thus blocks any requests for the style if the MIME type is not text/css or script if the MIME type is not a JavaScript MIME type. In addition, Cross-Origin Read Blocking protection for the following MIME-types is enabled: • text/html • text/plain • */json • */xml X-XSS-Protection The HTTP X-XSS-Protection Response Header is a feature of Internet Explorer, Chrome and Safari that stops pages from loading when they detect reflected cross-site scripting attacks. Although these protections are largely unnecessary in modern browsers, as sites implement a strong Content-Security-Policy that disables the use of inline JavaScript, they can still provide protection for users of older web browsers. Setting the mode to block tells the browser to prevent rendering the page if an attack is detected. X-Robots-Tag X-Robots-Tags are used to manage the crawling and indexing of pages. X-Download-Options The X-Download-Options is a parameter specific to IE 8, and how it handles downloaded HTML files. By setting this to noopen, downloaded HTML files will not be opened in the context of a web site, and thus included scripts won't run. X-Permitted-Cross-Domain-Policies https://owasp.org/www-project-secure-headers/#x-permitted-cross-domain-policies A Cross-Domain Policy file is an XML document that grants a web client, such as Adobe Flash Player or Adobe Acrobat, permission to handle data across different domains. By setting this to none, no policy files are allowed anywhere on the server. Referrer-Policy The Referrer-Policy HTTP Header controls how much referrer information should be included with requests. By specifying the no-referrer option, the referer header will be omitted entirely, which means that no referrer information is sent along with requests, which increases privacy and security. X-Frame-Options The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a frame, iframe, embed or object. This can be used this to avoid click-jacking attacks, by ensuring that the content is not embedded into other sites. Setting this to SAMEORIGIN means that the pages of a virtual host can only be displayed in a frame on the same origin as the page itself. X-Powered-By The snippets disables the X-Powered-By Header, which specifies the technology (e.g. ASP.NET, PHP, JBoss) used to build the web application. Since this is nothing but a security and privacy break, I suggest always disabled the X-Powered-By header. An Example Configuration Now that the secure SSL and Header snippets are created, it is time to create a virtual host file. To do so, a new file should be created in the sites-available directory. server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name yourDomain; ############################################################################## # SSL CONFIGURATION ########################################################## ############################################################################## # RSA certificates ssl_certificate /etc/letsencrypt/yourDomain/rsa/fullchain.pem; ssl_certificate_key /etc/letsencrypt/yourDomain/rsa/key.pem; # EdCSA certificates ssl_certificate /etc/letsencrypt/yourDomain/ecc/fullchain.pem; ssl_certificate_key /etc/letsencrypt/yourDomain/ecc/key.pem; ssl_trusted_certificate /etc/letsencrypt/yourDomain/ecc/ca.pem; # include SSL snippet include /etc/nginx/snippets/ssl.conf; # include security headers include /etc/nginx/snippets/headers.conf; ############################################################################## # WEBSITE CONFIGURATION €€#################################################### ############################################################################## location / { root /var/www/yourDomain; index index.html try_files$uri \$uri/ /index.html;
}
}


Test Results

There are various tools that can be used to test the security of the SSL configuration.