Franklin

Multi-domain certificate on nginx and postfix

Basicly for average webhosting HTTPS sites there are 3 kinds of certificates: Single-domain, Wildcard or Multi-domain. Here I will explain how they are different for normal use and how to get and implement the awesome and completely free multi-domain certificate from Let’s Encrypt on your nginx and postfix installations.

tl;dr – skip to Certificate

  1. Certificate types
  2. — SNI (Server Name Indication)
  3. Certificate
  4. — Install Let’s Encrypt
  5. — Generate DH param file
  6. — Generation command
  7. — Include multiple sites
  8. — Scripting
  9. Postfix configuration
  10. nginx configuration – server
  11. nginx configuration – website
  12. Check your installation
  13. Changelog
  14. Related stuff

Certificate types

Single-domain is your old-school TLS certificate. You generate a CSR file with the Common-Name (CN) set to your domain and your CA returns you a signed certificate for your domainname and often also the ‘www’ prefix. Ranging from $30 to $100 per year.

Wildcard is the one that matches only the subdomains by setting the CN to *.hostname hence the wildcard naming. To include the main hostname you need to include it in the CSR’s Subject Alternative Name field or SAN in short. Pretty much the same as a single-domain cert but quiet costly, from $90 to $400 on each yearly renewal.

Multi-domain or SAN (Subject Alternative Name) is a certificate that validates on all included hostnames. You have to include the CN hostname in the SAN list and for the free Let’s Encrypt it’s limited to 100 hostnames. Most paid SAN-certificates are limited to only 3 hostnames around $30 a year and they charge $10 per year for each additional one. They can be hostname and sub.domain, but really you can add any hostname you like if you wish to use one certificate for multiple sites. Before SNI came around this was the only way to secure multiple domainnames on one shared IP-address.

— SNI (Server Name Indication)

Then there is SNI or Server Name Indication. Without SNI-support the webserver receives the requested domainname in the Host-header only after the certificate exchange, because that’s when the headers are sent. Therefore you could only have one certificate per IP-address. With SNI however, the browser sends the hostname it wants during the handshake before the actual encryption happens. Then the webserver can select and send over the certificate that matches the requested hostname. This is pretty cool because each site can have it’s own certification while you still have only one IP-address to share with all of them.

The downsite of SNI is that is not supported by any Internet Explorer on Windows XP, the default browser in Android 2.3 and earlier, Safari in iOS 3 and before and Java 6 and 7. But then again, those are way to vulnerable to be still around! Using them would totally undermine the HTTPS security anyway.

Another minor problem could be the fact that the requested hostname is sent to the server unencrypted. An attacker or logger somewhere along the route could see which domainname you requested. I say minor because without SNI even though the attacker can’t see the requested hostname, they can simply call the IP and get the same certificate you requested, including the actual hostname. So nothing really changes on privacy.

Certificate

These instructions assume you have Ubuntu 16.04 installed with nginx and Postfix. They also help you pass the Qualys SSL Server Test.

— Install Let’s Encrypt

First install Let’s Encrypt:

sudo apt install letsencrypt

Yep that was all it takes, now create the certificate. The one I’ll generate is for this domain ‘frankl.in’ and its www prefix.

— Generate DH param file

You need to generate strong DH parameters. For now (2016) setting 2048 bits should be fine. Lower values are already cracked or are going to be pretty soon. However, higher values might slow down the load time of your website a lot on slow devices.

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

— Generation command

It is important to note that the first domain of the list will be the certificate’s Common Name or CN. That is the domain you see first when you click the lock in a browser to view the HTTPS details.

Also make sure the /.well-known path is not blocked by the nginx config. Let’s Encrypt stores a validation file there that it attempts to access on port 80 from a remote server during the certificate creation process.

Alright, let’s start by running this command:

sudo letsencrypt certonly \
 --rsa-key-size 4096 \
 --email your@mail.here \
 --webroot \
 -w /home/site1/public/ \
  -d frankl.in \
  -d www.frankl.in

