How-to guide · Install on a server

How to install a certificate in Apache httpd

In one sentence

Apache httpd serves your certificate with its full chain from one SSLCertificateFile, with a matching key, and you have proved it from outside.

Task
Install on a server
Platforms
  • Linux
  • Apache
Who it is for
Level
Practitioner · 5 steps
On this page
  1. Before you start
  2. Steps
  3. 1. Put the full chain in one file
  4. 2. Check the key before Apache sees it
  5. 3. Enable mod_ssl
  6. 4. Configure the virtual host
  7. 5. Test and reload
  8. Check it worked
  9. If something goes wrong
  10. Why this works
  11. Go deeper

Before you start

  • Your server certificate (the leaf certificate) and the CA’s intermediate certificate or certificates, in PEM format.
  • The private key you created with the CSR. It stays on the server.
  • Apache httpd 2.4.8 or later (check with apache2 -v or httpd -v), root access, and port 443 open.
  • If you use Let’s Encrypt, certbot can do all of this for you with its Apache plugin.

Why it matters: Apache sends exactly the chain you configure. A missing intermediate often goes unnoticed in desktop browsers but breaks curl, Java, Python and many phones.

Steps

1. Put the full chain in one file

Since Apache 2.4.8, SSLCertificateFile can hold the intermediates too, “sorted from leaf to root”, and this “obsoletes SSLCertificateChainFile” (mod_ssl). Put the leaf first, then the intermediates, and leave the root out (build the full chain):

sudo mkdir -p /etc/ssl/example
cat www.example.com.crt intermediate.crt | sudo tee /etc/ssl/example/fullchain.pem > /dev/null
sudo install -m 600 -o root -g root www.example.com.key /etc/ssl/example/privkey.pem

On RHEL, Fedora and their relatives, use /etc/pki/tls/certs/ and /etc/pki/tls/private/ instead, so the files get the SELinux labels httpd is allowed to read; run sudo restorecon -Rv /etc/pki/tls after copying.

2. Check the key before Apache sees it

openssl x509 -in /etc/ssl/example/fullchain.pem -noout -pubkey | openssl sha256
sudo openssl pkey -in /etc/ssl/example/privkey.pem -pubout | openssl sha256

The two hashes must be identical. This step matters more with Apache than with nginx: in our test, apachectl configtest said Syntax OK with a mismatched key, and the reload then stopped the whole server (step 5 has the log).

3. Enable mod_ssl

On Debian and Ubuntu:

sudo a2enmod ssl
Considering dependency mime for ssl:
Module mime already enabled
Considering dependency socache_shmcb for ssl:
Enabling module socache_shmcb.
Enabling module ssl.
...

Debian’s ports.conf already adds Listen 443 when mod_ssl is loaded. On the RHEL family, install it with sudo dnf install mod_ssl; it adds /etc/httpd/conf.d/ssl.conf.

4. Configure the virtual host

Debian and Ubuntu: /etc/apache2/sites-available/example.com-ssl.conf. RHEL family: a file in /etc/httpd/conf.d/.

<VirtualHost *:443>
    ServerName www.example.com
    ServerAlias example.com
    DocumentRoot /var/www/example

    SSLEngine on
    SSLCertificateFile    /etc/ssl/example/fullchain.pem
    SSLCertificateKeyFile /etc/ssl/example/privkey.pem
</VirtualHost>

<VirtualHost *:80>
    ServerName www.example.com
    ServerAlias example.com
    Redirect permanent / https://www.example.com/
</VirtualHost>

Every name in ServerName and ServerAlias must also be in the certificate’s Subject Alternative Name. Leave out SSLCertificateChainFile. For protocol versions, set SSLProtocol -all +TLSv1.2 +TLSv1.3 once in the server-wide SSL configuration (mods-available/ssl.conf or conf.d/ssl.conf). The built-in default is all -SSLv3, which still includes TLS 1.0 and 1.1. More in How to configure TLS securely.

Enable the site on Debian and Ubuntu:

sudo a2ensite example.com-ssl

5. Test and reload

sudo apachectl configtest
Syntax OK
sudo systemctl reload apache2     # RHEL family: sudo systemctl reload httpd

Syntax OK checks the configuration, not whether the key fits the certificate. With a wrong key, our graceful reload logged this (timestamps removed) and Apache exited:

[ssl:emerg] AH02565: Certificate and private key intranet.example.com:443:0 from /etc/ssl/example/fullchain.pem and /etc/ssl/example/privkey.pem do not match
[:emerg] AH00020: Configuration Failed, exiting

So always run step 2 before a reload, and watch sudo tail /var/log/apache2/error.log (RHEL: /var/log/httpd/error_log) afterwards. If Apache did stop, fix the files, repeat step 2 and start it again with sudo systemctl start apache2 (RHEL family: httpd).

Check it worked

From another machine if you can:

openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | grep -E '^ [0-9] s:|^   i:|Verify return code'

Our test server (Apache 2.4.58 on Ubuntu 24.04, private test CA, so we added -CAfile corp-root.crt):

 0 s:CN = intranet.example.com
   i:C = BE, O = Example Corp, CN = Example Corp Issuing CA 1
 1 s:C = BE, O = Example Corp, CN = Example Corp Issuing CA 1
   i:C = BE, O = Example Corp, CN = Example Corp Root CA
Verify return code: 0 (ok)

Two certificates, leaf first, code 0. Then curl -sS -o /dev/null -w '%{http_code}\n' https://www.example.com/ should print a status code. You can paste the -showcerts output into the chain checker:

Try it: Chain checkerOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

Every certificate the server sends, leaf first. The output of openssl s_client -showcerts -connect example.com:443 works as it is: we ignore the text around the certificates.

If something goes wrong

  • Only one certificate is sent, and clients report unable to verify the first certificate, unable to get local issuer certificate or PKIX path building failed. With the leaf alone, our test passed configtest, started, and served Verify return code: 21. Rebuild the file (step 1).
  • AH02565: Certificate and private key … do not match. The key belongs to another certificate, or the file starts with the intermediate instead of the leaf; we got this message both ways. Check the order and run step 2.
  • SSLCertificateFile: file '…' does not exist or is empty. configtest catches this one (AH00526: Syntax error). Fix the path.
  • The old certificate is still served. Another virtual host matches first. Run sudo apachectl -S to list the virtual hosts and which one is the default for port 443.
  • NET::ERR_CERT_COMMON_NAME_INVALID. The certificate’s Subject Alternative Name does not include the name in the address bar, or a different virtual host answered. Check with the hostname matcher.
  • NET::ERR_CERT_DATE_INVALID. The certificate expired, or the client’s clock is wrong.

Why this works

During the TLS handshake, mod_ssl sends the certificates from SSLCertificateFile in file order and signs the handshake with the matching key. The client builds a certificate chain from the leaf, through the intermediates you sent, to a root it already trusts; it cannot use a root you send, and many clients cannot fill in a missing intermediate. See Certificate chains and path validation, The TLS 1.3 handshake and Anatomy of an X.509 certificate.

Go deeper

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9