Setting up a PKI using CFSSL

cfssl is underdocumented and over-blogged. I’m not very keen on contributing to that, but what I read left me with some open questions. I’ll try to fill these gaps here.

Goals & Non-Goals

We’re trying to set up a Root certificate, an intermediate one and some client certificates. The directory layout will look like this:

pki
- ca
- intermediate
- certs

Preparation

This assumes that you have the cfssl installed.

First, we need to generate the configuration. As far as I know, there only needs to be one of these files for the whole pki. Assuming we’re in the pki directory:

$ cfssl print-defaults config > config.json
$ cat config.json
{
    "signing": {
        "default": {
            "expiry": "168h"
        },
        "profiles": {
            "www": {
                "expiry": "8760h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth"
                ]
            },
            "client": {
                "expiry": "8760h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "client auth"
                ]
            }
        }
    }
}

That gives us the default config, which we then need to adjust. Specifically, we need to add an intermediate-profile. Here’s what mine looks like:

$ cat config.json
{
    "signing": {
        "default": {
            "expiry": "168h"
        },
        "profiles": {
            "intermediate": {
                "expiry": "43800h",
                "usages": [
                    "cert sign",
                    "crl sign"
                ],
                "ca_constraint": {
                    "is_ca": true,
                    "max_path_len": 0,
                    "max_path_len_zero": true
                }
            },
            "client": {
                "expiry": "8760h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "client auth"
                ]
            },
            "server": {
                "expiry": "8760h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth"
                ]
            }
        }
    }
}

Besides restricting the usage of the intermediates, we also restrict their path length so they can only sign leaf certificates. I left the server in there for testing.

Generating the root certificate

$ mkdir -p ca && cfssl print-defaults csr > ca/ca.json
$ cat ca/ca.json
{
    "CN": "example.net",
    "hosts": [
        "example.net",
        "www.example.net"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "US",
            "ST": "CA",
            "L": "San Francisco"
        }
    ]
}

Again, this file needs to be updated. The hosts can be removed and the CN should be something that identifies the certificate. I use ca.pki.your.domain.tld, but as far as I can tell, it is common to use non-fqdn names, too. The location part under name doesn’t seem to matter. We’re ready to generate the certificate now.

$ cfssl gencert -initca ca/ca.json | cfssljson -bar ca/ca
$ cfssl certinfo -cert ca/ca.pem
< some info about the new root certificate >

Generating the intermediate certificates

Note: From what I’m reading, it’s good practice to use intermediate certificates because that allows you to keep the root keys airgapped. Not sure if this is actually a big benefit here. If an intermediate is compromised, we have to rotate CA anyways, since we don’t have revocation infrastructure.

Generate intermediate-01.json similar to how we did it with the ca. I personally copied it.

$ mkdir -p intermediate/
$ cp ca/ca.json intermediate/intermediate-01.json

It does need a different CN though. Again, I use intermediate-01.intermediate.pki.your.domain.tld. After that we’re ready to sign it:

$ cfssl gencert \
    -ca ca/ca.pem -ca-key ca/ca-key.pem \
    -config config.json -profile intermediate \
    intermediate/intermediate-01.json \
  | cfssljson -bare intermediate/intermediate-01
# cfssl certinfo intermediate/intermediate-01.pem
< info about your new certificate >

Generating client certificates

Exactly the same as the intermediate, except:

  • use a different CN (maybe something like foo.clients.pki.your.domain.tld)
  • sign it with the intermediate instead of the CA
  • select the client profile

What’s next?

You can verify that it worked or export the certificate for browsers etc