← Back to Blog

Creating a Local Certificate Authority with OpenSSL 3: A Walkthrough

Published: May 31, 2025

In this post, I'll walk you through setting up a local Certificate Authority (CA) using OpenSSL 3. This is a demo example and intentionally overlooks best practices for setting up a CA, such as procedural rigor, ensuring the right people are present during the ceremony, and avoiding leaving any individual alone during critical steps. For comprehensive guidance, refer to Microsoft's documentation on Planning and Implementing a Certification Authority.

This guide was created using an Intel Mac (remember those?), but the steps can be easily adapted for Windows or Linux environments.

Choosing the Right Algorithm

Start by selecting your preferred algorithm that aligns with your trust requirements and regulatory standards. Depending on your industry and company policies, you might opt for RSA with 3k or 4k key sizes to address post-quantum concerns, or a larger ECC algorithm if permitted. For this example, we'll use RSA 2048, a reasonably fast and industry-standard choice for demonstration purposes. However, CAs created from scratch today should ideally use larger key sizes.

Creating the Root CA Configuration

Create a file named root_ca.cnf with the necessary parameters. Ensure that any information entered in the fields clearly indicates that these are demo names. The file should look something like this:


        [ ca ]
        default_ca = CA_default

        [ CA_default ]
        dir             = ./rootCA              # Base directory for CA stuff (adjust as needed)
        certs           = $dir/certs            # Certificates from this CA
        crl_dir         = $dir/crl              # CRL directory
        new_certs_dir   = $dir/newcerts         # New certs
        database        = $dir/index.txt        # Certificate index (tracking issued certs)
        serial          = $dir/serial          # Serial number for new certs
        RANDFILE        = $dir/private/.rand    # Random seed
        private_key     = $dir/private/ca.key   # Our root key file
        certificate     = $dir/certs/ca.crt     # Our root certificate (self-signed)
        default_days    = 3650                 # Default validity (e.g. 10 years)
        default_md      = sha256               # Use SHA-2
        policy          = policy_strict        # Policy for issuing certificates

        [ policy_strict ]
        # Root CA should only issue to matching DN (as per 'man ca')
        countryName        = match
        stateOrProvinceName= match
        organizationName   = match
        organizationalUnitName = optional
        commonName         = supplied
        emailAddress       = optional

        [ req ]
        distinguished_name = req_distinguished_name
        string_mask        = utf8only
        default_md         = sha256

        [ req_distinguished_name ]
        countryName        = Country Name (2 letter code)
        stateOrProvinceName= State or Province Name
        localityName       = Locality Name
        0.organizationName = Organization Name
        organizationalUnitName = Organizational Unit Name
        commonName         = Common Name
        emailAddress       = Email Address

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

Generating the Root Key

Generate the root key using OpenSSL. A commonly used command is:


        openssl genrsa -out rootCA.key 2048
    

For more detailed instructions and options, refer to the OpenSSL documentation.

Finalizing the Root CA

After generating the root key, proceed with creating a self-signed root certificate and setting up the necessary directory structure and files (e.g., index.txt, serial) to complete the root CA setup. We do this with the commands


        mkdir -p rootCA/{certs,crl,newcerts,private}
        touch rootCA/index.txt
        echo 1000 > rootCA/serial
        echo 1000 > rootCA/crlnumber
    

Creating the Self-Signed Root Certificate


        openssl req -new -x509 -days 3650 \
        -key root_ca.key \
        -config root_ca.cnf \
        -subj "/C=US/ST=California/L=San Francisco/O=My Org/CN=Demo Root CA" \
        -extensions v3_ca \
        -out root_ca.crt
    

You can validate that it is a valid root CA using the command


        openssl x509 -noout -text -in root_ca.crt | grep -E "(CA:TRUE|Subject:|Issuer:)"
    

that shoudl output CA:TRUE.

