Skip to main content
RunBook Academy

LinuxLXXI · TLS and PKICAs and chains

Certificate authorities and chains - the trust model in practice

Intermediate⏱ ~16 minopenssl

What you'll learn

  • Distinguish root, intermediate, and leaf CAs
  • Set up a private CA hierarchy
  • Distribute trust stores
  • Manage the chain of trust

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

A certificate authority (CA) signs certificates. The trust chain goes from a trusted root CA through intermediate CAs to end-entity certificates. This lesson covers the trust model in practice.

CA hierarchy

A typical CA hierarchy:

Root CA (offline, secure storage)

├── Intermediate CA 1 (e.g. web services)
│   ├── server1.example.com
│   ├── server2.example.com
│   └── ...

├── Intermediate CA 2 (e.g. internal services)
│   ├── db.example.com
│   └── ...

└── Intermediate CA 3 (e.g. dev)
    └── ...

The root signs the intermediates. The intermediates sign end-entity certificates. The root is kept offline; the intermediates are online.

Why a hierarchy

A single root CA is a high-value target. If compromised, every certificate is compromised. A hierarchy:

  • Limits the blast radius: a compromised intermediate affects only the certificates it signed.
  • Allows different policies for different intermediates (e.g. web vs internal).
  • Allows rotation: a new intermediate can be issued without revoking the root.

The extension trap

Before the recipe, understand the one mistake that breaks most hand-built PKIs.

openssl x509 -req adds no X.509 v3 extensions. It copies the subject and the public key from the CSR, signs, and stops. Extensions requested in the CSR are discarded unless you ask for them explicitly.

That matters twice:

  • A CA certificate without basicConstraints = critical,CA:TRUE is not a CA. Every chain built through it fails validation with error 79 ... invalid CA certificate.
  • A server certificate without a subjectAltName cannot be matched to a hostname by any modern client. RFC 9525 removed the Common Name fallback; Chrome dropped it in 2017 and Go has rejected CN-only certificates since 1.15.

So every signing step below passes -extfile. That is not optional polish. It is the difference between a working PKI and one that has to be rebuilt.

Set up a private CA hierarchy

Run this on a disposable host or in a scratch directory. It writes only under ~/ca and the current directory, and it touches no system trust store.

Root CA

mkdir -p ~/ca/root/{certs,crl,csr,newcerts,private}
chmod 700 ~/ca/root/private
echo 1000 > ~/ca/root/serial
openssl genrsa -out ~/ca/root/private/root-ca.key 4096
openssl req -x509 -new -nodes -key ~/ca/root/private/root-ca.key \
    -sha256 -days 7300 -out ~/ca/root/certs/root-ca.crt \
    -subj "/C=US/ST=CA/O=MyOrg/CN=MyOrg Root CA" \
    -addext "basicConstraints=critical,CA:TRUE" \
    -addext "keyUsage=critical,keyCertSign,cRLSign" \
    -addext "subjectKeyIdentifier=hash"

openssl req -x509 does honour -addext, so the root gets its CA extensions inline. The root signs itself, so there is no CSR to lose them.

Intermediate CA

mkdir -p ~/ca/intermediate/{certs,crl,csr,newcerts,private}
chmod 700 ~/ca/intermediate/private
echo 1000 > ~/ca/intermediate/serial
openssl genrsa -out ~/ca/intermediate/private/intermediate-ca.key 4096
openssl req -new -key ~/ca/intermediate/private/intermediate-ca.key \
    -out ~/ca/intermediate/csr/intermediate-ca.csr \
    -subj "/C=US/ST=CA/O=MyOrg/CN=MyOrg Intermediate CA"

cat > intermediate-ext.cnf <<'EOF'
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
EOF

openssl x509 -req -in ~/ca/intermediate/csr/intermediate-ca.csr \
    -CA ~/ca/root/certs/root-ca.crt -CAkey ~/ca/root/private/root-ca.key \
    -CAcreateserial -out ~/ca/intermediate/certs/intermediate-ca.crt \
    -days 3650 -sha256 -extfile intermediate-ext.cnf

pathlen:0 says this intermediate may sign end-entity certificates but no further CAs. That is the constraint you want: it stops a compromised intermediate from minting its own sub-CAs.

