Photo by Emmanuelle Magnenat / Unsplash

Environments: Cert work

Tools Nov 12, 2025

If you're setting up an environment that has some sort of presence on the internet, you're going to need a certificate so that people can get to it with https.

What's my use case?

My play environments use VSCode in the browser. VSCode expects encrypted connections, so needs https.

Your use case might be able to live with http, but I reckon that's probably only temporary.

Here's a short ansible task that I built, along with LLM assistance. On the machine that has the site that needs a certificate:

  • It installs python libraries to do the work.
  • It makes a credentials file including the secret cloudflare credential, restricting its access.
  • It checks whether a cert exists (it doesn't want to request a new cert if it already has one) and whether it is about to expire (and gets a new cert if it expires within 30 days).
  • It gets the cert from LetsEncrypt,
    • passing the cloudflare credentials as a way to identify to Certbot (and LetsEncrypt) that the entity requesting a cert is entitled to that cert. There are other ways
    • using certonly to only gets the cert. This pops it in the default /etc/letsencrypt/live/ directory, and leaves setup to somewhere else. An --ngnix option would set up ssl here, but I'm leaving it until later. This may change.
    • agreeing to everything without pause
  • It gets rid of the credentials file (whether it got a cert or not)
  • It reloads the nginx server (so the cert gets used)
  • It displays a message (for log needs).

Why use a credentials file?

Q: After going to the trouble of keeping a credential secret, why write it to a file?

A: Because the certbot cloudflare plugin insists.

Learn from my mistake

In my initial script, I didn't delete the credentials file, leaving it visible to anyone with access to my play environments... and my environments give command-line access, so that's anyone. Oh dear. It was restricted to root, but with the access I give workshop participants, even that might be flaky.

Thankfully, cloudflare's credentials can be limited. Still, yikes.

How did I use LLMs and what did they get wrong

My Ansible – indeed, my yaml – is poor, and I used Claude in various guises to get me to this point.

Initially, over several LLM requests and fiddling, I got something 'working' – as in I got an Ansible-built repeatable environment that used my configuration and secrets and matched my description. It was a single awful file.

I used my smarts, and more LLM requests, to actually read the file and split it into coherent parts. This was one part, and was aggregated from chunks from all over the monolith.

When I looked at it a third time, I recognised that it was doing undesirable things.

  • it wrote credentials to a file, then left it in place for any fule to find. Not to read, though.
  • if a package manager on the server was still busily installing, this script flaked out when asking to install certbot. So it flaked out the first time, most times. It needed to retry, not barf.
  • it created an ssl directory and did other ssl setup tasks – those were duplicated later, and some indeed needed to be done later. So those tasks are deferred, but I imagine I'll come back to this.
  • it suggested a set of entirely spurious possible options to the certbot command when I wanted to pass credentials directly. Although it had used the 'right' plugins, it didn't have information to use them. I went to find the page, gave it to the LLM, and asked it to double-check my work.

I also asked the LLMs to check my approaches and to explain things to me. It's particularly handy to give the right webpage / manual page as an attachment, because I can ask questions and get answers that make sense of the liberally-scattered special terms. I use Msty and a jina.ai key to do that.

- name: Install certbot and DNS plugins
  apt:
    name: 
      - certbot
      - python3-certbot-nginx
      - python3-certbot-dns-cloudflare
    state: present
    force_apt_get: yes
  environment:
    DEBIAN_FRONTEND: noninteractive
  async: 300
  poll: 10
  when: inventory_hostname in groups['new_droplets']
  ## often needs a couple of retries as packages are locked.
  retries: 5
  delay: 30
  until: result is succeeded
  register: result

- name: Create Cloudflare credentials file for certbot
  copy:
    content: |
      dns_cloudflare_api_token = {{ cloudflare_api_token }}
    dest: /etc/letsencrypt/cloudflare.ini
    mode: '0600'
    owner: root
    group: root

- name: Check if Let's Encrypt certificate already exists
  stat:
    path: "/etc/letsencrypt/live/{{ group_name }}.{{ main_domain }}/fullchain.pem"
  register: cert_exists

- name: Check certificate expiration if it exists
  command: openssl x509 -in "/etc/letsencrypt/live/{{ group_name }}.{{ main_domain }}/fullchain.pem" -noout -checkend 2592000
  register: cert_valid
  failed_when: false
  when: cert_exists.stat.exists

## Needs to use a credentials file, as option to use token directly does not exist
## Set to always delete the credentials file, whether cert obtained or not
## puts cert into /etc/letsencrypt/live/{{ domain }}/
- block:
    - name: Get Let's Encrypt certificate using DNS challenge
      command: certbot certonly 
        --dns-cloudflare 
        --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini 
        --dns-cloudflare-propagation-seconds 60 
        -d {{ group_name }}.{{ main_domain }} 
        --email {{ certbot_email }} 
        --agree-tos --non-interactive
      when: not cert_exists.stat.exists or (cert_exists.stat.exists and cert_valid.rc != 0)
      notify: reload nginx
  always:
    - name: Remove Cloudflare credentials file
      file:
        path: /etc/letsencrypt/cloudflare.ini
        state: absent


- name: Display certificate status
  debug:
    msg: "Certificate for {{ group_name }}.{{ main_domain }} {{ 'already exists and is valid' if (cert_exists.stat.exists and cert_valid.rc == 0) else 'was created/renewed' }}"

Tags

James Lyndsay

Getting better at software testing. Singing in Bulgarian. Staying in. Going out. Listening. Talking. Writing. Making.