Managing SSL Certificates on OctoPi with Ansible

Managing SSL Certificates on OctoPi with Ansible

June 28, 2021 ( last updated : June 29, 2021 )
octopi ansible devops


I’ve recently upgraded 3d printers to a Creality Ender 3 v2 and on the advice of a long time co-conspirator immediately departed down the path of setting up OctoPrint via OctoPi. This is great and I highly recommend it, one button printing without getting out of your chair is addictive. By default OctoPi generates self-signed certificates and allows both secure and insecure connections. We can do better.

Firstly, for any moderately sane person this process is well documented on a couple of excellent forum posts:

Having said this, handling certificates shouldn’t be a manual process - you forget how to do it and make mistakes and don’t want to do it regularly. So to the automation machine!

All the Ansible

There are stacks of guides on how to create certificates so I won’t be re hashing that. For OctoPi the above resources are a great starting point. For what it’s worth, I’m using cfssl to generate certificates from an intermediate certificate.

The great thing about working from OctoPi is that we have a relatively constant, known platform to work from. This code only needs to work on this device and we do not need to care for edge cases of trying to deploy Python 3 on Mandrake or anything else horrible.

Release the Certs!

Firstly, we need to get the certificates onto the OctoPi host. At present, my certificates are stored locally (that automation is coming…) so the simple approach is to directly use the copy module:

  - name: Certificates | Place the certificate in the correct location on the host
    copy:
      src: certs/octo_one-bundle.pem
      dest: /etc/ssl/octo_one-bundle.pem
    register: cert1

  - name: Certificates | Place the CA bundle in the correct location on the host
    copy:
      src: certs/ca-bundle.pem
      dest: /etc/ssl/ca-bundle.pem

You may notice that I have elected to copy both the certificate and the CA bundle to the host. It is strictly true that only the service cert (octo_one-bundle.pem) is required, I would like to incorporate the use of certificates for client authentication in the future, which will require access to the CA. Best install it now.

Load the Certs!

As HAProxy is already in use in the default OctoPi configuration, no installation is necessary - just a minor patch to the configuration files.

Whilst Ansible is generally at its most robust when using templated configuration files, when only minor changes are required to the file it can be okay to carefully apply lineinfile. Be warned, this can go badly if you do not have appropriate regex pattern matching to ensure that lines will be amended if changes are made in the source!

These changes are summarised by:

  1. Forcing all HTTP connections on port 80 to be redirected to HTTPS on 443.
  2. Specifying the path to the certificate the server should use.
  - name: Configuration | Require HAProxy to upgrade connections to HTTPS
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      insertafter: 'option forwardfor except 127.0.0.1'
      regexp: '^\s*redirect\s+scheme\s+https'
      line: "        redirect scheme https if !{ hdr(Host) -i 127.0.0.1 } !{ ssl_fc }"
    
  - name: Configuration | Map HAProxy to the correct SSL Certificate
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
      line: "        bind :::443 v4v6 ssl crt /etc/ssl/octo_one-bundle.pem"

You will notice that cert paths are currently hard coded. I only have one 3D Printer, when the next one arrives I will deal with the scaling problem.

Restarting HAProxy

After changing the configuration file it is required to restart HAProxy. This is done directly with the service module:

  - name: Post | Restart HAProxy if required
    ansible.builtin.service:
      name: haproxy
      state: restarted

However, this will enforce restarting HAProxy every time this playbook is executed. A good practice when dealing with Ansible is to only undertake actions that are essential, so let’s try and keep track of what changes are being made. This is done by registering the output of the previous commands as follows:

  - name: Certificates | Place the certificate in the correct location on the host
    copy:
      src: certs/octo_one-bundle.pem
      dest: /etc/ssl/octo_one-bundle.pem
    register: cert1

  - name: Certificates | Place the CA bundle in the correct location on the host
    copy:
      src: certs/ca-bundle.pem
      dest: /etc/ssl/ca-bundle.pem
    register: cert2

  - name: Configuration | Require HAProxy to upgrade connections to HTTPS
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      insertafter: 'option forwardfor except 127.0.0.1'
      regexp: '^\s*redirect\s+scheme\s+https'
      line: "        redirect scheme https if !{ hdr(Host) -i 127.0.0.1 } !{ ssl_fc }"
    register: config1

  - name: Configuration | Map HAProxy to the correct SSL Certificate
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
      line: "        bind :::443 v4v6 ssl crt /etc/ssl/octo_one-bundle.pem"
    register: config2

  - name: Post | Restart HAProxy if required
    ansible.builtin.service:
      name: haproxy
      state: restarted
    when: cert1.changed or cert2.changed or config1.changed or config2.changed

Won’t someone think of the prints though?

All this automation is great, but we don’t want it to get in the way of the number one reason for this existing - printing parts! In order to ensure minimal interruptions to the little Pi that is running this, I’d like to incorporate a check to ensure that the printer is not in the middle of a print prior to running the rest of the script. This is done by using the built in API to OctoPrint.

