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