Environments: Cert work
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
certbotwith thedns-cloudflareplugin (installed earlier) - indicating the domain with
-d
- using
- 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
certonlyto only gets the cert. This pops it in the default/etc/letsencrypt/live/directory, and leaves setup to somewhere else. An--ngnixoption 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
ssldirectory 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
certbotcommand 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' }}"