Linux servers.

Installing Dual TLS Certificates and Running TLS

Steps Toward The Goal

On earlier pages I showed how to set up a Google Compute Engine virtual machine, run FreeBSD, and set up a basic Apache/PHP web server. Now let's set up HTTPS!

On this page I will:
1: Set up both RSA and ECC key pairs.
2: Obtain Let's Encrypt certificates for both.
3: Set up automatic renewal.

On the final page I will tune the Apache HTTP headers.

Why TLS?

TLS, the successor to SSL, authenticates a web server and provides confidentiality and integrity of the data.

Try Google Cloud Platform and receive $50

In July 2018, Google's Chrome browser began marking HTTP pages as Not Secure. Google had warned the previous year that Chrome would eventually do this. Once Chrome does something, the other browsers quickly follow.

Google also announced back in 2014 that HTTPS already had a slight effect on a page's rank in search results, and the weight would probably increase. In other words, if you don't run HTTPS, you will be ranked lower in search results. Similarly, once Google's search algorithm starts doing something, the other search engines follow.

So, even if a web site is purely informational, you still want to set up TLS.

Public-Key Security

Asymmetric cryptography, also called public-key cryptography, bases its security on a trapdoor function. That's a function that is easy to compute in one direction but enormously difficult to compute in the opposite direction. RSA, which was developed in the late 1970s, relies on the difficulty of factoring the product of two very large prime numbers.

It is easy to multiply integers, even ones with hundreds of digits, but it is impractically difficult to start with such a product and figure out which two large prime numbers went into it.

Then people worried: what if someone develops a general-purpose quantum computer? Shor's Algorithm could quickly factor very large numbers if you have such a platform on which to run it.

Around this time, people were starting to use mobile devices for Internet access, but smart phones with fast multi-core CPUs had not yet appeared.

So, Elliptic Curve Cryptography or ECC suddenly became popular. Its trapdoor function is based on a discrete logarithm, entirely different from RSA's factoring. Analysis by NSA and NIST showed that ECC provides equal security with much smaller keys than RSA, requiring much less computation.

So, there were two advantages for ECC: much higher performance, and expected resistance to sudden obsolescence when quantum computers appear. Certificate authorities began issuing dual certificates for sites: one based on ECC which newer clients would prefer for performance, and RSA as a fall-back.

Since then, cryptographers have discovered that ECC will be just as susceptible as RSA to attack by quantum computers. But ECC still has a performance advantage.

Let's Encrypt

Let's Encrypt is a certificate authority founded by the Electronic Frontier Foundation, the Mozilla Foundation, the University of Michigan, Akamai Technologies, and Cisco Corporation.

The Let's Encrypt CA gives you a certificate that's good for 90 days. The short certificate lifetime makes automated renewal important.

and Linux

ACME, the Automated Certificate Management Environment, is a protocol for interacting with the Let's Encrypt CA. You use the certbot program to carry out the various steps. Install the py36-certbot package to get it.

Creating the RSA Certificate

This is very easy! For my site I simply did this:

# certbot certonly --webroot \
	-w /usr/local/www/htdocs/ \
	-d -d

I told it where the web document root was located, and then listed the domain names. Yes, clients will be redirected from to as needed, but they must first make a secure connection to the server and ask for the longer name with "www.".

There is some narrative output, and you are asked for an email address in case they need to send you an urgent renewal or security notice. You agree to the terms of service, then answer whether it's OK to share your email address with the EFF, and then it's done!

I didn't tell it anything about the cryptography, so it generated and installed a 2048-bit RSA key pair. Returning to this project later, I wanted 4096-bit RSA keys. Just add this option:
--rsa-key-size 4096

What Did You Get?

The key pair, certificate, and associated files have been created and saved under /usr/local/etc/letsencrypt. Let's see what files are there.

# cd /usr/local/etc/letsencrypt
# tree -F
|-- accounts/
|   |--
|   |   `-- directory/
|   |       `-- d72ae2a5cf968487add7cbdece6e3aab/
|   |           |-- meta.json
|   |           |-- private_key.json
|   |           `-- regr.json
|   `--
|       `-- directory/
|           `-- 5f78856fecb3b21a157f41d986716e2c/
|               |-- meta.json
|               |-- private_key.json
|               `-- regr.json
|-- archive/
|   `--
|       |-- cert1.pem
|       |-- chain1.pem
|       |-- fullchain1.pem
|       `-- privkey1.pem
|-- csr/
|   `-- 0000_csr-certbot.pem
|-- keys/
|   `-- 0000_key-certbot.pem
|-- live/
|   `--
|       |-- README
|       |-- cert.pem -> ../../archive/
|       |-- chain.pem -> ../../archive/
|       |-- fullchain.pem -> ../../archive/
|       `-- privkey.pem -> ../../archive/
`-- renewal/

