How to automate step-ca with ansible

Managing certs can be a hard thing. Luckily ansible can help! I will be sharing parts of my ansible configuration for step-ca and explaining it by the way.
Note we arnt using the unoffical existing playbook for ansible.

Before we do the playbook, we need to configure ca.json!

{
  "root": "",
  "federatedRoots": null,
  "crt": "",
  "key": "",
  "address": ":9001",
  "insecureAddress": "9002",
  "dnsNames": [
    "localhost"
  ],
  "logger": {
    "format": "text",
    "level": "info",
    "output": "<dir to playbook>/step-ca/logs/ca.log"
  },
  "authority": {
    "provisioners": [
    ],
    "template": {},
    "backdate": "59m0s",
    "certificate": {
      "defaultDuration": "8760h0m0s"
    }
  },
  "tls": {
    "cipherSuites": [
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
    ],
    "minVersion": 1.2,
    "maxVersion": 1.3,
    "renegotiation": false
  },
  "password": "your_password",
  "commonName": "Step Online CA"
}

This has a bunch of default options i perfer, remember to keep provisioners empty, it would be filled automatically by the ansible config! the same goes for crt, root and key! Ideally keep it in the same directory.

---
- name: Set up step-ca and Generate Certificates
  hosts: localhost
  vars:
    ca_root_key_path: "(path)/root_ca.key"
    ca_root_crt_path: "(path)/root_ca.crt"
    ca_intermediate_key_path: "(path)/intermediate_ca.key"
    ca_intermediate_crt_path: "(path)/intermediate_ca.crt"
    ca_json_path: "(path)/ca.json"
    ca_url: "https://localhost:9001"
    root_password: "your_root_password"
    intermediate_password: "your_intermediate_password"
    jwk_password: "your_jwk_password"
    hostname: "{{ hostname }}"

This first bit establishes the variables you will need to fill in to get this working. This eludes to some other things that will be in this tutorial, like we will need to use jwk (put a password in the jwk_password section, this will help with the encryption used for your certs, you likely dont need to enter them in), specify hostname (put the website domain). Put the paths you want each of these certs to be in, (i recommend putting them in the same path)


The ca URL will be where step-ca runs.

  tasks:
    - name: Create password file for root certificate
      ansible.builtin.copy:
        dest: "<directory to step-ca>/step-ca/certs/root_password.txt"
        content: "{{ root_password }}"
        mode: '0600'

    - name: Generate Root Certificate
      ansible.builtin.command:
        cmd: "step certificate create 'Root CA' {{ ca_root_crt_path }} {{ ca_root_key_path }} --profile root-ca --password-file <directory to step-ca>/step-ca/certs/root_password.txt"

This creates the root certificate, labeled as it, it also specifies the path for the root key and makes the password file which will be used to decrypt it (by encrypting it with it), then it applies it to the root cert. It creates the root password file with Read-Only perms unless you are root. The root certificate is the only thing that is self signed and is the foundation of the PKI stack (a chain of trust across certificates)


    - name: Create password file for intermediate certificate
      ansible.builtin.copy:
        dest: "<directory to step-ca>/step-ca/certs/intermediate_password.txt"
        content: "{{ intermediate_password }}"
        mode: '0600'

    - name: Generate Intermediate Certificate
      ansible.builtin.command:
        cmd: "step certificate create 'Intermediate CA' {{ ca_intermediate_crt_path }} {{ ca_intermediate_key_path }} --profile intermediate-ca --ca {{ ca_root_crt_path }} --ca-key {{ ca_root_key_path }} --password-file <directory to step-ca>/step-ca/certs/intermediate_password.txt --ca-password-file <directory to step-ca>/step-ca/certs/root_password.txt"

This does the same thing for the intermediary certificates. It creates a password file and then generates the intermediate certificate with the settings specifying the path, the key path, the location of the root key path, and the password files to access the root files and to apply to the intermediate certificate. The intermediate cert is issued by the root certificate and will be used for any domain cert to be issued.

    - name: Update ca.json with Certificate Paths
      ansible.builtin.lineinfile:
        path: "{{ ca_json_path }}"
        regexp: '^(\s*"root": )'
        line: '  "root": "{{ ca_root_crt_path }}",'

    - name: Add intermediate certificate path to ca.json
      ansible.builtin.lineinfile:
        path: "{{ ca_json_path }}"
        regexp: '^(\s*"crt": )'
        line: '  "crt": "{{ ca_intermediate_crt_path }}",'

    - name: Add in the key certificate path to ca.json
      ansible.builtin.lineinfile:
        path: "{{ ca_json_path }}"
        regexp: '^\s*"key": '
        line: '  "key": "{{ ca_intermediate_key_path }}",'
        firstmatch: true

