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.