To interact with this API a Application Key is needed. This is generated in the OctoPrint Settings Menu. Once generated, this can be assigned to variable in Ansible. Ideally API keys should be stored securely in a credential store such as Ansible Vault, but due to the risk on this particular interaction I have chosen to store it just as a playbook variable. Another thing to fix in the next iteration!

The following block of code connects to the OctoPrint API and gets the current status. If the status is “Printing”, the playbook fails to avoid running any further commands against the host whilst it may be busy.

  - name: Printing Check | Status API Call
    ansible.builtin.uri:
      validate_certs: no
      url: "https:///api/job"
      body_format: json
      headers:
        X-Api-Key: ""
    register: printer_status_response
    connection: local

  - name: Printing Check | Get State from Response
    ansible.builtin.set_fact: 
      printer_state: ""

  - name: Printing Check | Fail due to currently printing
    fail: 
      msg: "Unable to carry out action as printer is running."
    when: printer_state == "Printing"

The connection: local directive on the uri module call is done to enforce this coming from the host running Ansible. Although in this case it’s just my laptop, if this code were to support execution from Ansible Tower (or AWX) understanding where the API calls are running from is important to ensure appropriate security controls are in place.

What about the clients?

So far this configuration has been all about setting up standard server side HTTPS. Provided the appropriate CA certificates have been loaded onto a client, this connection will be trusted. However, I mentioned earlier that I would eventually like to client certificate based authentication working. This enables the server to trust the client as authentic in addition to the previous situation whereby the client trusted the server.

In HAProxy this is done by including a path to the CA file as well as enforcing verification of client certificates. Modifying the previous lineinfile task for client support is as simple as:

  - name: Configuration | Map HAProxy to the correct SSL Certificate and enforce Client Certificate
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
      line: "        bind :::443 v4v6 ssl crt /etc/ssl/octo_one-bundle.pem ca-file /etc/ssl/ca-bundle.pem verify required"
    register: config2

This will then require the client to produce a certificate prior to connection.

I had this all configured and working with the browser, however I rapidly discovered that it breaks the Prusa-Slicer OctoPrint integration. I use this heavily, so the client certificate functionality had to go! From reading through github issues it appears that the OctoPrint integration is not actively developed by the development team and it’s probably unlike that the niche functionality of providing a client certificate will be added in the near future.

The Wrap-Up

This runs locally on my laptop as a standalone Ansible Playbook. It is simple to execute and provides all the functionality I need, whilst hopefully future proofing any changes that may occur in the OctoPrint configuration files as upgrades are performed.

The complete code is:

- hosts: <YOUR HOST HERE>
  gather_facts: minimal
  become: yes
  vars:
    octoprint_api_key: <YOUR API KEY HERE>

  tasks: 
  - name: Printing Check | Status API Call
    ansible.builtin.uri:
      validate_certs: no
      url: "https:///api/job"
      body_format: json
      headers:
        X-Api-Key: ""
    register: printer_status_response
    connection: local

  - name: Printing Check | Get State from Response
    ansible.builtin.set_fact: 
      printer_state: ""

  - name: Printing Check | Fail due to currently printing
    fail: 
      msg: "Unable to carry out action as printer is running."
    when: printer_state == "Printing"

  - name: Certificates | Place the certificate in the correct location on the host
    copy:
      src: certs/<CERT>-bundle.pem
      dest: /etc/ssl/<CERT>-bundle.pem
    register: cert1

  - name: Certificates | Place the CA bundle in the correct location on the host
    copy:
      src: certs/<CA>-bundle.pem
      dest: /etc/ssl/<CA>-bundle.pem
    register: cert2

  - name: Configuration | Require HAProxy to upgrade connections to HTTPS
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      insertafter: 'option forwardfor except 127.0.0.1'
      regexp: '^\s*redirect\s+scheme\s+https'
      line: "        redirect scheme https if !{ hdr(Host) -i 127.0.0.1 } !{ ssl_fc }"
    register: config1

  # - name: Configuration | Map HAProxy to the correct SSL Certificate and enforce Client Certificate
  #   ansible.builtin.lineinfile:
  #     path: /etc/haproxy/haproxy.cfg
  #     regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
  #     line: "        bind :::443 v4v6 ssl crt /etc/ssl/<CERT>-bundle.pem ca-file /etc/ssl/<CA>-bundle.pem verify required"
  #   register: config2

  - name: Configuration | Map HAProxy to the correct SSL Certificate
    ansible.builtin.lineinfile:
      path: /etc/haproxy/haproxy.cfg
      regexp: '^\s*bind\s+:::443\s+v4v6\s+ssl\s+crt\s+'
      line: "        bind :::443 v4v6 ssl crt /etc/ssl/<CERT>-bundle.pem"
    register: config2

  - name: Post | Restart HAProxy if required
    ansible.builtin.service:
      name: haproxy
      state: restarted
    when: cert1.changed or cert2.changed or config1.changed or config2.changed

If further functionality is added or I manage to convert this to a role in the future I will update this post with a link, but at the moment it honestly doesn’t seem like the effort.

I’m honestly not sure this will ever help someone, if it does that is great! It is however a great example of how trivial it can be to integrate Ansible with a third party API indicating application state.

Originally published June 28, 2021 (Updated June 29, 2021)

Related posts :