We can also view all the details of the certificate using openssl x509 -in root_ca.crt -noout -text which should output:


    Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            34:f4:87:28:43:25:42:ea:b6:07:b0:70:90:bc:80:46:b5:b4:ee:b2
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, ST=California, L=San Francisco, O=My Org, CN=Demo Root CA
        Validity
            Not Before: May 31 18:36:36 2025 GMT
            Not After : May 29 18:36:36 2035 GMT
        Subject: C=US, ST=California, L=San Francisco, O=My Org, CN=Demo Root CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:ce:56:4e:72:36:3a:ad:d8:45:78:dd:23:d9:cb:
                    f0:80:b3:de:73:61:78:84:22:5e:e8:59:2e:1c:45:
                    43:74:99:82:3d:67:2d:0a:ca:7c:54:64:54:5a:19:
                    0d:46:70:3b:9b:74:14:2d:70:f5:43:3b:96:10:38:
                    3f:a4:11:f4:76:e8:fe:f3:f8:5b:c0:6e:cb:7d:45:
                    a0:8d:09:2d:2d:38:88:00:8c:f7:aa:81:ae:2c:3c:
                    26:66:c0:47:b2:ee:a1:9d:76:44:c1:07:41:de:84:
                    06:c2:2b:28:c9:50:15:f9:fb:0a:e8:50:ef:88:aa:
                    0b:38:6b:4d:9e:eb:f3:d7:ae:12:ec:f4:a1:67:3f:
                    0d:09:f9:8d:59:cd:b5:e6:0d:91:c5:80:cb:62:23:
                    24:01:b5:10:4b:ea:be:e1:1c:d0:f0:5b:51:09:00:
                    87:db:0c:4f:0e:f7:c5:54:3e:46:f7:cb:89:62:2c:
                    20:c2:4f:4d:49:1e:e2:d3:29:3e:ab:ad:34:62:7d:
                    de:b4:91:cb:71:18:6a:ee:f8:a2:85:6b:bf:65:86:
                    e7:3a:1e:88:89:8b:cf:59:07:f5:e8:81:5b:79:78:
                    87:12:ce:55:f7:80:a9:ba:21:b1:72:cd:43:d1:da:
                    2b:20:2c:25:2a:74:54:03:b3:6c:c1:9e:7d:32:5f:
                    e0:b7
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                9A:59:A6:CA:0E:17:71:7A:95:C9:98:79:F8:10:BE:56:D2:B5:87:E2
            X509v3 Authority Key Identifier: 
                9A:59:A6:CA:0E:17:71:7A:95:C9:98:79:F8:10:BE:56:D2:B5:87:E2
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        7f:8c:67:e2:7f:0d:d3:e8:75:bc:f1:55:b5:b6:e2:85:76:63:
        d5:45:55:a7:af:ea:e1:a0:f9:13:82:95:99:84:f2:b7:d5:57:
        50:98:f7:a5:65:36:3f:8e:8d:ca:77:79:91:d5:ad:90:0e:68:
        a7:17:e2:41:47:f9:ff:24:02:a1:ba:96:d6:a7:db:62:2a:c2:
        d9:8f:dc:3d:56:e3:df:6b:1b:b7:f8:e8:3d:1f:3c:16:22:17:
        81:b9:53:9b:7f:ec:da:e4:80:2d:7e:e5:37:36:88:78:66:37:
        15:03:a7:b2:07:0e:e1:a0:6c:1c:f9:34:6f:03:51:db:ff:eb:
        05:27:a3:b7:7d:b0:e9:c3:da:f5:b4:06:c1:16:c5:11:fc:fa:
        e0:7d:d8:02:2a:7e:7a:eb:83:49:87:31:5f:41:10:ce:67:54:
        3b:e8:83:3f:dd:08:e9:67:5e:0c:3d:d5:6c:f6:1f:fd:0d:8a:
        56:27:10:47:d9:67:7d:7a:be:67:c3:c3:30:62:f0:13:eb:af:
        02:46:c9:e3:44:b5:c8:41:6b:71:c1:c2:44:37:f0:c7:53:25:
        88:59:7d:a2:32:7a:07:bd:c8:28:c9:b4:09:a6:af:7a:94:c7:
        f3:61:9a:c9:ff:52:38:f0:31:9f:fd:5f:75:f9:0d:e1:55:5d:
        8a:56:2a:09
    

