> ## Content Index
> Fetch the complete content index at: https://www.workroom-productions.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Environments: Cert work
- URL: https://www.workroom-productions.com/environments-cert-work/
- Published: 2025-11-12T11:56:42.000Z
- Updated: 2025-11-12T12:16:33.000Z
- Description: Ansible to get a certificate by leveraging CloudFlare with LetsEncrypt
- Author: James Lyndsay
- Tags: Tools, tiny tool

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,  
  - using [certbot](https://eff-certbot.readthedocs.io/en/stable/using.html#certbot-commands) with the [dns-cloudflare plugin](https://certbot-dns-cloudflare.readthedocs.io/en/stable/) (installed earlier)
  - indicating the domain with `-d`
  - 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](https://certbot-dns-cloudflare.readthedocs.io/en/stable/) 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](https://msty.ai) and a [jina.ai](https://jina.ai/) key to do that.

```yaml
- 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' }}"

```