This adjusts the ca.json file so that it will have the certificate paths, and key paths, there is still more needing to be added. Like the keys. But we will get to that. Note that this is not idempotent and as of the moment I have not devised a way for the playbook to be ran multiple times as running it again with a existing provisioner section will yield a error. The way this part of the playbook works is by using regex and finding the line with the key section, cert section and root section, then fill in the corresponding sections.

    - name: Read public JWK file contents
      ansible.builtin.slurp:
        src: "<directory to step-ca>/step-ca/certs/public.jwk"
      register: public_jwk_contents

    - name: Read private JWK file contents
      ansible.builtin.slurp:
        src: "<directory to step-ca>/step-ca/certs/private.jwk"
      register: private_jwk_contents

    - name: Parse public JWK file contents
      ansible.builtin.set_fact:
        public_jwk_json: "{{ public_jwk_contents.content | b64decode | from_json }}"

#    - name: Parse private JWK file contents
#      ansible.builtin.set_fact:
#        private_jwk_json: "{{ private_jwk_contents.content | b64decode | from_json }}"

This reads the JWK files and parse them. Slurp being ansibles tool for reading files, it reads the public and private jwk files (following the private-public key arcitecture) and then it will parse it into json before appending it to a variable which will later be used to write it too.

 - name: Format JWK using step crypto jose format
      shell: sudo cat <directory to step-ca>/step-ca/certs/private.jwk | step crypto jose format
      register: formatted_jwk
      changed_when: false

This formats the JWK key, this took awhile to figure out with me needing to make a discussion on the github, its important that the x, y and kid varibles are present, “step crypto jose format” is the command to achieve it.

    - name: Set encryptedKey fact
      set_fact:
        encrypted_key: "{{ formatted_jwk.stdout }}"

    - name: Set fact for x value
      ansible.builtin.set_fact:
        provisioner_x_value: "{{ public_jwk_json.x }}"

    - name: Set fact for y value
      ansible.builtin.set_fact:
        provisioner_y_value: "{{ public_jwk_json.y }}"

    - name: Set fact for kid value
      ansible.builtin.set_fact:
        provisioner_kid_value: "{{ public_jwk_json.kid }}"

This gets all of the kid, x, y and encrypted key values from the JWK file and appends it to varibles, which will later be used to append it to ca.json, next step would be to create ca.json!

    - name: Add new provisioner to ca.json
      ansible.builtin.shell: |
        jq '.authority.provisioners += [{
          "type": "jwk",
          "name": "NAME HERE",
          "encryptedKey": "{{ encrypted_key  }}",
          "key": {
            "use": "sig",
            "kty": "EC",
            "crv": "P-256",
            "alg": "ES256",
            "x": "{{ provisioner_x_value }}",
            "y": "{{ provisioner_y_value }}",
            "kid": "{{ provisioner_kid_value }}"
          },
        }]' {{ ca_json_path }} > {{ ca_json_path }}.tmp && mv {{ ca_json_path }}.tmp {{ ca_json_path }}
      register: provisioner_update
      changed_when: provisioner_update.rc == 0

This fills in the provisioned spot, it uses jq, a tool for processing JSON, to append a new provisioner within the array of provisioners, this wont bode well with a already modified ca.json.

    - name: Start step-ca server
      ansible.builtin.command: "step-ca --password-file <directory to step ca>/step-ca/certs/intermediate_password.txt {{ ca_json_path }}"
      async: 60
      poll: 0
      ignore_errors: true
      register: step_ca_output

    - name: Wait for step-ca to start
      wait_for:
        port: 443
        host: localhost
        timeout: 30

This starts the step-ca server and waits for step-ca, as to generate a cert, it requires the work of a server and a client, the client will need to request the certificate. It will be on port 9001.

- name: Create Certificate
  hosts: ca
  vars:
    hostname: "{{ hostname }}"
    ca_url: "https://localhost:9001"
    ca_root_crt_path: "<directory to step-ca>/step-ca/certs/root_ca.crt"

This starts a new ansible block where it will create the certificates by making a request to the server on the port it is running on (9001).

    - name: Create directory for certs
      ansible.builtin.file:
        path: "<directory to step-ca>/step-ca/{{ hostname }}"
        state: directory
        mode: '0644'

This creates the directory for the domain certs, which the certs will be kept in, this is just so its neat.

    - name: Create certificate
      ansible.builtin.command: >
        step ca certificate {{ hostname }}
        <directory to step-ca>/step-ca/{{ hostname }}/{{ hostname }}.crt
       <directory to step-ca>/step-ca/{{ hostname }}/{{ hostname }}.key
        --provisioner-password-file=<directory to step-ca>/step-ca/certs/jwk_password.txt
        --ca-url={{ ca_url }}
        --root={{ ca_root_crt_path }}
        --not-before=10h
      args:
        creates: "<directory to step-ca>/step-ca/{{ hostname }}/{{ hostname }}.crt"

This creates the final cert! You can now use these certs for https!

How to automate step-ca with ansible

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top