--- # --------------------------------------------------------------------------- # TLS certificate lifecycle via lego (ACME DNS-01 against Cloudflare). # # This file only READS the Cloudflare token from elk_cloudflare_token_env_file. # It never receives the token as an Ansible variable - the token is expected # to already exist on disk (root:root, 0600), placed there by cloud-init at # provisioning time. See the role README for the IaC side of this. # --------------------------------------------------------------------------- - name: Check if Cloudflare token file exists at legacy/staging path ansible.builtin.stat: path: /etc/retailor/cloudflare-token.env register: elk_cf_token_src_stat - name: Ensure folder exists ansible.builtin.command: cmd: mkdir -p "{{ elk_lego_path }}" when: elk_cf_token_src_stat.stat.exists changed_when: true - name: Move Cloudflare token file into place if found at staging path ansible.builtin.command: cmd: cp /etc/retailor/cloudflare-token.env "{{ elk_cloudflare_token_env_file }}" when: elk_cf_token_src_stat.stat.exists changed_when: true - name: Assert the Cloudflare token file exists at the expected location ansible.builtin.stat: path: "{{ elk_cloudflare_token_env_file }}" register: elk_cf_token_stat - name: Fail early with a clear message if the token file is missing ansible.builtin.fail: msg: > {{ elk_cloudflare_token_env_file }} does not exist. This role expects cloud-init (or equivalent provisioning) to have already written a scoped Cloudflare API token here before Ansible runs, or a legacy copy at /etc/retailor/cloudflare-token.env to be moved into place. See the IaC notes in the role README. when: not elk_cf_token_stat.stat.exists - name: Ensure the token file has correct ownership and permissions ansible.builtin.file: path: "{{ elk_cloudflare_token_env_file }}" mode: "0640" owner: root group: root when: elk_cf_token_stat.stat.exists - name: Re-stat token file after permission enforcement ansible.builtin.stat: path: "{{ elk_cloudflare_token_env_file }}" register: elk_cf_token_stat - name: Verify the token file is not world readable ansible.builtin.assert: that: - elk_cf_token_stat.stat.mode == "0640" fail_msg: > {{ elk_cloudflare_token_env_file }} should be mode 0640 (currently {{ elk_cf_token_stat.stat.mode }}) since it holds a Cloudflare API credential. success_msg: "Cloudflare token file permissions verified." - name: Create lego directories ansible.builtin.file: path: "{{ item }}" state: directory owner: root group: root mode: "0700" loop: - "{{ elk_lego_path }}" - "{{ elk_lego_path }}/accounts" - name: Create lego certificates directory (traversable by container users) ansible.builtin.file: path: "{{ elk_lego_certs_dir }}" state: directory owner: root group: root mode: "0755" - name: Check installed lego version (if any) ansible.builtin.command: "{{ elk_lego_install_dir }}/lego --version" register: elk_lego_installed_version changed_when: false failed_when: false - name: Download and install lego binary when: > elk_lego_installed_version.rc != 0 or elk_lego_version not in (elk_lego_installed_version.stdout | default('')) block: - name: Download lego release tarball ansible.builtin.get_url: url: "https://github.com/go-acme/lego/releases/download/v{{ elk_lego_version }}/lego_v{{ elk_lego_version }}_linux_{{ elk_lego_arch }}.tar.gz" dest: "/tmp/lego_{{ elk_lego_version }}.tar.gz" mode: "0644" - name: Extract lego binary ansible.builtin.unarchive: src: "/tmp/lego_{{ elk_lego_version }}.tar.gz" dest: /tmp remote_src: true include: - lego - name: Install lego binary ansible.builtin.copy: src: /tmp/lego dest: "{{ elk_lego_install_dir }}/lego" owner: root group: root mode: "0755" remote_src: true - name: Clean up downloaded artifacts ansible.builtin.file: path: "{{ item }}" state: absent loop: - "/tmp/lego_{{ elk_lego_version }}.tar.gz" - /tmp/lego - name: Check whether a certificate already exists for this domain ansible.builtin.stat: path: "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.crt" register: elk_cert_stat # First issuance uses `lego ... run`, which is what actually requests a new # certificate; `renew` (used by the systemd timer below) refuses to run # against a domain with no existing certificate. - name: Issue the initial certificate (first run only) ansible.builtin.shell: | set -o pipefail export $(cat "{{ elk_cloudflare_token_env_file}}" | xargs) && \ {{ elk_lego_install_dir }}/lego \ --accept-tos \ --email "{{ elk_tls_acme_email }}" \ --server "{{ elk_tls_acme_server }}" \ --domains "{{ elk_tls_domain }}" \ --dns cloudflare \ --dns.resolvers "1.1.1.1:53,8.8.8.8:53" \ --path "{{ elk_lego_path }}" \ run args: executable: /bin/bash when: not elk_cert_stat.stat.exists register: elk_lego_first_issue notify: "restart elk stack" - name: Ensure issued cert/key are readable by the containers (root-owned, group-readable) ansible.builtin.file: path: "{{ item }}" owner: root group: root mode: "0644" loop: - "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.crt" - "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.issuer.crt" when: elk_cert_stat.stat.exists or elk_lego_first_issue is not skipped - name: Private key stays root-only-readable but group-readable for the container runtime ansible.builtin.file: path: "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.key" owner: root group: root mode: "0640" when: elk_cert_stat.stat.exists or elk_lego_first_issue is not skipped # --------------------------------------------------------------------------- # Ongoing renewal: systemd oneshot service + daily timer. lego's `renew` # subcommand only actually re-issues (and only then fires --run-hook) when # the certificate is within elk_tls_renew_days of expiring, so this is safe # to run daily without causing unnecessary container restarts. # --------------------------------------------------------------------------- - name: Template lego renewal systemd service ansible.builtin.template: src: lego-renew.service.j2 dest: /etc/systemd/system/lego-renew.service owner: root group: root mode: "0644" register: elk_lego_service_unit - name: Template lego renewal systemd timer ansible.builtin.template: src: lego-renew.timer.j2 dest: /etc/systemd/system/lego-renew.timer owner: root group: root mode: "0644" register: elk_lego_timer_unit - name: Reload systemd if units changed ansible.builtin.systemd: daemon_reload: true when: elk_lego_service_unit is changed or elk_lego_timer_unit is changed - name: Enable and start the lego renewal timer ansible.builtin.systemd: name: lego-renew.timer enabled: true state: started