<!-- https://pkizerotohero.com/guides/install-certificate-apache/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Install a TLS certificate in Apache httpd 2.4: the full chain in SSLCertificateFile, the matching key, mod_ssl, then test and reload safely.

Source: https://pkizerotohero.com/guides/install-certificate-apache/ · Last reviewed: 2026-10-20

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](https://pkizerotohero.com/guides/#task-install)

Platforms

- Linux
- Apache

Who it is for

- [IT ops](https://pkizerotohero.com/start/#itops)
- [Developer](https://pkizerotohero.com/start/#developer)

Level

Practitioner · 5 steps

## Before you start

- Your server certificate (the [leaf certificate](https://pkizerotohero.com/handbook/glossary/#leaf-certificate)) and the CA’s [intermediate certificate](https://pkizerotohero.com/handbook/glossary/#intermediate-certificate) or certificates, in PEM format.
- The [private key](https://pkizerotohero.com/handbook/glossary/#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](https://pkizerotohero.com/guides/certbot-lets-encrypt/) 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](https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile)). Put the leaf first, then the intermediates, and leave the root out ([build the full chain](https://pkizerotohero.com/guides/build-fullchain/)):

```bash
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

```bash
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:

```bash
sudo a2enmod ssl
```

```text
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/`.

```apacheconf
<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](https://pkizerotohero.com/guides/tls-configuration/).

Enable the site on Debian and Ubuntu:

```bash
sudo a2ensite example.com-ssl
```

### 5. Test and reload

```bash
sudo apachectl configtest
```

```text
Syntax OK
```

```bash
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:

```text
[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:

```bash
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`):

```text
 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:

*Interactive tool: Chain checker (runs in the browser at [/tools/chain/](https://pkizerotohero.com/tools/chain/))*

## If something goes wrong

- **Only one certificate is sent, and clients report [`unable to verify the first certificate`](https://pkizerotohero.com/errors/openssl-verify-error-21/), [`unable to get local issuer certificate`](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/) or [`PKIX path building failed`](https://pkizerotohero.com/errors/java-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`](https://pkizerotohero.com/errors/chrome-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](https://pkizerotohero.com/tools/hostname/).
- **[`NET::ERR_CERT_DATE_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/).** The certificate expired, or the client’s clock is wrong.

Note

Protect the key

Keep the key readable by root only (mode 600). Apache reads it as root at start-up, before it switches to its unprivileged user. Never commit it to a repository or bake it into an image.

## Why this works

During the [TLS handshake](https://pkizerotohero.com/handbook/glossary/#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](https://pkizerotohero.com/handbook/glossary/#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](https://pkizerotohero.com/handbook/certificate-chains/), [The TLS 1.3 handshake](https://pkizerotohero.com/handbook/tls-handshake/) and [Anatomy of an X.509 certificate](https://pkizerotohero.com/handbook/x509-certificates/).

## Go deeper

- [mod\_ssl reference](https://httpd.apache.org/docs/2.4/mod/mod_ssl.html): `SSLCertificateFile`, `SSLCertificateKeyFile`, `SSLProtocol` and their defaults.
- [Apache SSL/TLS how-to](https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html): basic and stricter configurations from the Apache project.
- [RFC 9846, section 4.5.1](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.1): the TLS 1.3 rules for the order of the certificate list.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/install-certificate-apache/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 13: Certificate chains and path validation](https://pkizerotohero.com/handbook/certificate-chains/)
- [Chapter 15: The TLS 1.3 handshake](https://pkizerotohero.com/handbook/tls-handshake/)
- [Chapter 8: Anatomy of an X.509 certificate](https://pkizerotohero.com/handbook/x509-certificates/)

### Error pages

- [verify error:num=21:unable to verify the first certificate](https://pkizerotohero.com/errors/openssl-verify-error-21/)
- [SSL certificate problem: unable to get local issuer certificate](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/)
- [NET::ERR\_CERT\_COMMON\_NAME\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-common-name-invalid/)
- [NET::ERR\_CERT\_DATE\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/)
- [PKIX path building failed … unable to find valid certification path to requested target](https://pkizerotohero.com/errors/java-pkix-path-building-failed/)

### Tools

- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.
- [Certificate decoder](https://pkizerotohero.com/tools/decoder/): Paste a PEM certificate and read every field in plain words.
- [Hostname matcher](https://pkizerotohero.com/tools/hostname/): Test whether a name matches a certificate's SANs and wildcards.

### Topics

- [TLS and HTTPS](https://pkizerotohero.com/topics/tls/)

## More guides: Install on a server

- [How to build the full chain file your server needs](https://pkizerotohero.com/guides/build-fullchain/)
- [How to install a certificate in nginx](https://pkizerotohero.com/guides/install-certificate-nginx/)
- [How to install a certificate in IIS](https://pkizerotohero.com/guides/install-certificate-iis/)
- [How to install a certificate in Tomcat and other Java servers](https://pkizerotohero.com/guides/install-certificate-tomcat/)