What is all that?

  • --rsa-key-size 4096 — Set the private key size to 4096 bits which is very secure and required given all the recent breaches. Nowadays 2048 is the minimum by standards and is expected to get cracked soon while anything lower is already cracked.
  • --email your@mail.here — You will receive important certificate-related notices on this address.
  • --webroot — We are going to specify website paths and their domains. This will write the verification file to /home/site1/public/.well-known/ for the verification server to access, but only for the hostnames frankl.in and www.frankl.in.
  • -w — The path where the website’s public files are.
  • -d — Include a hostname (domainname) for this site.

— Include multiple sites

Now the interesting part is you can include multiple sites in one singular certificate! Example:

sudo letsencrypt certonly \
 --rsa-key-size 4096 \
 --email your@mail.here \
 --webroot \
 -w /home/site1/public/ \
  -d frankl.in \
  -d www.frankl.in \
 -w /home/site2/public/ \
  -d myhostname.net \
  -d v4.myhostname.net \
  -d v6.myhostname.net \
  -d www.myhostname.net

This will include all these hostnames in the certificate and write the verification file to /home/site1/public/.well-known/ for frankl.in and related and write the file to /home/site2/public/.well-known/ for myhostname.net and its subdomains.

When everything went okay it will tell you where the certificate is stored, that will be /etc/letsencrypt/live/frankl.in/fullchain.pem in my example. The domain in the path is again the Common Name (CN), the first hostname in your list.

— Scripting

When you run this command it should present you the Let’s Encrypt terms of service. That can be annoying for automation purposes in which case you can include the --agree-tos argument to avoid that text.

Postfix configuration

Config /etc/postfix/main.cf

Note: these lines disable SSLv3 support, see here for details. Should only affect really old clients.

myhostname = mx.domain.tld
smtpd_tls_cert_file = /etc/letsencrypt/live/domain.tld/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/domain.tld/privkey.pem
smtpd_tls_CAfile = /etc/letsencrypt/live/domain.tld/chain.pem

tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA
tls_ssl_options = NO_COMPRESSION
smtpd_use_tls = yes
smtpd_tls_security_level = may
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_mandatory_ciphers = high
smtpd_tls_loglevel = 0

smtp_tls_security_level = may
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
smtp_tls_mandatory_ciphers = high
smtp_tls_mandatory_exclude_ciphers = RC4, MD5
smtp_tls_exclude_ciphers = aNULL
smtp_tls_note_starttls_offer = yes
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_loglevel = 1

# only needed when using smtp auth
smtpd_tls_auth_only = yes

sudo service postfix restart

nginx configuration – server

Config SSL basics in /etc/nginx/conf.d/ssl.conf so you don’t have to include them with each site config.

ssl_session_cache shared:SSL:10m;

ssl_session_timeout 5m;
ssl_session_tickets off;
ssl_buffer_size 4k;
ssl_ecdh_curve secp384r1;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/dhparam.pem;

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4;
resolver_timeout 15s;

SSL-stapling will make HTTPS requests a bit faster because the server will take care of retrieving the OCSP responses for the entire certificate chain and serve that to the clients. However, the first hit on your site right after restarting nginx will be slow because the server does need to download the OCSP chain once. That is also why you need the resolver to find the OCSP server that is stored in the certificate. Here I include Google’s public DNS, which should be just fine.

nginx configuration – website

Config HTTPS site in /etc/nginx/sites-enabled/sitename.

# Site 1 - domain.tld
server {
  listen 80;
  listen 443 ssl http2;

  server_name .domain.tld;
  root /home/domain/public;

  ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;
  ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/domain.tld/cert.pem;
}

# Site 2 - other.tld
server {
  listen 80;
  listen 443 ssl http2;

  server_name .other.tld;
  root /home/other/public;

  ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;
  ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/domain.tld/cert.pem;
}

sudo service nginx restart

Check your installation

Now before you run off to some tools to check your setup, you should first visit the domain you are going to check at least once in your own browser. That will cause nginx to retrieve the OCSP responses and cache them. Otherwise the test will report that stapling is not working.

Test nginx with Qualys SSL Server Test.

And test Postfix with CheckTLS.

Changelog

2017-04-08
Changed cert-only to certonly

2016-08-21
Full rewrite to use LetsEncrypt and HTTP/2

Buy me a coffee

2 Comments

  1. Thanks so much for documenting these procedures.

    I want to point out one small issue, however. The parameter “cert-only” doesn’t or no longer works. It should be “certonly” instead.

Leave a Reply