How-to guide · Install on a server
How to build the full chain file your server needs
In one sentence
You will have one PEM file with your certificate followed by its intermediates in the right order, verified offline and proven to work with a strict client.
- Platforms
- Linux
- nginx
- Apache
- Kubernetes
- Level
- Practitioner · 5 steps
On this page
Before you start
- OpenSSL (Linux, macOS with Homebrew, or Git Bash on Windows).
- Your server certificate as PEM, and whatever the CA sent with it: an intermediate file, a “CA bundle”, or a
.p7b. - The matching private key stays where it is; the chain file never contains it.
A server must send its leaf certificate and every intermediate up to (not including) the root. Browsers often repair a missing intermediate on their own; curl on Linux, Java, Python, Node.js and many mobile apps do not, and fail with errors such as unable to get local issuer certificate.
Steps
1. Identify every certificate you have
for f in *.crt; do
echo "== $f"; openssl x509 -in "$f" -noout -subject -issuer
done
== int.crt
subject=C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1
issuer=C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
== root.crt
subject=C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
issuer=C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
== www.crt
subject=C=BE, O=Example Shop, CN=www.example.com
issuer=C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1
The leaf is the one with your host name. A certificate whose subject equals its issuer is a root; leave it out. For a bundle with several certificates in one file, list them with openssl crl2pkcs7 -nocrl -certfile bundle.pem | openssl pkcs7 -print_certs -noout. A .p7b needs converting first: How to convert between DER and PEM.
2. Find a missing intermediate
If no file has a subject equal to your leaf’s issuer, ask the CA for its current intermediate, or download it from the “CA Issuers” address in your certificate:
openssl x509 -in www.crt -noout -ext authorityInfoAccess
That address usually serves DER; convert it to PEM as shown in the DER guide. Not every certificate has one: the Baseline Requirements only recommend, and no longer require, the Authority Information Access (AIA) extension in publicly trusted TLS certificates (since 4 October 2026). Use the intermediate that came with this certificate. CAs rotate intermediates, and an old chain file with a new leaf fails too.
3. Put them in order and build the file
The order is: your certificate, then the certificate that issued it, then the one that issued that, and so on. Stop before the root. The TLS 1.3 specification, RFC 9846, puts it this way: “The sender’s certificate MUST come in the first CertificateEntry in the list”, and the root “MAY be omitted” (section 4.5.1).
Build the file with OpenSSL rather than plain cat:
{ openssl x509 -in www.crt; openssl x509 -in int.crt; } > fullchain.pem
Each openssl x509 writes one clean PEM block ending in a newline, without Bag Attributes or other text. Plain cat www.crt int.crt works too, unless a file lacks its final newline. Then the boundary breaks:
-----END CERTIFICATE----------BEGIN CERTIFICATE-----
OpenSSL-based servers reject that file: OpenSSL itself reports PEM routines:get_header_and_data:bad end line, and our Python test server refused it with SSLError [SSL] PEM lib.
With two intermediates, add the second after the first: { openssl x509 -in www.crt; openssl x509 -in int1.crt; openssl x509 -in int2.crt; } > fullchain.pem.
4. Verify the file offline
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
openssl verify -show_chain -CAfile root.crt -untrusted fullchain.pem fullchain.pem
subject=C=BE, O=Example Shop, CN=www.example.com
issuer=C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1
subject=C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1
issuer=C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
fullchain.pem: OK
Chain:
depth=0: C=BE, O=Example Shop, CN=www.example.com (untrusted)
depth=1: C=BE, O=Keystone Demo, CN=Keystone Demo Issuing CA 1 (untrusted)
depth=2: C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
Each issuer is the next subject, and OK means the chain reaches the root. For a certificate from a public CA, leave out -CAfile: OpenSSL then uses its own trust store. Also confirm that the key matches the first certificate: How to check that a private key matches a certificate.
5. Configure the server
- nginx:
ssl_certificate /etc/nginx/tls/fullchain.pem;andssl_certificate_keyfor the key. The nginx documentation says: “The server certificate must appear before the chained certificates in the combined file” (nginx docs). See How to install a certificate in nginx. - Apache 2.4.8 and later: the same file in
SSLCertificateFile;SSLCertificateChainFileis deprecated (mod_ssl docs). See How to install a certificate in Apache httpd. - Kubernetes:
kubectl create secret tls web-tls --cert=fullchain.pem --key=www.key; the full chain goes intls.crt. - certbot already writes
fullchain.pemnext tocert.pem; point your server atfullchain.pem(certbot guide). - IIS does not use a chain file: it builds the chain from the Windows store, so import the intermediate there (IIS guide).
Reload the server after every change.
Check it worked
Ask the live server what it sends, with a strict client:
openssl s_client -connect www.example.com:443 -servername www.example.com -verify_return_error -showcerts </dev/null 2>/dev/null \
| grep -E '^ *[0-9]+ s:|^ +i:|Verify return code'
Our test server, loading fullchain.pem (with -CAfile root.crt added, because the test root is private):
0 s:C = BE, O = Example Shop, CN = www.example.com
i:C = BE, O = Keystone Demo, CN = Keystone Demo Issuing CA 1
1 s:C = BE, O = Keystone Demo, CN = Keystone Demo Issuing CA 1
i:C = BE, O = Keystone Demo, CN = Keystone Demo Root CA
Verify return code: 0 (ok)
The same server loading only www.crt sent one certificate and failed with Verify return code: 21 (unable to verify the first certificate). Paste the -showcerts output into the chain checker for a second opinion:
If something goes wrong
- Error 20 or 21 from a client: the intermediate is missing or is the wrong one. Compare the leaf’s issuer with the intermediate’s subject (step 1). Clients say
PKIX path building failed(Java),CERTIFICATE_VERIFY_FAILED(Python) orUNABLE_TO_VERIFY_LEAF_SIGNATURE(Node.js). - The server will not start, with
key values mismatch: the first certificate in the file is not the one that matches the key. Usually the order is reversed. Our test server, given the intermediate first, refused to load the file. PEM liborbad end lineerrors: a missing newline between two blocks (step 3). Rebuild the file withopenssl x509.- It works in the browser but not in curl: the browser repaired the chain. Trust the strict test above.
- The chain worked last year but fails after renewal: the CA changed intermediates, or an intermediate expired. Use the chain that came with the new certificate.
Why this works
A client links each certificate to its issuer by name and key identifier and checks each signature, until it reaches a root in its own trust store. It already has the root, so the server must supply everything in between, starting with its own certificate. Strict clients do not download missing links, so the file you configure decides whether they succeed. Certificate chains and path validation explains the linking, Two roads to the same root explains chains with cross-signed certificates, and the file formats appendix explains PEM bundles.
Go deeper
- RFC 9846, section 4.5.1: the order of certificates a TLS 1.3 server sends.
- RFC 5280, section 4.2.2.1: Authority Information Access and the “CA Issuers” address.
- nginx: SSL certificate chains: building and using a chain file.
- Apache mod_ssl:
SSLCertificateFile: one file with the leaf and its intermediates. - OpenSSL
verifymanual:-untrusted,-CAfileand-show_chain.
Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.