Skip to content

Instantly share code, notes, and snippets.

@cecilemuller
Last active March 15, 2024 21:15
Star You must be signed in to star a gist
Save cecilemuller/a26737699a7e70a7093d4dc115915de8 to your computer and use it in GitHub Desktop.
How to setup Let's Encrypt for Nginx on Ubuntu 18.04 (including IPv6, HTTP/2 and A+ SSL rating)

How to setup Let's Encrypt for Nginx on Ubuntu 18.04 (including IPv6, HTTP/2 and A+ SLL rating)


Virtual hosts

Let's say you want to host domains first.com and second.com.

Create folders for their files:

mkdir /var/www/first
mkdir /var/www/second

Create a text file /etc/nginx/sites-available/first.conf containing:

server {
	listen 80 default_server;
	listen [::]:80 default_server ipv6only=on;

	server_name first.com www.first.com;
	root /var/www/first;

	index index.html;
	location / {
		try_files $uri $uri/ =404;
	}
}

Create a text file /etc/nginx/sites-available/second.conf containing:

server {
	listen 80;
	listen [::]:80;

	server_name second.com www.second.com;
	root /var/www/second;

	index index.html;
	location / {
		try_files $uri $uri/ =404;
	}
}

Note that only the first domain has the keywords default_server and ipv6only=on in the listen lines.

Replace the default virtual host:

sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/first.conf /etc/nginx/sites-enabled/first.conf
sudo ln -s /etc/nginx/sites-available/second.conf /etc/nginx/sites-enabled/second.conf

sudo nginx -t
sudo systemctl stop nginx
sudo systemctl start nginx

Check that Nginx is running:

sudo systemctl status nginx

Expected results at this stage:

  • http://first.com and http://www.first.com serve the files from /var/www/first
  • http://second.com and http://www.second.com serve the files from /var/www/second
  • https://www.first.com and https://www.second.com don't work yet

Certbot

Install Certbot for Nginx:

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-nginx

Setup the certificates & convert Virtual Hosts to HTTPS:

sudo certbot --nginx

It will ask for:

  • an email address
  • agreeing to its Terms of Service
  • which domains to use HTTPS for (it detects the list using server_name lines in your Nginx config)
  • whether to redirect HTTP to HTTPS (recommended) or not

You could stop here if all you want is HTTPS as this already gives you an A rating and maintains itself.

Test your site with SSL Labs using https://www.ssllabs.com/ssltest/analyze.html?d=www.YOUR-DOMAIN.com

Expected results at this stage:

  • http://first.com redirects to https://first.com
  • http://second.com redirects to https://second.com
  • http://www.first.com redirects to https://www.first.com
  • http://www.second.com redirects to https://www.second.com
  • https://first.com and https://www.first.com serve the files from /var/www/first
  • https://second.com and https://www.first.comserve the files from /var/www/second

Automatic renewal

There is nothing to do, Certbot installed a cron task to automatically renew certificates about to expire.

You can check renewal works using:

sudo certbot renew --dry-run

You can also check what certificates exist using:

sudo certbot certificates

HTTP/2

first.conf should now look something like this, now that Certbot edited it:

server {
	server_name first.com www.first.com;
	root /var/www/first.com;

	index index.html;
	location / {
		try_files $uri $uri/ =404;
	}

	listen [::]:443 ssl ipv6only=on; # managed by Certbot
	listen 443 ssl; # managed by Certbot
	ssl_certificate /etc/letsencrypt/live/first.com/fullchain.pem; # managed by Certbot
	ssl_certificate_key /etc/letsencrypt/live/first.com/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
	if ($host = www.first.com) {
		return 301 https://$host$request_uri;
	} # managed by Certbot

	if ($host = first.com) {
		return 301 https://$host$request_uri;
	} # managed by Certbot

	listen 80 default_server;
	listen [::]:80 default_server;

	server_name first.com www.first.com;
	return 404; # managed by Certbot
}

Certbot didn't add HTTP/2 support when it created the new server blocks, so replace these lines:

listen [::]:443 ssl ipv6only=on;
listen 443 ssl;

by this:

listen [::]:443 ssl http2 ipv6only=on;
listen 443 ssl http2;
gzip off;

There is already an open Github issue requesting Certbot to add http2 automatically, so hopefully this step can soon be removed.


Stronger settings for A+

Trusted certificate

The HTTPS server blocks in first.conf and second.conf contain these lines, added by Certbot:

ssl_certificate /etc/letsencrypt/live/YOUR-DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOUR-DOMAIN/privkey.pem;

The stronger settings use OCSP Stapling, so each virtual host will need a ssl_trusted_certificate as well.

Add this line (using the folder name that Certbot generated for your domain) after the ssl_certificate_key line:

ssl_trusted_certificate /etc/letsencrypt/live/YOUR-DOMAIN/chain.pem;

SSL

Now let's edit the shared SSL settings at /etc/letsencrypt/options-ssl-nginx.conf. It most likely looks like this initially:

ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
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";

If you tested with SSL Labs, you probably noticed that quite a few ciphers were flagged as "weak".

So replace the contents of the file with:

ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1d;
ssl_session_tickets off;

ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;

ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload;";
add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; script-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self';";
add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

Now restart Nginx, and test the domain again with SSL Labs using https://www.ssllabs.com/ssltest/analyze.html?d=www.YOUR-DOMAIN.com&latest: it should now be rated A+, congratulations! 🙂


Conclusion

You could further improve using content-specific features like Content Security Policy and Subresource Integrity, and Brotli compression to replace gzip.

Online testing tools:

Useful links:

If Let's Encrypt is useful to you, consider donating to Let's Encrypt or donating to the EFF.

@OsoianMarcel
Copy link

Thank you very much, I'm using this tutorial each time I need letsencrypt.
P.S. Works fine for apache2 too.

@junjielee
Copy link

Thank, it's very useful

Copy link

ghost commented May 22, 2018

awesome thank you!

@LarryWachira
Copy link

Thanks a million! Very well explained.

@umarali
Copy link

umarali commented May 30, 2018

Life saver! Thanks very much!

@maikdiepenbroek
Copy link

Easy to follow and effective guide, thanks 👍

@bennetcq
Copy link

bennetcq commented Jun 29, 2018

Nearly perfect for me! Thanks!
-----when edit /etc/letsencrypt/options-ssl-nginx.conf----
must delete or comment as followed----
#add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; script-src 'self'; img-src 'self'; style-src 'self'; base-ur$
#add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin";
#add_header X-Frame-Options SAMEORIGIN;
#add_header X-Content-Type-Options nosniff;
#add_header X-XSS-Protection "1; mode=block";

if uncomment above code, only result in SSL A, not A+

@cecilemuller
Copy link
Author

cecilemuller commented Jun 29, 2018

@bennetcq Please use a markdown block for the comment (either start every line with a tab, either wrap the group of lines with 4 ticks) because the question is quite hard to read with that huge text.

Are you asking why you're getting only A rating when the add_header lines start with # ? If so, that's because the settings are being ignored by Nginx, # symbols in configs are for writing arbitrary comments.

Also the Content-Security-Policy line in your code looks incomplete, see the one form the tutorial:

add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload;";
add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; script-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self';";
add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

@alpex8
Copy link

alpex8 commented Jul 3, 2018

Hi @cecilemuller,
thanks for this guide, helped me a lot. There is one thing in /etc/letsencrypt/options-ssl-nginx.conf that doesn't seem to work for me.
I have a virtual host operating as a proxy to my fritz box configuration page.
Having

add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; script-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self';";

enabled results in an empty page when I access the fritz box sub domain.
When I comment this line everything works as intended.
Do you have any suggestion how I could fix that?

Regards

EDIT: I just noticed that even when I disable the mentioned line I get an A+ rating.

@MagePsycho
Copy link

letsencrypt-auto --nginx vs certbot --nginx which one is the better approach to install?

@MagePsycho
Copy link

There is nothing to do, Certbot installed a cron task to automatically renew certificates about to expire.
But when I checked with

sudo crontab -e

There was nothing.

Can you please confirm with the auto-renewal process.

@JamesSwift
Copy link

A very handy reference. I made a simple script to set-up my web server, and manage adding and removing domains with this technique. You might find it useful: https://github.com/JamesSwift/SharedServerTools

@markomilivojevic
Copy link

For multiple virtual hosts, run command separately for each host.

Copy link

ghost commented Dec 25, 2018

I don't understand. Why would you want to redirect http://first.com to https://first.com and also http://www.first.com to https://www.first.com ? You always want to redirect to a single state, and never serve to user both www and non-www.

So correct redirect should be (here could be either everything to www or everything to non-www), like:

http://first.com -> https://first.com
http://www.first.com -> https://first.com
https://www.first.com -> https://first.com

@WeeHong
Copy link

WeeHong commented Feb 4, 2019

Thank you for posting useful gist.

@robsch
Copy link

robsch commented Feb 13, 2019

Comment in file options-ssl-nginx.conf:

This file contains important security parameters. If you modify this file
manually, Certbot will be unable to automatically provide future security
updates. Instead, Certbot will print and log an error message with a path to
the up-to-date file that you will need to refer to when manually updating
this file.

I just wanted to remove TLSv1 and TLSv1.1 (someone else has had the same requirement). The solution for me would be to modify that file. But I'm wondering if that could cause any problems. Could it? During renewal or updating certbot at any time. Or where else?