14 directories, 18 files

Creating the ECC Certificate

Now I will create an ECC private key and certificate. You need a reasonably recent version of openssl. See what yours is capable of:

$ openssl ecparam -list_curves | less

I will use elliptic curve P-384, designated secp384r1. That is the strongest elliptic curve included in NSA Suite B cryptography, and therefore many people interpret that to mean they should not use secp521r1. Browsers support secp384r1, they no longer support secp521r1. See the U.S. NIST SP 800-57 "Recommendation for Key Management" for the secp384r1 definition, and this comparison of relative strength against brute-force attack:

Key Length in Bits for Approximately Equal Resistance to Brute-Force Attacks, per NIST/NSA
80 80 1024 160
112 112 2048 224
128 128 3072 256
192 192 7680 384
256 256 15,360 521

The first time I used certbot I let it generate an RSA key pair. Now I need to generate an ECC certificate-signing request. Start by generating an ECC private key:

$ openssl ecparam -genkey -name secp384r1 | openssl ec -out ecc-privkey.pem

Before generating the CSR or Certificate Signing Request, I must slightly change the OpenSSL configuration to enable multiple names, both and

  1. Edit /etc/ssl/openssl.cnf.
  2. Find and uncomment the entry:
    req_extensions = v3_req
  3. Add a line below that:
    subjectAltName = @alt_names
  4. Add a new stanza at the end of the file:
    ## Added
    DNS.1 =
    DNS.2 =

Now I can generate the CSR:

$ openssl req -new -sha256 -key ecc-privkey.pem -nodes -outform pem -out ecc-csr.pem
[... output deleted ...]
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Indiana
Locality Name (eg, city) []:West Lafayette
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Cromwell International
Organizational Unit Name (eg, section) []:Cromwell International
Common Name (e.g. server FQDN or YOUR name) []
Email Address []
[... output deleted ...]

Now ask Let's Encrypt to generate a certificate. This time we pass it our new CSR.

$ certbot certonly -w /usr/local/www/htdocs \
	-d -d \
	--email \
	--csr ecc-csr.pem --agree-tos

This gives us three new files in the local directory:

0000_cert.pem = The certificate itself
0000_chain.pem = The signing chain
0001_chain.pem = The full chain including our certificate

Storing Both Certificates

It took some research after initial frustration to learn that certbot is very fussy about file names when it comes to renewal.

You have archive/ with the actual files, and live/ with symbolic links pointing to them. You can rename the archive and live directories, but the files must have specific names.

The "archive" directory, or whatever you end up naming it, must have files named precisely cert1.pem, chain1.pem, fullchain1.pem, and privkey1.pem.

The "live" directory, again possibly renamed, must have symbolic links with those names minus the "1", precisely cert.pem, chain.pem, fullchain.pem, and privkey.pem.

First, I rearranged the existing hierarchy under /usr/local/etc/letsencrypt:

  1. Rename the existing "archive" and "live" directories rsa-archive and rsa-live.
  2. Recreate the symbolic links in rsa-live/ to point to the relocated "archive" files.
  3. Edit renewal/ and make corresponding changes to the paths.
  4. Rename that file
  5. Verify that renewal still works:
    certbot renew --dry-run

Next, create new directories ecc-archive/ and ecc-live/ and:

  1. Move the ECC files into the ecc-archive area, changing the names as required.
  2. Create the symbolic links under ecc-live.
  3. Rename the RSA files in csr and keys, and move the corresponding ECC files into those areas.
  4. Copy the file in renewal to and edit that new file so it refers to the ECC files.

The result of all this is the following, where:
yellow indicates renamed files and changed file content,
green indicates (re)created symbolic links,
blue indicates new files and directories, and
grey indicates unchanged files

