PKI with CFSSL
In this post we will learn how to deploy our own Public Key Infrastructure (PKI) by using the CFSSL tooling. This may be useful if you want to run your own Certificate Authority (CA) in order to issue certificates for your systems and/or users.
Introduction to CFSSL
CFSSL is a tool set created by Cloudflare and released as Open Source software. Before you continue reading this post I’d suggest reading this introductory post to PKI and CFSSL by Cloudflare.
This post assumes you already have basic knowledge on PKI and in how the CFSSL tooling works, if you don’t have it, go read the post linked above.
Installing the CFSSL tooling
In order to install the CFSSL tooling you can go to the GitHub Releases and download the binaries from there.
Warning
Below commands will only work for Linux x86_64 machines.
sudo curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssl_1.6.4_linux_amd64 -o /usr/local/bin/cfssl
sudo curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.4/cfssljson_1.6.4_linux_amd64 -o /usr/local/bin/cfssljson
sudo curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.4/multirootca_1.6.4_linux_amd64 -o /usr/local/bin/multirootca
sudo chmod +x /usr/local/bin/{cfssl,cfssljson,multirootca}
PKI Organization
For this example, the following organization will be used.

Creating the Root CA
Let’s create a folder to store the PKI files:
mkdir -p ~/cafiles/{root,intermediate,config,certificates}Before issuing the Root CA, we need to define its config:
Note
The expiration is 10 years. You want to have a long expiration time for your Root CA to avoid having to re-roll the PKI too often.
cat << "EOF" > ~/cafiles/root/root-csr.json { "CN": "Linuxera Root Certificate Authority", "key": { "algo": "ecdsa", "size": 256 }, "names": [ { "C": "ES", "L": "Valencia", "O": "Linuxera Internal", "OU": "CA Services", "ST": "Valencia" } ], "ca": { "expiry": "87600h" } } EOFIssue the Root CA with
cfssl:cfssl gencert -initca ~/cafiles/root/root-csr.json | cfssljson -bare ~/cafiles/root/root-caAt this point, we have our Root CA ready.
Creating the Intermediate CA
Issuing certificates directly with the Root CA is not advised. You should be issuing intermediary CAs with the Root CA instead. This allows for better organization of your PKI, and in case of a security incident you won’t have to re-roll the whole PKI, instead you will only re-roll the affected Intermediate CA.
For this test, we will be issuing only an Intermediate CA. In real scenarios, is pretty common having multiple intermediates, and sometimes these intermediate CAs will be used to issue other intermediate CAs.
Define the Intermediate CA config:
Note
The expiration is 8 years. For Intermediate CAs you also want to have quite a long expiration time.
cat << "EOF" > ~/cafiles/intermediate/intermediate-csr.json { "CN": "Linuxera Intermediate CA", "key": { "algo": "ecdsa", "size": 256 }, "names": [ { "C": "ES", "L": "Valencia", "O": "Linuxera Internal", "OU": "Linuxera Internal Intermediate CA", "ST": "Valencia" } ] } EOFGenerate the key for the Intermediate CA:
cfssl genkey ~/cafiles/intermediate/intermediate-csr.json | cfssljson -bare ~/cafiles/intermediate/intermediate-caDefine a CFSSL
signingprofile for the Intermediate CAs. This is done via a config file.cert signandcrl sign- Expiration set to 8 years.
- CA constraints define that the certificates issued will be used by CAs
is_ca: trueandmax_path_len: 1limits this intermediate CA to only be able to issue sub-intermediate CAs that cannot issue additional CAs. (This could be allowed withmax_path_len: 0andmax_path_len_zero: true).
cat << "EOF" > ~/cafiles/config/config.json { "signing": { "default": { "expiry": "8760h" }, "profiles": { "intermediate": { "usages": ["cert sign", "crl sign"], "expiry": "70080h", "ca_constraint": { "is_ca": true, "max_path_len": 1 } } } } } EOFSign the Intermediate CA with the Root CA:
cfssl sign -ca ~/cafiles/root/root-ca.pem -ca-key ~/cafiles/root/root-ca-key.pem -config ~/cafiles/config/config.json -profile intermediate ~/cafiles/intermediate/intermediate-ca.csr | cfssljson -bare ~/cafiles/intermediate/intermediate-caAt this point, our Intermediate CA is ready to issue certificates, and we can take our Root CA offline. Usually, the private key gets stored in an HSM and after that it’s deleted from the file system.
rm -f ~/cafiles/root/root-ca-key.pem
Issuing certificates with the Intermediate CA
Before issuing the certificate, we will add a new signing profile to our config. We will be defining a
hostsigning profile that defines different usages as well as an expiration of 1 year for the certificates.cat << "EOF" > ~/cafiles/config/config.json { "signing": { "default": { "expiry": "8760h" }, "profiles": { "intermediate": { "usages": ["cert sign", "crl sign"], "expiry": "70080h", "ca_constraint": { "is_ca": true, "max_path_len": 1 } }, "host": { "usages": ["signing", "digital signing", "key encipherment", "server auth"], "expiry": "8760h" } } } } EOFWith the profile ready, let’s create the certificate config:
cat << "EOF" > ~/cafiles/certificates/my-host-csr.json { "CN": "testhost.linuxera.org", "hosts": ["testhost.linuxera.org", "192.168.122.120"], "names": [ { "C": "ES", "L": "Valencia", "O": "Linuxera Internal", "OU": "Linuxera Internal Hosts" } ] } EOFFinally, use the
cfssltooling to issue this certificate with the Intermediate CA using thehostprofile:cfssl gencert -ca ~/cafiles/intermediate/intermediate-ca.pem -ca-key ~/cafiles/intermediate/intermediate-ca-key.pem -config ~/cafiles/config/config.json -profile host ~/cafiles/certificates/my-host-csr.json | cfssljson -bare ~/cafiles/certificates/my-hostAt this point, we can verify the cert we just created:
openssl x509 -in ~/cafiles/certificates/my-host.pem -noout -subject -issuer -startdate -enddateNote
We can see the issuer is our Intermediate CA.
subject=C = ES, L = Valencia, O = Linuxera Internal, OU = Linuxera Internal Hosts, CN = testhost.linuxera.org issuer=C = ES, ST = Valencia, L = Valencia, O = Linuxera Internal, OU = Linuxera Internal Intermediate CA, CN = Linuxera Intermediate CA notBefore=Aug 9 10:09:00 2023 GMT notAfter=Aug 8 10:09:00 2024 GMTIf we check the certificate file
~/cafiles/certificates/my-host.pem, we will see that it only contains the certificate for the host and not the full bundle (Intermediate CAs + Cert). We can generate a full chain cert with the command below:Note
Bundles are useful when you intend to use the certificate for an app like a web server, that way you will be sending the certificate + all the intermediate CAs certificates up to the Root CA so the client can verify its trust. Including the Root CA cert is not required, your client should already trust the Root CA, if it doesn’t trust it that won’t change even if you send it as part of the bundle.
cfssl bundle -ca-bundle ~/cafiles/root/root-ca.pem -int-bundle ~/cafiles/intermediate/intermediate-ca.pem -cert ~/cafiles/certificates/my-host.pem | cfssljson -bare ~/cafiles/certificates/my-host-fullchainWe should have the bundled cert available:
Warning
In some Linux distributions, the previous
cfssl bundlecommand may not generate the bundled cert. If that’s the case you can get the same result by runningcat ~/cafiles/certificates/my-host.pem ~/cafiles/intermediate/intermediate-ca.pem > ~/cafiles/certificates/my-host-fullchain.pemcat ~/cafiles/certificates/my-host-fullchain.pem-----BEGIN CERTIFICATE----- MII... -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MII... -----END CERTIFICATE-----Finally, we could verify the cert:
openssl verify -CAfile <(cat ~/cafiles/root/root-ca.pem ~/cafiles/intermediate/intermediate-ca.pem) ~/cafiles/certificates/my-host.pem/home/mario/cafiles/certificates/my-host.pem: OK
Exposing our PKI to remote systems with MultiRootCA
So far, we have been using cfssl tooling to issue certificates while connected to a system where our PKI is stored. In real environments, you may need to issue certificates for different people/systems in a more convenient way.
The MultiRootCA program is an authenticated-signer-only server that is used as a remote server for cfssl instances. It is intended for:
- Running
cfsslas a service on servers to generate keys. - Act as a remote signer to manage the CA keys for issuing certificates.
Let’s start by issuing a certificate for the multirooca server:
cat << "EOF" > ~/cafiles/certificates/multirootca-server-csr.json { "CN": "multirootca-server.linuxera.org", "hosts": ["multirootca-server.linuxera.org", "192.168.122.153"], "names": [ { "C": "ES", "L": "Valencia", "O": "Linuxera Internal", "OU": "Linuxera Internal Hosts" } ] } EOFcfssl gencert -ca ~/cafiles/intermediate/intermediate-ca.pem -ca-key ~/cafiles/intermediate/intermediate-ca-key.pem -config ~/cafiles/config/config.json -profile host ~/cafiles/certificates/multirootca-server-csr.json | cfssljson -bare ~/cafiles/certificates/multirootca-serverWe will secure the signing profiles in our config. We will be defining an
auth_keythat clients requesting a signed certificate must provide in order to get it signed.Note
The Auth Key is a 16 byte hexadecimal string. You can generate one by running
openssl rand -hex 16cat << "EOF" > ~/cafiles/config/config.json { "signing": { "default": { "expiry": "8760h" }, "profiles": { "intermediate": { "usages": ["cert sign", "crl sign"], "expiry": "70080h", "ca_constraint": { "is_ca": true, "max_path_len": 1 } }, "host": { "usages": ["signing", "digital signing", "key encipherment", "server auth", "client auth"], "expiry": "8760h", "auth_key": "default" } } }, "auth_keys": { "default": { "key": "b50ed348c4643d34706470f36a646fd4", "type": "standard" } } } EOFWe need to tell multirootca where to find the different certificates for our Intermediate CA:
cat <<EOF > ~/cafiles/config/multiroot-profile.ini [linuxeraintermediate] private = file://${HOME}/cafiles/intermediate/intermediate-ca-key.pem certificate = ${HOME}/cafiles/intermediate/intermediate-ca.pem config = ${HOME}/cafiles/config/config.json EOFFinally, we can run the multirootca server:
multirootca -a 0.0.0.0:8000 -l default -roots ~/cafiles/config/multiroot-profile.ini -tls-cert ~/cafiles/certificates/multirootca-server.pem -tls-key ~/cafiles/certificates/multirootca-server-key.pemA more appropriate way of running the server would be using a systemd service:
cat <<EOF | sudo tee /etc/systemd/system/multirootca.service [Unit] Description=CFSSL PKI Certificate Authority After=network.target [Service] User=${USER} ExecStart=/usr/local/bin/multirootca -a 0.0.0.0:8000 -l linuxeraintermediate -roots ${HOME}/cafiles/config/multiroot-profile.ini -tls-cert ${HOME}/cafiles/certificates/multirootca-server.pem -tls-key ${HOME}/cafiles/certificates/multirootca-server-key.pem Restart=on-failure Type=simple [Install] WantedBy=multi-user.target EOFsudo systemctl daemon-reload sudo systemctl enable multirootca --now
Requesting certificates to the multirootca
Now that the Intermediate CA has been exposed with the multirootca program, we can go ahead and request it to sign some certificates. We can do this from a remote location, or from the same server where multirootca is running.
Generate a certificate config:
cat << "EOF" > my-cert-request-csr.json { "CN": "myserver.linuxera.org", "hosts": ["myserver.linuxera.org", "192.168.122.222"], "names": [ { "C": "ES", "L": "Valencia", "O": "Linuxera Internal", "OU": "Linuxera Internal Hosts" } ] } EOFGenerate a request profile. This is required for
cfsslto know how to request the certificate to the multirootca:Warning
We need to define the Auth key, otherwise multirootca will not sign our certificate. And the location of the multirootca server, we can use IP:Port or DNS:Port.
cat <<EOF > request-profile.json { "signing": { "default": { "auth_remote": { "remote": "ca_server", "auth_key": "default" } } }, "auth_keys": { "default": { "key": "b50ed348c4643d34706470f36a646fd4", "type": "standard" } }, "remotes": { "ca_server": "https://multirootca-server.linuxera.org:8000" } } EOFFinally, we send the request by specifying the
hostprofile, which is the one we will be using for signing host certificates:Warning
We need to specify the Intermediate CA certificate via the
-tls-remote-caflag.cfssl gencert -config ./request-profile.json -tls-remote-ca ./intermediate-ca.pem -profile host ./my-cert-request-csr.json | cfssljson -bare my-cert2023/08/09 11:43:15 [INFO] generate received request 2023/08/09 11:43:15 [INFO] received CSR 2023/08/09 11:43:15 [INFO] generating key: ecdsa-256 2023/08/09 11:43:15 [INFO] encoded CSR 2023/08/09 11:43:15 [INFO] Using trusted CA from tls-remote-ca: ./intermediate-ca.pemWe should have a valid certificate now:
openssl x509 -in ./my-cert.pem -noout -subject -issuer -startdate -enddatesubject=C = ES, L = Valencia, O = Linuxera Internal, OU = Linuxera Internal Hosts, CN = myserver.linuxera.org issuer=C = ES, ST = Valencia, L = Valencia, O = Linuxera Internal, OU = Linuxera Internal Intermediate CA, CN = Linuxera Intermediate CA notBefore=Aug 9 11:38:00 2023 GMT notAfter=Aug 8 11:38:00 2024 GMT
Closing Thoughts
We have seen how to run our own PKI with the CFSSL tooling, in the next post we will see how to leverage this PKI from Kubernetes by using cert-manager.