From d408884f188a3aa1bd54bd00021b5e42c7af5ee5 Mon Sep 17 00:00:00 2001 From: Simon Caron <8635747+simoncaron@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:56:49 -0400 Subject: [PATCH] Add Cloudflare DNS-01 Challenge * Add Cloudflare support * Ensure cron install * linting * Add Redhat cron install * Update vars --- .github/FUNDING.yml | 4 -- .github/workflows/ci.yml | 6 +-- .github/workflows/release.yml | 4 +- .github/workflows/stale.yml | 34 ---------------- README.md | 29 +++++++++++--- defaults/main.yml | 12 +++++- meta/main.yml | 2 +- molecule/default/converge.yml | 2 +- molecule/default/playbook-snap-install.yml | 2 +- molecule/default/playbook-source-install.yml | 2 +- .../default/playbook-standalone-nginx-aws.yml | 2 +- tasks/create-cert-dns-cloudflare.yml | 39 +++++++++++++++++++ tasks/install-from-source.yml | 6 +++ tasks/install-with-package.yml | 6 +++ tasks/install-with-snap.yml | 5 +++ tasks/main.yml | 8 ++++ tasks/renew-cron.yml | 5 +++ templates/cloudflare.ini.j2 | 7 ++++ vars/RedHat.yml | 4 ++ vars/Ubuntu-16.04.yml | 2 - vars/default.yml | 2 + 21 files changed, 126 insertions(+), 57 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/stale.yml create mode 100644 tasks/create-cert-dns-cloudflare.yml create mode 100644 templates/cloudflare.ini.j2 create mode 100644 vars/RedHat.yml delete mode 100644 vars/Ubuntu-16.04.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 96b4938..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms ---- -github: geerlingguy -patreon: geerlingguy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e402a55..e15ce93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ name: CI defaults: run: - working-directory: 'geerlingguy.certbot' + working-directory: 'simoncaron.certbot' jobs: @@ -21,7 +21,7 @@ jobs: - name: Check out the codebase. uses: actions/checkout@v4 with: - path: 'geerlingguy.certbot' + path: 'simoncaron.certbot' - name: Set up Python 3. uses: actions/setup-python@v5 @@ -59,7 +59,7 @@ jobs: - name: Check out the codebase. uses: actions/checkout@v4 with: - path: 'geerlingguy.certbot' + path: 'simoncaron.certbot' - name: Set up Python 3. uses: actions/setup-python@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd4d88b..18c69ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ name: Release defaults: run: - working-directory: 'geerlingguy.certbot' + working-directory: 'simoncaron.certbot' jobs: @@ -24,7 +24,7 @@ jobs: - name: Check out the codebase. uses: actions/checkout@v4 with: - path: 'geerlingguy.certbot' + path: 'simoncaron.certbot' - name: Set up Python 3. uses: actions/setup-python@v5 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index ae4514f..0000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Close inactive issues -'on': - schedule: - - cron: "55 12 * * 1" # semi-random time - -jobs: - close-issues: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@v8 - with: - days-before-stale: 120 - days-before-close: 60 - exempt-issue-labels: bug,pinned,security,planned - exempt-pr-labels: bug,pinned,security,planned - stale-issue-label: "stale" - stale-pr-label: "stale" - stale-issue-message: | - This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! - - Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark issues as stale. - close-issue-message: | - This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. - stale-pr-message: | - This pr has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! - - Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark issues as stale. - close-pr-message: | - This pr has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. - repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 40d147a..8791c67 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ansible Role: Certbot (for Let's Encrypt) -[![CI](https://github.com/geerlingguy/ansible-role-certbot/actions/workflows/ci.yml/badge.svg)](https://github.com/geerlingguy/ansible-role-certbot/actions/workflows/ci.yml) +[![CI](https://github.com/simoncaron/ansible-role-certbot/actions/workflows/ci.yml/badge.svg)](https://github.com/simoncaron/ansible-role-certbot/actions/workflows/ci.yml) Installs and configures Certbot (for Let's Encrypt). @@ -26,7 +26,7 @@ By default, this role configures a cron job to run under the provided user accou ### Automatic Certificate Generation -Currently the `standalone` and `webroot` method are supported for generating new certificates using this role. +Currently the `standalone`, `webroot`, and `dns-cloudflare` methods are supported for generating new certificates using this role. **For a complete example**: see the fully functional test playbook in [molecule/default/playbook-standalone-nginx-aws.yml](molecule/default/playbook-standalone-nginx-aws.yml). @@ -36,7 +36,7 @@ Set `certbot_create_if_missing` to `yes` or `True` to let this role generate cer certbot_create_method: standalone -Set the method used for generating certs with the `certbot_create_method` variable — current allowed values are: `standalone` or `webroot`. +Set the method used for generating certs with the `certbot_create_method` variable — current allowed values are: `standalone`, `webroot`, or `dns-cloudflare`. certbot_testmode: false @@ -86,6 +86,25 @@ This install method is currently experimental and may or may not work across all When using the `webroot` creation method, a `webroot` item has to be provided for every `certbot_certs` item, specifying which directory to use for the authentication. Also, make sure your webserver correctly delivers contents from this directory. +#### DNS-01 Challenge with Cloudflare + +When using the `dns-cloudflare` creation method, you need to configure Cloudflare DNS credentials: + + certbot_cloudflare_email: "your-email@example.com" + certbot_cloudflare_api_key: "your-global-api-key" + # OR use API token instead (recommended): + certbot_cloudflare_api_token: "your-api-token" + certbot_cloudflare_propagation_seconds: 10 + +You can use either the email + Global API Key combination OR an API token. The API token method is recommended as it's more secure and allows for more granular permissions. + +For API token setup: +1. Go to Cloudflare Dashboard → My Profile → API Tokens +2. Create a token with `Zone:DNS:Edit` permissions for the zones you want certificates for +3. Set the `certbot_cloudflare_api_token` variable with this token + +This method supports wildcard certificates and doesn't require your server to be publicly accessible on ports 80/443. + ### Source Installation from Git You can install Certbot from it's Git source repository if desired with `certbot_install_method: source`. This might be useful in several cases, but especially when older distributions don't have Certbot packages available (e.g. CentOS < 7, Ubuntu < 16.10 and Debian < 8). @@ -102,7 +121,7 @@ The directory inside which Certbot will be cloned. ### Wildcard Certificates -Let's Encrypt supports [generating wildcard certificates](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579), but the process for generating and using them is slightly more involved. See comments in [this pull request](https://github.com/geerlingguy/ansible-role-certbot/pull/60#issuecomment-423919284) for an example of how to use this role to maintain wildcard certs. +Let's Encrypt supports [generating wildcard certificates](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579), but the process for generating and using them is slightly more involved. See comments in [this pull request](https://github.com/simoncaron/ansible-role-certbot/pull/60#issuecomment-423919284) for an example of how to use this role to maintain wildcard certs. Michael Porter also has a walkthrough of [Creating A Let’s Encrypt Wildcard Cert With Ansible](https://www.michaelpporter.com/2018/09/creating-a-wildcard-cert-with-ansible/), specifically with Cloudflare. @@ -120,7 +139,7 @@ None. certbot_auto_renew_hour: "5" roles: - - geerlingguy.certbot + - simoncaron.certbot See other examples in the `tests/` directory. diff --git a/defaults/main.yml b/defaults/main.yml index 3f29db0..929edef 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -12,12 +12,18 @@ certbot_hsts: false # Parameters used when creating new Certbot certs. certbot_create_if_missing: false -certbot_create_method: standalone +certbot_create_method: dns-cloudflare certbot_create_extra_args: "" certbot_admin_email: email@example.com certbot_expand: false -# Default webroot, overwritten by individual per-cert webroot directories +# Cloudflare DNS credentials (use either API token OR email+api_key) +certbot_cloudflare_email: "" +certbot_cloudflare_api_key: "" +certbot_cloudflare_api_token: "" +certbot_cloudflare_propagation_seconds: 60 + +# Default webroot, overwritten by individual per-cert webroot directories (not used with DNS-01) certbot_webroot: /var/www/letsencrypt certbot_certs: [] @@ -39,6 +45,8 @@ certbot_create_command: >- {{ '--expand' if certbot_expand else '' }} {{ '--webroot-path ' if certbot_create_method == 'webroot' else '' }} {{ cert_item.webroot | default(certbot_webroot) if certbot_create_method == 'webroot' else '' }} + {{ '--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini' if certbot_create_method == 'dns-cloudflare' else '' }} + {{ '--dns-cloudflare-propagation-seconds ' + (certbot_cloudflare_propagation_seconds | string) if certbot_create_method == 'dns-cloudflare' else '' }} {{ certbot_create_extra_args }} --cert-name {{ cert_item_name }} -d {{ cert_item.domains | join(',') }} diff --git a/meta/main.yml b/meta/main.yml index 730be61..5699f69 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -3,7 +3,7 @@ dependencies: [] galaxy_info: role_name: certbot - author: geerlingguy + author: simoncaron description: "Installs and configures Certbot (for Let's Encrypt)." company: "Midwestern Mac, LLC" license: "license (BSD, MIT)" diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 8f74aef..e8facbe 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -25,4 +25,4 @@ when: ansible_os_family == 'Debian' roles: - - geerlingguy.certbot + - simoncaron.certbot diff --git a/molecule/default/playbook-snap-install.yml b/molecule/default/playbook-snap-install.yml index 572f1b5..3f2e002 100644 --- a/molecule/default/playbook-snap-install.yml +++ b/molecule/default/playbook-snap-install.yml @@ -23,4 +23,4 @@ roles: - geerlingguy.git - - geerlingguy.certbot + - simoncaron.certbot diff --git a/molecule/default/playbook-source-install.yml b/molecule/default/playbook-source-install.yml index 28da1f3..746a754 100644 --- a/molecule/default/playbook-source-install.yml +++ b/molecule/default/playbook-source-install.yml @@ -23,4 +23,4 @@ roles: - geerlingguy.git - - geerlingguy.certbot + - simoncaron.certbot diff --git a/molecule/default/playbook-standalone-nginx-aws.yml b/molecule/default/playbook-standalone-nginx-aws.yml index a5c53e5..563c38b 100644 --- a/molecule/default/playbook-standalone-nginx-aws.yml +++ b/molecule/default/playbook-standalone-nginx-aws.yml @@ -126,7 +126,7 @@ when: ansible_os_family == 'Debian' roles: - - geerlingguy.certbot + - simoncaron.certbot - geerlingguy.nginx tasks: diff --git a/tasks/create-cert-dns-cloudflare.yml b/tasks/create-cert-dns-cloudflare.yml new file mode 100644 index 0000000..e2c46b5 --- /dev/null +++ b/tasks/create-cert-dns-cloudflare.yml @@ -0,0 +1,39 @@ +--- +- name: Determine certificate name + set_fact: + cert_item_name: "{{ cert_item.name | default(cert_item.domains | first | replace('*.', '')) }}" + +- name: Check if certificate already exists. + stat: + path: /etc/letsencrypt/live/{{ cert_item_name }}/cert.pem + register: letsencrypt_cert + +- name: Create Cloudflare credentials file + template: + src: cloudflare.ini.j2 + dest: /etc/letsencrypt/cloudflare.ini + owner: root + group: root + mode: 0600 + when: certbot_cloudflare_email or certbot_cloudflare_api_token + +- name: Check if domains have changed + block: + - name: Register certificate domains + shell: "{{ certbot_script }} certificates --cert-name {{ cert_item_name }} | grep Domains | cut -d':' -f2" + changed_when: false + register: letsencrypt_cert_domains_dirty + + - name: Cleanup domain list + set_fact: + letsencrypt_cert_domains: "{{ letsencrypt_cert_domains_dirty.stdout | trim | split(' ') | map('trim') | select('!=', '') | list | sort }}" + + - name: Determine if domains have changed + set_fact: + letsencrypt_cert_domains_changed: "{{ letsencrypt_cert_domains != (cert_item.domains | map('trim') | select('!=', '') | list | sort) }}" + + when: letsencrypt_cert.stat.exists + +- name: Generate new certificate if one doesn't exist. + command: "{{ certbot_create_command }}" + when: not letsencrypt_cert.stat.exists or letsencrypt_cert_domains_changed | default(false) diff --git a/tasks/install-from-source.yml b/tasks/install-from-source.yml index daee685..49ba673 100644 --- a/tasks/install-from-source.yml +++ b/tasks/install-from-source.yml @@ -15,3 +15,9 @@ file: path: "{{ certbot_script }}" mode: 0755 + +- name: Install certbot-dns-cloudflare plugin via pip. + pip: + name: certbot-dns-cloudflare + state: present + when: certbot_create_method == 'dns-cloudflare' diff --git a/tasks/install-with-package.yml b/tasks/install-with-package.yml index aec074c..90807df 100644 --- a/tasks/install-with-package.yml +++ b/tasks/install-with-package.yml @@ -4,6 +4,12 @@ name: "{{ certbot_package }}" state: present +- name: Install Certbot Cloudflare DNS plugin. + package: + name: "{{ certbot_dns_cloudflare_package }}" + state: present + when: certbot_create_method == 'dns-cloudflare' + - name: Set Certbot script variable. set_fact: certbot_script: "{{ certbot_package }}" diff --git a/tasks/install-with-snap.yml b/tasks/install-with-snap.yml index 7a0ca65..84eb691 100644 --- a/tasks/install-with-snap.yml +++ b/tasks/install-with-snap.yml @@ -29,6 +29,11 @@ name: certbot classic: true +- name: Install certbot-dns-cloudflare plugin via snap. + snap: + name: certbot-dns-cloudflare + when: certbot_create_method == 'dns-cloudflare' + - name: Symlink certbot into place. file: src: /snap/bin/certbot diff --git a/tasks/main.yml b/tasks/main.yml index 894143c..06b3792 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -29,5 +29,13 @@ loop_control: loop_var: cert_item +- include_tasks: create-cert-dns-cloudflare.yml + with_items: "{{ certbot_certs }}" + when: + - certbot_create_if_missing + - certbot_create_method == 'dns-cloudflare' + loop_control: + loop_var: cert_item + - import_tasks: renew-cron.yml when: certbot_auto_renew diff --git a/tasks/renew-cron.yml b/tasks/renew-cron.yml index 394a30e..b01ce04 100644 --- a/tasks/renew-cron.yml +++ b/tasks/renew-cron.yml @@ -1,4 +1,9 @@ --- +- name: Install cron package (if not present). + package: + name: "{{ certbot_cron_package }}" + state: present + - name: Add cron job for certbot renewal (if configured). cron: name: Certbot automatic renewal. diff --git a/templates/cloudflare.ini.j2 b/templates/cloudflare.ini.j2 new file mode 100644 index 0000000..e6f81fc --- /dev/null +++ b/templates/cloudflare.ini.j2 @@ -0,0 +1,7 @@ +# Cloudflare API credentials +{% if certbot_cloudflare_api_token %} +dns_cloudflare_api_token = {{ certbot_cloudflare_api_token }} +{% else %} +dns_cloudflare_email = {{ certbot_cloudflare_email }} +dns_cloudflare_api_key = {{ certbot_cloudflare_api_key }} +{% endif %} \ No newline at end of file diff --git a/vars/RedHat.yml b/vars/RedHat.yml new file mode 100644 index 0000000..108330c --- /dev/null +++ b/vars/RedHat.yml @@ -0,0 +1,4 @@ +--- +certbot_package: certbot +certbot_dns_cloudflare_package: python3-certbot-dns-cloudflare +certbot_cron_package: cronie diff --git a/vars/Ubuntu-16.04.yml b/vars/Ubuntu-16.04.yml deleted file mode 100644 index 83cf124..0000000 --- a/vars/Ubuntu-16.04.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -certbot_package: letsencrypt diff --git a/vars/default.yml b/vars/default.yml index d88f2dc..68ffea3 100644 --- a/vars/default.yml +++ b/vars/default.yml @@ -1,2 +1,4 @@ --- certbot_package: certbot +certbot_dns_cloudflare_package: python3-certbot-dns-cloudflare +certbot_cron_package: cron