# cd /usr/local/etc/letsencrypt
# tree -F
|-- accounts/
|   |--
|   |   `-- directory/
|   |       `-- d72ae2a5cf968487add7cbdece6e3aab/
|   |           |-- meta.json
|   |           |-- private_key.json
|   |           `-- regr.json
|   `--
|       `-- directory/
|           `-- 5f78856fecb3b21a157f41d986716e2c/
|               |-- meta.json
|               |-- private_key.json
|               `-- regr.json
|-- csr/
|   |-- ecc-csr.pem
|   `-- rsa-csr.pem
|-- ecc-archive/
|   `--
|       |-- cert1.pem
|       |-- chain1.pem
|       |-- fullchain1.pem
|       `-- privkey1.pem
|-- ecc-live/
|   `--
|       |-- cert.pem -> ../../ecc-archive/
|       |-- chain.pem -> ../../ecc-archive/
|       |-- fullchain.pem -> ../../ecc-archive/
|       `-- privkey.pem -> ../../ecc-archive/
|-- keys/
|   |-- ecc-privkey.pem
|   `-- rsa-privkey.pem
|-- renewal/
|   `--
|-- rsa-archive/
|   `--
|       |-- cert1.pem
|       |-- chain1.pem
|       |-- fullchain1.pem
|       `-- privkey1.pem
`-- rsa-live/
        |-- README
	|-- cert.pem -> ../../rsa-archive/
	|-- chain.pem -> ../../rsa-archive/
	|-- fullchain.pem -> ../../rsa-archive/
	`-- privkey.pem -> ../../rsa-archive/

18 directories, 28 files

# cat renewal/
# renew_before_expiry = 30 days
version = 0.18.2
archive_dir = /usr/local/etc/letsencrypt/rsa-archive/
cert = /usr/local/etc/letsencrypt/rsa-live/
privkey = /usr/local/etc/letsencrypt/rsa-live/
chain = /usr/local/etc/letsencrypt/rsa-live/
fullchain = /usr/local/etc/letsencrypt/rsa-live/

# Options used in the renewal process
authenticator = webroot
installer = None
account = 5f78856fecb3b21a157f41d986716e2c
webroot_path = /usr/local/www/htdocs,
[[webroot_map]] = /usr/local/www/htdocs = /usr/local/www/htdoc

Now test this with certbot renew --dry-run.

Examining the Certificates

We can use the openssl tool to parse and display the certificates.

# openssl x509 -in ecc-live/ -text -noout
        Version: 3 (0x2)
        Serial Number:
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
            Not Before: Oct 10 18:22:54 2017 GMT
            Not After : Jan  8 18:22:54 2018 GMT
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
            X509v3 Subject Key Identifier: 
            X509v3 Authority Key Identifier: 

            Authority Information Access: 
                OCSP - URI:
                CA Issuers - URI:

            X509v3 Subject Alternative Name: 
            X509v3 Certificate Policies: 
                  User Notice:
                    Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at

    Signature Algorithm: sha256WithRSAEncryption

# openssl x509 -in rsa-live/ -text -noout
        Version: 3 (0x2)
        Serial Number:
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
            Not Before: Sep 27 19:01:00 2017 GMT
            Not After : Dec 26 19:01:00 2017 GMT
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
            X509v3 Subject Key Identifier: 
            X509v3 Authority Key Identifier: 

            Authority Information Access: 
                OCSP - URI:
                CA Issuers - URI:

            X509v3 Subject Alternative Name: 
            X509v3 Certificate Policies: 
                  User Notice:
                    Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at

    Signature Algorithm: sha256WithRSAEncryption

Automated Renewal

The problem is that the certbot program cannot renew a certificate for an ECC public key. It really doesn't renew anything, its "renewal" process generates a new RSA key pair and obtains a certificate with the new public key.

The --csr option to specify a certificate signing request works only with the certonly subcommand, as described on the certbot manual page.

If you create a configuration file in the renewal directory for your ECC key pair, it will simply create an additional RSA key pair and create a certificate for the public key. It places the results in files that you expect to hold ECC keys and certificate.

Be careful developing your solution

You can only request 5 duplicate certificates per week. So, develop any scripts with the certificate renewals commented out, until you're pretty sure that it's going to work. See the rate limit documentation for details.

The solution for automated renewal

Create the following script in /root/renew-all-certs:



## "Renew" the RSA certificate -- this actually generates a fresh
## 4096-bit RSA key pair and creates a certificate from the public key.
echo "RSA renewal ====================================" > $LOGFILE
certbot --force-renewal --rsa-key-size 4096 renew >> $LOGFILE 2>&1
## Use the Certificate-Signing Request for the existing ECC
## public key, and request a new certificate.
## You can read a CSR with:
## $ openssl req -noout -text -in /path/to/csr.pem
echo "ECC renewal ====================================" >> $LOGFILE
certbot certonly --non-interactive --webroot \
	-w /usr/local/www/htdocs \
	-d -d \
	--email \
	--csr /usr/local/etc/letsencrypt/csr/ecc-csr.pem \
	--agree-tos >> $LOGFILE 2>&1
