Creating a secure root CA using OpenSSL with latest encryption standards (2026/Q1)

This article talks about creating a root certificate authority in 2026 using the latest standards. Assumption is OpenSSL ≥ 3.0 (default on most modern Linux distros). The Root CA used for internal PKI (TLS, code signing, device certs, etc.). Let me know if you disagree with anything.

The examples have been tested on Red Hat Enterprise Linux 9.

Standards‑based (widely accepted)

Hash: SHA‑256 or stronger
Key sizes: ECDSA P‑384 (smaller keys, faster, strong security)
Private key encryption: AES‑256
X.509 extensions: basicConstraints = CA:TRUE
Root validity = 20 years (shorter is safer though)
Keep in mind that the root should be kept offline root. Only the intermediate should be online.

Lets start off with creating a secure directory layout:

mkdir -p pki/root/{certs,crl,newcerts,private}
mkdir -p pki/intermediate/{certs,crl,csr,newcerts,private}
chmod 700 pki/root/private pki/intermediate/private
touch pki/root/index.txt pki/intermediate/index.txt
echo 1000 > pki/root/serial
echo 1000 > pki/intermediate/serial
echo 1000 > pki/intermediate/crlnumber

Create the file pki/root/openssl.cnf with these contents for the root CA config:

[ ca ]
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = ./pki/root
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE = $dir/private/.rand # The root key and root certificate.
private_key       = $dir/private/root.key.pem
certificate       = $dir/certs/root.cert.pem

# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/ca.crl.pem
crl_extensions = crl_ext
default_crl_days = 30

default_md = sha256

name_opt = ca_default
cert_opt = ca_default
default_days = 7300
preserve = no
policy = policy_strict
x509_extensions   = v3_ca
unique_subject    = no

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ req ]
# Options for the `req` tool (`man req`).
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
prompt              = yes
default_md = sha256

# Extension to add when the -x509 option is used.
x509_extensions = v3_ca

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
commonName = Common Name
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
organizationName = Organization Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
emailAddress = Email Address

# Optionally, specify some defaults.
commonName_default              = Example Root CA
countryName_default             = DE
localityName_default =
0.organizationName_default = MyOrg
organizationName_default        = Example Corp
organizationalUnitName_default =
emailAddress_default =

