--- # ----------------------------------------------------------------------------- # Certbot Deployment Playbook # ----------------------------------------------------------------------------- # Deploys certbot with Namecheap DNS-01 validation for wildcard certificates # Host: hippocamp.helu.ca (OCI HAProxy instance) # # Secrets are fetched automatically from OCI Vault via group_vars/all/secrets.yml # ----------------------------------------------------------------------------- - name: Deploy Certbot with Namecheap DNS-01 Validation hosts: ubuntu vars: ansible_common_remote_group: "{{ certbot_group | default(omit) }}" allow_world_readable_tmpfiles: true tags: [certbot, ssl, deploy] handlers: - name: restart certbot-renew timer become: true ansible.builtin.systemd: name: certbot-renew.timer state: restarted daemon_reload: true tasks: - name: Check if host has certbot service ansible.builtin.set_fact: has_certbot_service: "{{ 'certbot' in services | default([]) }}" - name: Skip hosts without certbot service ansible.builtin.meta: end_host when: not has_certbot_service # ------------------------------------------------------------------------- # System Setup # ------------------------------------------------------------------------- - name: Create certbot group become: true ansible.builtin.group: name: "{{ certbot_group }}" system: true - name: Create certbot user become: true ansible.builtin.user: name: "{{ certbot_user }}" comment: "Certbot SSL Certificate Management" group: "{{ certbot_group }}" system: true shell: /usr/sbin/nologin home: "{{ certbot_directory }}" create_home: false - name: Add ansible user to certbot group become: true ansible.builtin.user: name: "{{ ansible_user }}" groups: "{{ certbot_group }}" append: true # ------------------------------------------------------------------------- # Directory Structure # ------------------------------------------------------------------------- - name: Create certbot directories become: true ansible.builtin.file: path: "{{ item }}" owner: "{{ certbot_user }}" group: "{{ certbot_group }}" state: directory mode: '0750' loop: - "{{ certbot_directory }}" - "{{ certbot_directory }}/config" - "{{ certbot_directory }}/work" - "{{ certbot_directory }}/logs" - "{{ certbot_directory }}/credentials" - "{{ certbot_directory }}/hooks" - name: Create haproxy group for certificate directory become: true ansible.builtin.group: name: "{{ haproxy_group | default('haproxy') }}" system: true - name: Create haproxy user for certificate directory become: true ansible.builtin.user: name: "{{ haproxy_user | default('haproxy') }}" comment: "HAProxy Load Balancer" group: "{{ haproxy_group | default('haproxy') }}" system: true shell: /usr/sbin/nologin home: /nonexistent create_home: false - name: Create certificate output directory become: true ansible.builtin.file: path: /etc/haproxy/certs owner: "{{ certbot_user }}" group: "{{ haproxy_group | default('haproxy') }}" state: directory mode: '0750' # ------------------------------------------------------------------------- # Python Virtual Environment # ------------------------------------------------------------------------- - name: Install Python venv package become: true ansible.builtin.apt: name: - python3-venv - python3-pip state: present update_cache: true - name: Create virtual environment become: true become_user: "{{ certbot_user }}" ansible.builtin.command: python3 -m venv {{ certbot_directory }}/.venv args: creates: "{{ certbot_directory }}/.venv/bin/activate" vars: ansible_common_remote_group: "{{ certbot_group }}" allow_world_readable_tmpfiles: true - name: Upgrade pip in virtualenv become: true become_user: "{{ certbot_user }}" ansible.builtin.pip: name: pip state: latest virtualenv: "{{ certbot_directory }}/.venv" vars: ansible_common_remote_group: "{{ certbot_group }}" allow_world_readable_tmpfiles: true - name: Install certbot and Namecheap DNS plugin become: true become_user: "{{ certbot_user }}" ansible.builtin.pip: name: - certbot - certbot-dns-namecheap state: present virtualenv: "{{ certbot_directory }}/.venv" vars: ansible_common_remote_group: "{{ certbot_group }}" allow_world_readable_tmpfiles: true # ------------------------------------------------------------------------- # Namecheap Credentials # ------------------------------------------------------------------------- - name: Get public IP for Namecheap API ansible.builtin.uri: url: https://ifconfig.me/ip return_content: true register: public_ip_result delegate_to: localhost become: false - name: Set client IP fact ansible.builtin.set_fact: namecheap_client_ip: "{{ public_ip_result.content | trim }}" - name: Template Namecheap credentials become: true ansible.builtin.template: src: namecheap.ini.j2 dest: "{{ certbot_directory }}/credentials/namecheap.ini" owner: "{{ certbot_user }}" group: "{{ certbot_group }}" mode: '0600' # ------------------------------------------------------------------------- # Renewal Hooks # ------------------------------------------------------------------------- - name: Template renewal hook script become: true ansible.builtin.template: src: renewal-hook.sh.j2 dest: "{{ certbot_directory }}/hooks/renewal-hook.sh" owner: "{{ certbot_user }}" group: "{{ certbot_group }}" mode: '0750' - name: Template certificate metrics script become: true ansible.builtin.template: src: cert-metrics.sh.j2 dest: "{{ certbot_directory }}/hooks/cert-metrics.sh" owner: "{{ certbot_user }}" group: "{{ certbot_group }}" mode: '0750' # ------------------------------------------------------------------------- # Initial Certificate Request # ------------------------------------------------------------------------- - name: Check if certificate already exists become: true ansible.builtin.stat: path: "{{ certbot_directory }}/config/live/{{ certbot_cert_name }}/fullchain.pem" register: cert_exists - name: Build domain arguments for certbot ansible.builtin.set_fact: certbot_domain_args: "{{ certbot_domains | map('regex_replace', '^', '-d ') | join(' ') }}" - name: Request initial certificate become: true become_user: "{{ certbot_user }}" ansible.builtin.shell: | source {{ certbot_directory }}/.venv/bin/activate certbot certonly \ --non-interactive \ --agree-tos \ --email {{ certbot_email }} \ --authenticator dns-namecheap \ --dns-namecheap-credentials {{ certbot_directory }}/credentials/namecheap.ini \ --dns-namecheap-propagation-seconds 120 \ --config-dir {{ certbot_directory }}/config \ --work-dir {{ certbot_directory }}/work \ --logs-dir {{ certbot_directory }}/logs \ --cert-name {{ certbot_cert_name }} \ {{ certbot_domain_args }} args: executable: /bin/bash when: not cert_exists.stat.exists register: certbot_request - name: Run renewal hook after initial certificate become: true ansible.builtin.command: "{{ certbot_directory }}/hooks/renewal-hook.sh" when: certbot_request.changed # ------------------------------------------------------------------------- # Systemd Timer for Auto-Renewal # ------------------------------------------------------------------------- - name: Create certbot renewal service become: true ansible.builtin.copy: content: | [Unit] Description=Certbot Renewal After=network-online.target Wants=network-online.target [Service] Type=oneshot User={{ certbot_user }} Group={{ certbot_group }} ExecStart=/bin/bash -c 'source {{ certbot_directory }}/.venv/bin/activate && certbot renew --config-dir {{ certbot_directory }}/config --work-dir {{ certbot_directory }}/work --logs-dir {{ certbot_directory }}/logs --deploy-hook {{ certbot_directory }}/hooks/renewal-hook.sh' PrivateTmp=true dest: /etc/systemd/system/certbot-renew.service mode: '0644' notify: restart certbot-renew timer - name: Create certbot renewal timer become: true ansible.builtin.copy: content: | [Unit] Description=Run certbot renewal twice daily [Timer] OnCalendar=*-*-* 00,12:00:00 RandomizedDelaySec=3600 Persistent=true [Install] WantedBy=timers.target dest: /etc/systemd/system/certbot-renew.timer mode: '0644' notify: restart certbot-renew timer - name: Enable and start certbot renewal timer become: true ansible.builtin.systemd: name: certbot-renew.timer enabled: true state: started daemon_reload: true # ------------------------------------------------------------------------- # Initial Metrics Update # ------------------------------------------------------------------------- - name: Ensure prometheus textfile directory exists become: true ansible.builtin.file: path: "{{ prometheus_node_exporter_text_directory }}" state: directory owner: prometheus group: prometheus mode: '0755' - name: Run certificate metrics script become: true ansible.builtin.command: "{{ certbot_directory }}/hooks/cert-metrics.sh" changed_when: false # ------------------------------------------------------------------------- # Verification # ------------------------------------------------------------------------- - name: Verify certificate exists become: true ansible.builtin.stat: path: "{{ haproxy_cert_path }}" register: final_cert - name: Certificate deployment status ansible.builtin.debug: msg: "Certificate deployed: {{ final_cert.stat.exists }}"