## The above creates three files in the local directory.
## Move them into place.
echo "Installing files ===============================" >> $LOGFILE
mv -fv 0000_cert.pem  $ARCHIVE/cert.pem >> $LOGFILE
mv -fv 0000_chain.pem $ARCHIVE/chain.pem >> $LOGFILE
mv -fv 0001_chain.pem $ARCHIVE/fullchain.pem >> $LOGFILE
## Restart the web server so it's now using the new files.
echo "Apache restart =================================" >> $LOGFILE
/usr/local/etc/rc.d/apache24 stop >> $LOGFILE 2>&1
/usr/local/etc/rc.d/php-fpm stop >> $LOGFILE 2>&1
/usr/local/etc/rc.d/php-fpm start >> $LOGFILE 2>&1
/usr/local/etc/rc.d/apache24 start >> $LOGFILE 2>&1 

Now set up a crontab job to run your script every so often. I generate new certificates every Friday morning, 1040 UTC:

# crontab -l
#       day of       day of
# min hr month month  week  command
  40  10   *     *    fri   /root/renew-all-certs 

Here is the resulting output:

RSA renewal ====================================
Saving debug log to /var/log/letsencrypt/letsencrypt.log

Processing /usr/local/etc/letsencrypt/renewal/
Plugins selected: Authenticator webroot, Installer None
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for
http-01 challenge for
Waiting for verification...
Cleaning up challenges

new certificate deployed without reload, fullchain is


Congratulations, all renewals succeeded. The following certs have been renewed:
  /usr/local/etc/letsencrypt/rsa-live/ (success)
ECC renewal ====================================
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Performing the following challenges:
http-01 challenge for
http-01 challenge for
Using the webroot path /usr/local/www/htdocs for all unmatched domains.
Waiting for verification...
Cleaning up challenges
Server issued certificate; certificate written to /root/0000_cert.pem
Cert chain written to <fdopen>
Cert chain written to <fdopen>
 - Congratulations! Your certificate and chain have been saved at:
   Your cert will expire on 2018-05-17. 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:
   Donating to EFF:          

Installing files ===============================
0000_cert.pem -> /usr/local/etc/letsencrypt/ecc-live/
0000_chain.pem -> /usr/local/etc/letsencrypt/ecc-live/
0001_chain.pem -> /usr/local/etc/letsencrypt/ecc-live/
Apache restart =================================
Stopping apache24.
Waiting for PIDS: 22998.
Stopping php_fpm.
Waiting for PIDS: 22989.
Performing sanity check on php-fpm configuration:
[06-May-2021 18:45:58] NOTICE: configuration file /usr/local/etc/php-fpm.conf test is successful

Starting php_fpm.
Performing sanity check on apache24 configuration:
Syntax OK
Starting apache24. 

Enabling HTTPS

Edit the httpd.conf configuration file and add the following to the end of the file, changing the hostname and file system paths as needed. Make sure to use the file fullchain.pem, which contains the full certificate chain, and not cert.pem which has just your site's certificate.

# Put these directives at the global level:
LoadModule ssl_module libexec/apache24/
Listen 443

# Put these within individual VirtualHost stanzas
# if you are hosting several sites on one server.
<VirtualHost *:443>
    SSLEngine on
    # ECC secp384r1
    SSLCertificateFile "/usr/local/etc/letsencrypt/ecc-live/"
    SSLCertificateKeyFile "/usr/local/etc/letsencrypt/ecc-live/"
    # RSA
    SSLCertificateFile "/usr/local/etc/letsencrypt/rsa-live/"
    SSLCertificateKeyFile "/usr/local/etc/letsencrypt/rsa-live/"

Initial Test

Restart Apache and verify that you can connect with both HTTP and HTTPS.

You could run a Qualys server test, but at this point it would probably get a "B" grade at best. We will tune the configuration, but first let's set up redirection.

Redirect to HTTPS and Remove "www."

The goal is to accept all connections, redirecting all of these:
to this:

Add the following to your .htaccess file in the root of the web site. Bold shows additions here. Don't duplicate the RewriteEngine line.

# Do NOT duplicate the following line if it
# already exists earlier in the file.
RewriteEngine on