[ v3_ca ]
basicConstraints = critical, CA:true, pathlen:1
keyUsage = critical, keyCertSign, cRLSign, digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer [ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

Here is to note that pathlen:1 only allows one intermediate level. The critical flags are important for strict validation.

Next create the file pki/intermediate/openssl.cnf for the intermediate CA:

[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = ./pki/intermediate
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
private_key       = $dir/private/intermediate.key.pem
certificate       = $dir/certs/intermediate.cert.pem
default_md        = sha256
default_days      = 1825            # 5 years for an issuing CA (recommendation)

# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/ca.crl.pem
crl_extensions = crl_ext
default_crl_days = 30

unique_subject    = no
policy            = policy_loose
x509_extensions   = v3_intermediate_ca

[ policy_loose ]
countryName             = supplied
organizationName        = supplied
commonName              = supplied

[ v3_intermediate_ca ]
basicConstraints        = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer

# AIA / CDP for chain building (set to your URLs)
authorityInfoAccess     = caIssuers;URI:http://pki.example.corp/ca/intermediate.cer
crlDistributionPoints   = URI:http://pki.example.corp/crl/intermediate.crl

# ---- Leaf Profiles (TLS, mTLS, Code Signing, Device PKI) ----
[ usr_cert_base ]
basicConstraints        = critical, CA:false
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
keyUsage                = critical, nonRepudiation, digitalSignature, keyEncipherment
# Add CRL/OCSP locations for relying parties
authorityInfoAccess     = OCSP;URI:http://ocsp.example.corp, caIssuers;URI:http://pki.example.corp/ca/intermediate.cer
crlDistributionPoints   = URI:http://pki.example.corp/crl/intermediate.crl

# Extensions for client certificates (`man x509v3_config`).
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
extendedKeyUsage = clientAuth, emailProtection

# Server TLS (per RFC 5280; TLS requirements per NIST SP 800-52r2)
[ server_tls ]
subjectAltName          = @alt_names

# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

# Client TLS / mTLS
[ client_tls ]
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth
subjectAltName          = @alt_names

# 3) Code Signing (per RFC 5280 EKUs)
[ code_signing ]
keyUsage                = critical, digitalSignature, contentCommitment
extendedKeyUsage        = codeSigning

# 4) Device PKI (IoT / mutual auth). Use both serverAuth and clientAuth if devices host services.
[ device_leaf ]
keyUsage                = critical, digitalSignature, keyAgreement
extendedKeyUsage        = serverAuth, clientAuth
subjectAltName          = @alt_names

# Used by -reqexts when creating CSRs that need SANs
[ req ]
distinguished_name      = req_distinguished_name
req_extensions          = req_ext
default_md              = sha256

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = DE
organizationName                = Organization Name
organizationName_default        = Example Corp
stateOrProvinceName             = State or Province Name
stateOrProvinceName_default     = Lower Saxony
commonName                      = Common Name

[ req_ext ]
subjectAltName                  = @alt_names

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

# Example SANs; override with -config/-section or env per host
[ alt_names ]
DNS.1 = example.corp
DNS.2 = www.example.corp
IP.1  = 10.0.0.10

Now let's generate the root and intermediate CA private keys:

# Root key (encrypted at rest)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 \
  -aes-256-cbc -out pki/root/private/root.key.pem
chmod 400 pki/root/private/root.key.pem

# Intermediate key
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 \
  -aes-256-cbc -out pki/intermediate/private/intermediate.key.pem
chmod 400 pki/intermediate/private/intermediate.key.pem

Create the self-signed root certificate and the intermediate CA

openssl req -new -x509 -config pki/root/openssl.cnf \
  -key pki/root/private/root.key.pem \
  -sha256 -days 7300 -extensions v3_ca \
  -out pki/root/certs/root.cert.pem
# Keep this in an offline storage and use it only to sign intermediates!

# Certificate Signing Request (CSR)
openssl req -new -config pki/intermediate/openssl.cnf \
  -key pki/intermediate/private/intermediate.key.pem \
  -sha256 -out pki/intermediate/csr/intermediate.csr.pem

# Sign the CSR with the root certificate
openssl ca -batch -config pki/root/openssl.cnf -extfile pki/intermediate/openssl.cnf \
  -extensions v3_intermediate_ca -days 1825 -notext -md sha256 \
  -in pki/intermediate/csr/intermediate.csr.pem \
  -out pki/intermediate/certs/intermediate.cert.pem

chmod 444 pki/intermediate/certs/intermediate.cert.pem
cat pki/intermediate/certs/intermediate.cert.pem pki/root/certs/root.cert.pem > pki/intermediate/certs/ca-chain.cert.pem

You can verify the certificate using this command:

openssl x509 -noout -text -in pki/root/certs/root.cert.pem

You should look out for CA:TRUE, Key Usage: Certificate Sign, CRL Sign and correct validity period.

 

CRL generation (intermediate):

openssl ca -config pki/intermediate/openssl.cnf -gencrl -out pki/intermediate/crl/intermediate.crl

Recommendation: Stand up OCSP for real‑time status, keep CRLs published for resilience.

 

You can test the chain validity with:

# Verify a leaf against the intermediate+root

openssl verify -CAfile pki/intermediate/certs/ca-chain.cert.pem server.cert.pem

I recommend to keep the root CA offline and store the private key in a safe, encrypted place. The root should only be used to sign intermediate CAs. Use the intermediate CAs for daily operations. Plan key rollover years before expiry.

Issue Leaf Certificates (Profiles)

## Server TLS (SAN‑based). Keep the lifetime ≤ 397 days for TLS (mirrors public CA baseline for operational hygiene).

# Create CSR with SANs (override alt_names via a temp section file or inline)
openssl req -new -nodes \
  -newkey ec:<(openssl ecparam -name P-384) \
  -keyout server.key.pem \
  -out server.csr.pem \
  -config pki/intermediate/openssl.cnf \
  -reqexts req_ext

# Sign as server TLS
openssl ca -batch -config pki/intermediate/openssl.cnf \
  -extensions server_tls -days 397 -notext -md sha256 \
  -in server.csr.pem -out server.cert.pem

## Client TLS (mTLS)

openssl req -new -nodes \
  -newkey ec:<(openssl ecparam -name P-384) \
  -keyout client.key.pem \
  -out client.csr.pem \
  -config pki/intermediate/openssl.cnf \
  -reqexts req_ext

openssl ca -batch -config pki/intermediate/openssl.cnf \
  -extensions client_tls -days 730 -notext -md sha256 \
  -in client.csr.pem -out client.cert.pem

## Code Signing (For Windows Authenticode/EV ecosystems you may need additional policies or dedicated OIDs. For SBOM/signing pipelines, consider timeStamping EKU on a separate TSA.)

openssl req -new -nodes \
  -newkey rsa:3072 \
  -keyout codesign.key.pem \
  -out codesign.csr.pem

openssl ca -batch -config pki/intermediate/openssl.cnf \
  -extensions code_signing -days 730 -notext -md sha256 \
  -in codesign.csr.pem -out codesign.cert.pem

## Device PKI (IoT devices, mutual auth)

openssl req -new -nodes \
  -newkey ec:<(openssl ecparam -name P-256) \
  -keyout device.key.pem \
  -out device.csr.pem \
  -config pki/intermediate/openssl.cnf \
  -reqexts req_ext

openssl ca -batch -config pki/intermediate/openssl.cnf \
  -extensions device_leaf -days 825 -notext -md sha256 \
  -in device.csr.pem -out device.cert.pem