A simple CA using CFSSL

What's in This Post

You can use CFSSL to create a very simple Certificate Authority for a home or test lab.

Certificate Authorities

Even for a simple use case, I always recommend creating a root Certificate Authority and an issuer or intermediate Certificate Authority. This will give you flexibility, allowing you to sign different clients with intermediate CAs that have different policies. It also allows you to follow best practice to keep your root CA offline, avoiding unnecessary risks of being compromised.

In my case, I decided to create a new internal Certificate Authority for my test lab. I'll be using the first intermediate CA to issue client certificates for the purposes of authenticating with AWS IAM Roles Anywhere.

Deploying CFSSL using Ansible

Installing CFSSL on Ubuntu is quite straight forward with Ansible

 1    vars:
 2      ubuntu_pkgs:
 3        - build-essential
 4        - golang
 5        - golang-cfssl
 6    tasks:
 7      - name: Install packages on Ubuntu 
 8        apt:
 9          name: "{{ item }}"
10          state: present
11        when: ansible_facts['distribution'] == "Ubuntu"
12        loop: "{{ ubuntu_pkgs }}"

Next we will create the relevant directories that will be used to house our certificates and other relevant files. I'm putting everything underneath the /etc/certificates folder.

 1    vars:
 2      ...
 3      pki_dir: "/etc/certificates"
 4      sub_pki_dirs:
 5        - "root"
 6        - "issuer"
 7        - "certificates"
 8        - "conf"
 9        - "bin"
10        - "log"
11      user: "signer"
12      group: "signers"
13      ...
14      - name: Add PKI directory
15        file:
16          path: "{{pki_dir}}"
17          state: directory
18          mode: '0750'
19          owner: "{{signer}}"
20          group: "{{signers}}"
21      - name: Add sub dirs to PKI
22        file:
23          path: "{{pki_dir}}/{{item}}"
24          state: directory
25          mode: '0750'
26          owner: "{{signer}}"
27          group: "{{signers}}"
28        loop: "{{ sub_pki_dirs }}"    

Now we will copy over some files that will assist with creating the CAs and signing CSRs. Since some of the files are Jinja2 templates, we also need to set some vars:

 1    vars:
 2      ...
 3      CA_LABELS:
 4        - "root"
 5        - "issuer"
 6      ca_algo:
 7        algo: "ecdsa"
 8        size: "256"
 9      ...
10      - name: Copy root-csr
11        ansible.builtin.template:
12          src: "../group_files/{{ target| default('cfssl')}}/csr.json.j2"
13          dest: "{{pki_dir}}/{{CA_LABEL}}/{{CA_LABEL}}-csr.json"
14          owner: "{{signers}}"
15          mode: '0640'
16          group: "{{signers}}"
17        loop: "{{ CA_LABELS }}"
18        loop_control:
19          loop_var: CA_LABEL
20      - name: Copy Readme.md
21        ansible.builtin.copy:
22          src: "../group_files/{{ target| default('cfssl')}}/Readme.md"
23          dest: "{{pki_dir}}/Readme.md"
24          owner: "{{signer}}"
25          mode: '0640'
26          group: "{{signers}}"
27      - name: Copy config file
28        ansible.builtin.copy:
29          src: "../group_files/{{ target| default('cfssl')}}/config.json"
30          dest: "{{pki_dir}}/conf/config.json"
31          owner: "{{signer}}"
32          mode: '0640'
33          group: "{{signers}}"
34      - name: Copy a sample host CSR json file
35        ansible.builtin.copy:
36          src: "../group_files/{{ target| default('cfssl')}}/sample-host.json"
37          dest: "{{pki_dir}}/certificates/sample-host.json"
38          owner: "{{signer}}"
39          mode: '0640'
40          group: "{{signers}}"
41      - name: Copy sign-host.sh script
42        ansible.builtin.copy:
43          src: "../group_files/{{ target| default('cfssl')}}/sign-host.sh"
44          dest: "{{pki_dir}}/bin/sign-host.sh"
45          owner: "{{signer}}"
46          group: "{{signers}}"
47          mode: '0770'      

After running our playbook we have the following directory structure:

 1signer@ca-server:/etc/certificates# tree
 2.
 3├── Readme.md
 4├── bin
 5│   └── sign-host.sh
 6├── certificates
 7│   └── sample-host.json
 8├── conf
 9│   └── config.json
10├── issuer
11│   └── issuer-csr.json
12├── log
13├── root
14    └── root-csr.json

The Readme.md file includes example commands to create and sign both the root and issuer CAs. One file you should look at and alter to suite your needs is config.json.

 1{
 2  "signing": {
 3    "default": {
 4      "expiry": "8760h"
 5    },
 6    "profiles": {
 7      "aws_issuer": {
 8        "usages": ["cert sign", "crl sign"],
 9        "expiry": "70080h",
10        "ca_constraint": {
11          "is_ca": true,
12          "max_path_len": 0
13        }
14      },
15      "host": {
16        "usages": [
17          "signing",
18          "digital signing",
19          "key encipherment",
20          "server auth"
21        ],
22        "expiry":"8760h"
23      }
24    }
25  }
26}

Above, you can see I have a profile for my AWS issuer and another for the clients whose CSRs I'll be signing. You should name these in a way that makes sense for you. Note also that the aws_issuer profile will not allow any additional CAs below that of the CA that uses the profile. That is, any CA with this profile will be able to sign CSRs for leaf or end entity certificates only (this is your typical host certificate).

The root CA

With the intermediate signed, you should copy the file root-ca-key.pem somewhere secure and offline (a usb stick that you put in a safe, not leave on your desk!) and then delete it from your host. You'll only need that file when you next need to create or renew an intermediate CA.

Get the code

You can find an up to date copy of the code here.