# Remove "www." and redirect HTTP to HTTPS
# Use a standard variable and a tagged regular expression to
# replace the URL with "https://", the host name, and the
# path minus any leading "www.":
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
# If they asked for the non-www name but with HTTP,
# build a new HTTPS URL with the host name and path:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# We added the following in an earlier step.
# If the client asks for a non-existent *.html file, rewrite it so
# the bad name isn't passed to the PHP engine causing a "Primary script
# unknown" log entry and a plain "File not found" page.
RewriteCond %{REQUEST_FILENAME} \.html$
RewriteRule (.*) - [H=text/html]

Yes, I did the redirection with the site-specific .htaccess file. I could have instead done it with slightly different syntax in the server-wide httpd.conf configuration file. For the single site, given its size and traffic, I didn't see a big advantage of one method over the other.

Now test all combinations of the redirections: http vs https, www. vs not, and include requests for nonexistent files and directories.

Improving the TLS Configuration

Mozilla Config Generator Apache SSL Howto

Apache has a good SSL/TLS how-to document. Even more useful, Mozilla has a configuration generator. You select your server, its version, and your OpenSSL versions, and then you select the security profile.

Which security profile should you use? It depends...

Let's say you're setting up a server for use within your organization, and you have full control of the desktop systems and any portable laptops that could be used to connect in from outside. I recommend the strictest "Modern" profile for that case. All your client machines will need to be fairly current, but that should already be the case.

However, let's say that you want to be open to most all clients from the public. That's my situation. I have ads on my site. While it would be nice if everyone used up-to-date operating systems and browsers, I don't want to block or even inconvenience people with somewhat outdated platforms.

I used the "Intermediate" profile as a starting point. Here is what I added toward the end of the httpd.conf configuration file, before and outside the VirtualHost stanza, so it will apply to all virtually hosted web sites I eventually set up on this server. The cipher suite is usually one enormously long line with no whitespace. That's hard to read and much harder to modify, so I prefer to split it across multiple lines. Just make sure each backslash is the last character on its line.

# TLS only, no SSL
SSLProtocol all -SSLv2 -SSLv3
# Specify ciphers in a preferred order.  I reordered what the configuration
# generator gave me, putting better ciphers first.

SSLHonorCipherOrder     on
# Disable compression and session tickets
SSLCompression          off
SSLSessionTickets       off
# Enable OCSP Stapling
LoadModule socache_shmcb_module libexec/apache24/
SSLUseStapling On
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
# Enable session resumption (caching).
# However do not use large values.  Large advertisers like
# Google and Facebook use that to track users.  Your site will
# look suspicious if it does this.  See, for example:
# 300 seconds (5 minutes) seems reasonable.  That's actually the
# default, no need to specify that, but here's the syntax.
SSLSessionCache "shmcb:logs/ssl_scache"
SSLSessionCacheTimeout 300
# Insist on HSTS or HTTP Strict Transport Security
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

And with that, an A+ evaluation from the Qualys analyzer:

Screenshot of Qualys SSL Labs A+ analysis of a HTTPS web server
More cautious configuration with Nginx

The server supports 3DES as a fallback position if none of the stronger ciphers are supported. That allows connections from IE 8 on XP. While 3DES is flagged in the report as weak, at least at the time that I did this it did not lower the score. Unlike RC4, which capped the grade at B at the time.

DNS CAA or Certification Authority Authorization provides a way to indicate in a DNS record just who is allowed to be a CA for the site. See this Qualys blog for details on what CAA is, and how the CA/Browser Forum has mandated its use.

Google Domains did not yet support CAA records when I started this project in July 2017, as you can see below. Google Cloud DNS, a different product, did.

Screenshot of Google Domains dashboard showing available record types.

By February 2018, they had added CAA records:

Screenshot of Google Domains dashboard with CAA records available.

Let's test it:

$ dig @localhost CAA

; <<>> DiG 9.10.6-P1 <<>> @localhost CAA
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26186
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 4096

;; ANSWER SECTION:	3570	IN	CAA	128 issue ""

;; AUTHORITY SECTION:	78163	IN	NS	78163	IN	NS	78163	IN	NS	78163	IN	NS

;; Query time: 0 msec
;; SERVER: ::1#53(::1)
;; WHEN: Wed Feb 14 14:44:24 EST 2018
;; MSG SIZE  rcvd: 198 

Final Step

I was very happy with this result! There are just a few more things to do.

Proceed to the final step to see how to tune the HTTP(S) headers.

Final step: HTTPS Headers