I found also this page at Webdock that recommends to modify this file. Although I don't use Webdock.

@adamblackxo
Copy link

Thanks a lot it works fine with my app on scaleway

@natharas
Copy link

natharas commented Mar 2, 2019

I tried a dry-run for renewal but it failed with the below

Processing /etc/letsencrypt/renewal/domainname.com.conf


Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator nginx, Installer nginx
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for domainname.com
nginx: [warn] conflicting server name "www.domainname.com" on 0.0.0.0:80, ignored
Waiting for verification...
Cleaning up challenges
nginx: [warn] conflicting server name "www.domainname.com" on 0.0.0.0:80, ignored
Attempting to renew cert (domainname.com) from /etc/letsencrypt/renewal/domainname.com.conf produced an unexpected error: Failed authorization proced ure. domainname.com (http-01): urn:ietf:params:acme:error:connection :: The server could not connect to the client to verify the domain :: Fetching http://domainname.com/.well-known/acme-challenge/Fe92cR_AHG9BqkiTogJ4q1giXkJGR2eeGM6R-dpiP6M: Timeout during connect (likely firewall problem). Sk ipping.
All renewal attempts failed. The following certs could not be renewed:
/etc/letsencrypt/live/domainname.com/fullchain.pem (failure)

@martondob
Copy link

This is fantastic!
Thank you soo much for doing such a great job.

@cecilemuller
Copy link
Author

Quick note about an issue I ran into recently: if your server fails to update certbot (0.28.0 minimum is now required as SNI is no longer supported), try sudo apt full-upgrade because the change from python 2 to 3 is holding back the certbot package on some servers.

@billpliske
Copy link

Saved the day.

@carlostiberiojr
Copy link

How can I do the same configurations and so on, if my ISP is blocking my port 80 and 443?
I cannot find anything related to that :(
To set my A record is already "impossible" because of that block... do we have some way to workaround that?

@rajeshkumaravel
Copy link

@cecilemuller thanks a lot...

@AlbertSuarez
Copy link

You saved my day @cecilemuller Thank you so much! 🙌

@wallysoncarvalho
Copy link

Hi, thanks a lot for the tutorial.
The thing is, in my server everything works perfectly, except that afeter installation of the certificate with certbot the server stops to respond. It stays awaiting....
I have no ideia why and cant find a solution on the internet.
Any idea that could help ?!
Thanks !!

@peterneubart
Copy link

@cecilemuller
this is awesome thanks alot!

I have a question regarding redirects from WWW to non-WWW:
https://www.example.com isn't redirecting to non-WWW

When obtaining the SSL certificate:
sudo certbot --nginx -d example.com -d www.example.com
I have choosen option: 2

How can I get this working?
So all requests http(s)://www. will get redirected to https:// without WWW

@emjayess
Copy link

I have a question regarding redirects from WWW to non-WWW:
https://www.example.com isn't redirecting to non-WWW

When obtaining the SSL certificate:
sudo certbot --nginx -d example.com -d www.example.com
I have choosen option: 2

How can I get this working?
So all requests http(s)://www. will get redirected to https:// without WWW

Hi Peter,

Out of the box, the redirection managed by certbot concerns http to https redirection (ensures a secure connection). For the www redirection, the most common case I think you'll find is redirecting from a root domain to the www subdomain or variation...

Which could be achieved like this:

if ($host = root-domain.com) {
    return 301 https://www.$host$request_uri;
}

Or this:

server_name root-domain.com;
return 301 https://www.root-domain.com$request_uri;

And so to go the other way and always shift visits to the www variation onto the root domain, you'll want to reverse that logic:

if ($host = www.root-domain.com) {
    return 301 https://root-domain.com$request_uri;
}

Or:

server_name www.root-domain.com;
return 301 https://root-domain.com$request_uri;

Such redirects generally should be implemented in their own server { ... } block.

I hope this helps :)

@tpaullee
Copy link

You are meticulous for this nice write-up. Thanks so much!
May be the link URL can be aliased to a better self-explanatory name to reach more readership.

@hitjethva
Copy link

I ran this command: certbot --apache -d linuxbuz.com

I am getting the following error:

IMPORTANT NOTES:

The following errors were reported by the server:

Domain: linuxbuz.com
Type: connection
Detail: Fetching
https://linuxbuz.com/.well-known/acme-challenge/ewpBCX7N0nzDyBZZILYP-y9sKHI4seFGac4Se7TpwfA:
Connection refused

@ddoherty
Copy link

Is it now preferred to use python 3?

e.g.,

apt-get install python3-certbot-nginx

Thanks.

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