A private Certificate Authority (CA) lets you issue and manage TLS certificates for internal services without paying a public CA for every certificate. On RHEL 9, OpenSSL provides all the tools needed to build a two-tier CA hierarchy: a root CA (kept offline and highly protected) and an intermediate CA (used for day-to-day signing). This tutorial covers creating the CA directory structure, generating root and intermediate CA keys and certificates, signing server certificates, distributing the CA trust bundle to RHEL’s system trust store, and revoking certificates with a Certificate Revocation List (CRL). By the end you will have a fully functional private PKI.

Prerequisites

  • RHEL 9 with openssl installed (dnf install -y openssl)
  • Root or sudo access to write to /etc/ssl/
  • Basic understanding of public key cryptography and X.509 certificates

Step 1 — Create the CA Directory Structure

A well-organized directory tree keeps root CA and intermediate CA files separate. Create the structure for both:

# Root CA directories
mkdir -p /etc/ssl/myca/root/{certs,crl,newcerts,private}
chmod 700 /etc/ssl/myca/root/private
touch /etc/ssl/myca/root/index.txt
echo 1000 > /etc/ssl/myca/root/serial
echo 1000 > /etc/ssl/myca/root/crlnumber

# Intermediate CA directories
mkdir -p /etc/ssl/myca/intermediate/{certs,crl,csr,newcerts,private}
chmod 700 /etc/ssl/myca/intermediate/private
touch /etc/ssl/myca/intermediate/index.txt
echo 1000 > /etc/ssl/myca/intermediate/serial
echo 1000 > /etc/ssl/myca/intermediate/crlnumber

Step 2 — Create the Root CA Key and Self-Signed Certificate

The root CA key should be protected with a strong passphrase. The root certificate is valid for 10 years and signs only the intermediate CA.

cd /etc/ssl/myca/root

# Create the OpenSSL config for the Root CA
cat > /etc/ssl/myca/root/root-ca.cnf << 'EOF'
[ca]
default_ca = CA_default

[CA_default]
dir               = /etc/ssl/myca/root
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
crlnumber         = $dir/crlnumber
private_key       = $dir/private/ca.key
certificate       = $dir/certs/ca.crt
default_md        = sha256
name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_strict

[policy_strict]
countryName            = match
stateOrProvinceName    = match
organizationName       = match
organizationalUnitName = optional
commonName             = supplied

[req]
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
x509_extensions     = v3_ca

[req_distinguished_name]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
organizationName                = Organization Name
commonName                      = Common Name

[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
EOF

# Generate the root CA private key (4096-bit RSA, passphrase protected)
openssl genrsa -aes256 -out private/ca.key 4096
chmod 400 private/ca.key

# Generate the self-signed root certificate (valid 10 years)
openssl req -config root-ca.cnf 
    -key private/ca.key 
    -new -x509 -days 3650 
    -sha256 -extensions v3_ca 
    -out certs/ca.crt

# Verify the root certificate
openssl x509 -noout -text -in certs/ca.crt | head -30

Step 3 — Create the Intermediate CA

The intermediate CA handles day-to-day certificate signing, keeping the root CA offline. Generate its key, create a CSR, and sign it with the root CA:

cd /etc/ssl/myca/intermediate

# Generate the intermediate CA private key
openssl genrsa -aes256 -out private/intermediate.key 4096
chmod 400 private/intermediate.key

# Create a CSR for the intermediate CA
openssl req -config /etc/ssl/myca/root/root-ca.cnf 
    -new -sha256 
    -key private/intermediate.key 
    -out csr/intermediate.csr

# Sign the intermediate CSR with the root CA
cd /etc/ssl/myca/root
openssl ca -config root-ca.cnf 
    -extensions v3_intermediate_ca 
    -days 1825 -notext -md sha256 
    -in /etc/ssl/myca/intermediate/csr/intermediate.csr 
    -out /etc/ssl/myca/intermediate/certs/intermediate.crt

# Create the certificate chain file (intermediate + root)
cat /etc/ssl/myca/intermediate/certs/intermediate.crt 
    /etc/ssl/myca/root/certs/ca.crt > 
    /etc/ssl/myca/intermediate/certs/ca-chain.crt

Step 4 — Sign a Server Certificate

Generate a key and CSR for a server, then sign it with the intermediate CA:

# On the server (or locally for internal services):
# Generate the server key
openssl genrsa -out /etc/ssl/myca/intermediate/private/server.key 2048

# Create a CSR for the server
openssl req -config /etc/ssl/myca/root/root-ca.cnf 
    -key /etc/ssl/myca/intermediate/private/server.key 
    -new -sha256 
    -out /etc/ssl/myca/intermediate/csr/server.csr

# Sign the server CSR with the intermediate CA
openssl ca -config /etc/ssl/myca/root/root-ca.cnf 
    -extensions server_cert 
    -days 375 -notext -md sha256 
    -in /etc/ssl/myca/intermediate/csr/server.csr 
    -out /etc/ssl/myca/intermediate/certs/server.crt

# Verify the server certificate
openssl x509 -noout -text -in /etc/ssl/myca/intermediate/certs/server.crt

# Verify the full chain
openssl verify -CAfile /etc/ssl/myca/intermediate/certs/ca-chain.crt 
    /etc/ssl/myca/intermediate/certs/server.crt

Step 5 — Distribute the CA Certificate to the System Trust Store

Add the root CA certificate to RHEL 9’s system-wide trust store so all tools that rely on the system CA bundle (curl, wget, dnf, etc.) will trust certificates signed by your CA:

# Copy the root CA cert to the trust anchors directory
cp /etc/ssl/myca/root/certs/ca.crt 
    /etc/pki/ca-trust/source/anchors/myca-root.crt

# Update the system trust bundle
update-ca-trust

# Verify it was added
trust list | grep -A3 "MyCA"

# Test with curl against a server using your CA-signed certificate
curl https://internal.example.com

Step 6 — Revoke a Certificate and Generate a CRL

When a certificate is compromised or no longer needed, revoke it with the intermediate CA and publish a Certificate Revocation List:

# Revoke the server certificate
openssl ca -config /etc/ssl/myca/root/root-ca.cnf 
    -revoke /etc/ssl/myca/intermediate/certs/server.crt

# Generate the CRL from the intermediate CA
openssl ca -config /etc/ssl/myca/root/root-ca.cnf 
    -gencrl 
    -out /etc/ssl/myca/intermediate/crl/intermediate.crl

# Inspect the CRL
openssl crl -in /etc/ssl/myca/intermediate/crl/intermediate.crl 
    -noout -text

# Verify a certificate against the chain + CRL
openssl verify 
    -CAfile /etc/ssl/myca/intermediate/certs/ca-chain.crt 
    -crl_check 
    -CRLfile /etc/ssl/myca/intermediate/crl/intermediate.crl 
    /etc/ssl/myca/intermediate/certs/server.crt

Conclusion

You have built a two-tier private Certificate Authority on RHEL 9 using OpenSSL — a root CA for ultimate trust and an intermediate CA for operational certificate signing. The CA certificate is trusted system-wide via RHEL’s trust store, server certificates are verified against the chain, and you have the tools to revoke certificates and publish CRLs. Keep the root CA key offline in encrypted storage and rotate the intermediate CA periodically to maintain a strong PKI posture.

Next steps: How to Configure Mandatory Access Control with SELinux Policies on RHEL 9, How to Deploy an OCSP Responder with OpenSSL on RHEL 9, and How to Automate Certificate Issuance with HashiCorp Vault PKI on RHEL 9.