Note the csr directory in the mkdir list. Without it, openssl req -new fails with Can't open ... for writing, No such file or directory and the rest of the recipe never runs.

End-entity (server) certificate

openssl genrsa -out server.key 2048
chmod 600 server.key
openssl req -new -key server.key -out server.csr \
    -subj "/C=US/ST=CA/O=MyOrg/CN=server.example.com" \
    -addext "subjectAltName=DNS:server.example.com,DNS:www.example.com"

cat > server-ext.cnf <<'EOF'
subjectAltName = DNS:server.example.com, DNS:www.example.com
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF

openssl x509 -req -in server.csr \
    -CA ~/ca/intermediate/certs/intermediate-ca.crt \
    -CAkey ~/ca/intermediate/private/intermediate-ca.key \
    -CAcreateserial -out server.crt -days 365 -sha256 \
    -extfile server-ext.cnf

The SAN appears in both places for a reason. In the CSR it tells the CA what you are asking for. In server-ext.cnf it is what the CA actually issues. The CA decides; the CSR only requests.

Verify before you distribute anything

# The intermediate must actually be a CA
openssl x509 -in ~/ca/intermediate/certs/intermediate-ca.crt \
    -noout -ext basicConstraints
# X509v3 Basic Constraints: critical
#     CA:TRUE, pathlen:0

# The leaf must actually carry a SAN
openssl x509 -in server.crt -noout -ext subjectAltName
# X509v3 Subject Alternative Name:
#     DNS:server.example.com, DNS:www.example.com

# The chain must build and the hostname must match
openssl verify -CAfile ~/ca/root/certs/root-ca.crt \
    -untrusted ~/ca/intermediate/certs/intermediate-ca.crt \
    -verify_hostname server.example.com -purpose sslserver \
    server.crt
# server.crt: OK

All three checks must pass. openssl verify on its own is a weak test: it accepts a Common Name as a hostname long after real clients stopped doing so. -verify_hostname together with the explicit subjectAltName check is what tells you a Go or Chrome client will accept the certificate.

The chain for server.crt is: server -> intermediate -> root.

Chain order

The chain file the server presents has a fixed order: the leaf first, then each intermediate in issuing order, and the root omitted.

cat server.crt ~/ca/intermediate/certs/intermediate-ca.crt > server-chain.crt

The root is left out on purpose. The client already has it in its trust store, and a client that does not have it will not start trusting it because the server sent a copy. Sending the root only wastes handshake bytes.

Get the order wrong and some clients still work, because they reorder the certificates themselves. Others do not. That difference is what makes chain-order bugs look intermittent.

Distribute trust stores

Client systems trust the root CA. They receive root-ca.crt and install it in their trust store:

  • Debian / Ubuntu: /usr/local/share/ca-certificates/, then update-ca-certificates.
  • RHEL family: /etc/pki/ca-trust/source/anchors/, then update-ca-trust.

The intermediate is sent along with the end-entity certificate, in the order described above: leaf first, intermediates next, root omitted.

Cross-signing

An intermediate can be cross-signed by multiple root CAs. This allows the intermediate to be trusted by clients that trust any of the root CAs.

For migration or compatibility, cross-signing is a common tool.

Cleanup

If you built the hierarchy above as an exercise, remove it:

rm -rf ~/ca
rm -f server.key server.csr server.crt server-chain.crt \
    server-ext.cnf intermediate-ext.cnf

Nothing in the recipe installs a trust anchor, so there is no system state to undo. If you did add root-ca.crt to a trust store while experimenting, remove it from /usr/local/share/ca-certificates/ or /etc/pki/ca-trust/source/anchors/, then re-run update-ca-certificates or update-ca-trust. A forgotten private root in a trust store is a standing compromise: its key is on a lab machine, and anything it signs is trusted.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the role of a root CA in a CA hierarchy?

  2. Q2. Root CAs are typically online and used for daily signing.

  3. Q3. Which of the following are valid in a CA hierarchy? Select all that apply.

  4. Q4. You sign an intermediate CSR with openssl x509 -req and no -extfile. What happens?

  5. Q5. In what order should a server present its certificate chain?

Passing score: 75%. Answers are checked in this browser.