Creating Intermediate Certificates

A good practice is to never use the root key directly for signing end-entity certs, instead create intermediate certificates that will be used for signing additional intermediate certificates or leaf certificates. One should always use at least a two tier hierarchy. This can be done similarly to the root CA creation but with different parameter values. For scalability and best practices, consider using services from cloud providers or PKI service providers.

Securing the Root CA

The root CA is critical and should be kept offline and secure. Options include storing it on an offline laptop (less recommended) or using dedicated hardware devices like a YubiKey or YubiHSM. Below, we'll explore both options, including import steps, and discuss their pros and cons.

Using a YubiKey

A YubiKey is a cost-effective solution (~$50) that allows you to use the PIV module to sign artifacts, which is relatively easy to set up but has limitations. Its PIV (smartcard) app can hold an RSA/ECC key and perform signatures. Pros: very affordable, easy to use, available on all platforms. Cons: Limited to RSA/ECC (no custom algos), limited slots, and no native key-wrapping backup. It’s a smartcard, not a full HSM. For many small labs this is fine as a root-of-trust device.

To import the key and certificate use the following commands:


        # Import the private key into slot 9a (PIV auth) (needs management key, PIN):
        yubico-piv-tool -a import-key -s 9a -k 010203040506070801020304050607080102030405060708 \
            -i root_ca.key -P 123456 -K PEM

        # Import the certificate for that key (so PIV knows the public part):
        yubico-piv-tool -a import-certificate -s 9a \
            -i root_ca.crt -K PEM
    

This uses the default management key (-k) and PIN (-P) to authenticate. We tell it our key is PEM format. The YubiKey now holds the root CA’s private key and cert in its PIV slot. Once imported, any signing operations (like issuing intermediate or leaf certs) could be done by having OpenSSL use the YubiKey via PKCS#11 or via yubico-piv-tool. For example, yubico-piv-tool -a sign-data -s 9a -S data can sign data with the key.

Using a YubiHSM

A YubiHSM is more expensive but offers greater capabilities, including exportability under wrap, allowing for backups and geographical distribution. For guidance on deploying YubiHSM 2 with Active Directory Certificate Services, see: Deploying YubiHSM 2 with ADCS.

To import the key to the YubiHSM, start by connecting to the device. A good guide on that can be found on Yubico's documentation site: Quick Start Tutorial - YubiHSM 2. Then in the yubihsm-shell use the following commands:


        put asymmetric 0 0 rootCA 1 sign-pkcs root_ca.key.pem
    

The shell stores that key in the HSM (you can see an “Object ID” returned). Optionally, you’d also import the certificate via a similar command or store it externally. Once in the HSM, operations like signing a CSR can be done using PKCS#11 engine or yubihsm-shell commands. The advantage of YubiHSM2 is key management: you can wrap/backup keys using Shamir’s secret sharing (M-of-N custodians), see Key Splitting and Key Custodians - YubiHSM 2, replicate keys across devices, and enforce role-based usage. It also supports more algorithms and more operations. The downside is cost (~$600+) and extra setup. But for a production root CA, it’s ideal.

Conclusion

Setting up a local Certificate Authority using OpenSSL 3 is a valuable exercise for understanding the intricacies of PKI. While this guide simplifies many aspects for demonstration purposes, it's crucial to follow best practices and consult comprehensive resources when implementing a CA in a production environment.

This guide has some important key take aways though, for example the use of a config file means that the issuance can be automated, inspected before the procedure and replicated when a new CA needs to be issued for various reasons. It also shows that the storage of sensitive key material is possible at reasonable costs and reasoanble efforts.

Now that you know the basics of root CA creation I hope you have a better understanding of the underlying processes and can appreciate the need for secure offline storage of the keys!