Using GitHub Actions to Manage Certbot (Let’s Encrypt) Certificates

GitHub Actions is an excellent source for all things automation. For personal accounts, there’s a limited free offering that allows you to run automation jobs. I use GH actions to update my websites.

I don’t think I would have written this if no existing solution worked with GitHub Actions. But after a search, I did not come up with anything that would lend itself to what I was trying to do. So, as the old saying goes, necessity is the mother of invention, and MacGyver is the father.

My problem was simple: automatically renew a certificate on a timer, then apply that certificate to an Azure Function. I could have programmed this using a Function App or a thousand other ways. But this solution turned out to be simple and used some of the available resources on GitHub that made it (more or less) secure, efficient, and free.

When I started on this little project, I wanted to write something more general-purpose, but I found that my approach was a niche solution with GitHub Actions, CloudFlare, Azure Functions. There may be other folks out there using this combination, but I did not find anyone. In any case, certbot provides plugins for several DNS providers, and the certs generated by certbot can be used on Azure Functions, Azure App Services, Key Vault, or practically any other service on Azure that accepts a PFX or a set of PEM files. So, hopefully, this script can at least serve as a template to help see how this *can* be done, even though the solutions may vary.

So here’s the recipe:

First, create a repository and make it private.  CertBot uses files stored on the file system to store information for renewing the certificates, including the private keys for your website. If a private repository isn’t secure enough for you, this might not be the best solution — more on this later.

Once the you’ve created the repo, create a script file. I recommend doing this in the GitHub Portal or on an Ubuntu host. CertBot will run on a Linux host on GitHub and uses symlinks, which can be flakey on Git for Windows or similar options. Also, GitHub Runners use Ubuntu by default. This script is a SAMPLE script. Your script will likely be different. I wrote this script as a point solution using Cloudflare and Azure Functions. How you use your certificate will vary depending on your solution.

#!/bin/bash

echo "Install dependencies"
sudo apt install -y certbot azure-cli openssl python3-certbot-dns-cloudflare

echo "Creates the CloudFlare Credentials"
echo "dns_cloudflare_api_key = $CF_API_KEY" > ./cftoken
echo "dns_cloudflare_email = $CF_EMAIL" >> ./cftoken

# This command calls certbot for Let's Encrpypt. This example uses CloudFlare DNS for validation. Let's Encrypt supports many different DNS providers
# https://certbot.eff.org/docs/using.html?highlight=dns#dns-plugins
# Configure cerbot to use your DNS provider. Web-based providers are not possible using this method.
# Also, you may want to use the --staging flag while working in development.

echo "Renews the certificates"
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ./cftoken -d $DOMAIN -m $CB_EMAIL --config-dir . --cert-path . --non-interactive --agree-tos

# Certbot runs as root, so it creates all the files as root. This changes the permissions so that other utilities can read the file.
echo "Set file permissions"
sudo chmod -R 777 ./*

echo "Creates a PFX for Azure"
openssl pkcs12 -inkey ./live/$DOMAIN/privkey.pem -in live/$DOMAIN/fullchain.pem -export -out $DOMAIN.pfx -passout pass:pwd123

echo "Login to Azure with a Service Principal"
az login --service-principal -u $SP_CLIENT_ID -p $SP_SECRET --tenant $TENANT_ID

echo "Uploads the certificate and gets the Thumbprint"
THUMBPRINT=$(az functionapp config ssl upload --certificate-file $DOMAIN.pfx --certificate-password pwd123 -n $FUNCTION_APP -g $RESOURCE_GROUP | jq --raw-output '.|.thumbprint')

echo "Binds the funciton app to the certificate"
az functionapp config ssl bind --certificate-thumbprint $THUMBPRINT --ssl-type SNI -n $FUNCTION_APP -g $RESOURCE_GROUP

ls -l

echo "Cleans Up Files"
rm ./cftoken
rm $DOMAIN.pfx

exit 0

Now, create a new Git Hub Action. You can do this in the GitHub Portal by selecting Actions -> New Workflow. Take the defaults and replace it with the following code.  Take note of the comments in the code. These control the schedule, among other things.

name: Renew Certs

# Controls when the workflow will run
on:
  # Triggers the workflow on a timer. This particular timer runs on the first day of the month. CertBot certs are renewable every 30 days, so this should work for that purpose.
  schedule:
    - cron:  '0 5 */31 * *'
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  job1:
    env:
      # These environmental variables values are set from repository secrets. The sample script references these values for authenticating against Cloudflare and Azure
      DOMAIN: ${{ secrets.DOMAIN }} 
      CB_EMAIL: ${{ secrets.CB_EMAIL }} 
      SP_CLIENT_ID: ${{ secrets.SP_CLIENT_ID }} 
      SP_SECRET: ${{ secrets.SP_SECRET }} 
      TENANT_ID: ${{ secrets.TENANT_ID }} 
      FUNCTION_APP: ${{ secrets.FUNCTION_APP }} 
      RESOURCE_GROUP: ${{ secrets.RESOURCE_GROUP }} 
      CF_API_KEY: ${{ secrets.CF_API_KEY }} 
      CF_EMAIL: ${{ secrets.CF_EMAIL }} 
    name: Modifiy repository files
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2.3.2
      - name: Run the scipt
        working-directory: ${{env.working-directory}}
        run: |          
          bash certbot.sh
      - name: Commit and push changes
        working-directory: ${{env.working-directory}}
        run: |
          git config --global user.name "githubuser"
          git config --global user.email "email@example.com"
          git add -A
          git commit -m "Update Certs"
          git push

Now, invoke the action manually in the GitHub Portal by selecting Actions -> Renew Certs -> Run Workflow. This will invoke the script, create the scaffolding, and download the initial set of certificates from Let’s Encrypt. Once the script completes, the scripts should show up in the repository.

Now about all that annoy, pesky, security stuff. This solution stores key files in the repository, and under normal circumstances, this would be a considerable security risk. This may not be a big deal for a personal repository, but for other repos, this could be a huge problem. There are ways to mitigate this, though. One solution would be to encrypt key files (.pem), specifically the private key files. The rest of the files are pretty useless without the private keys. You can do this using openssl or pgp, and store your encryption key as a secret. You could also use a program like 7Zip to put all the files in a password-protected archive and zip/unzip it when the script runs. Whether you encrypt or zip, you can store the password/key in a secret in the repository secrets.

Well, that’s it! The script will run on the schedule in the GitHub Action, and it will use the scripts in the repo with each subsequent run. Sit back, get a cup of coffee, and enjoy the automation of GitHub Actions and certbot!

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X