- name: install certbot include_tasks: tasks/install_packages.yml vars: package: certbot - name: create certbot directories file: path: "{{ item }}" state: directory loop: - "{{ acme_directory }}" - "{{ acme_directory }}/cron" - name: change certbot directory permissions file: path: "{{ acme_directory ~ '/' ~ item }}" state: directory mode: "g+rx,o+rx" loop: - archive - live - name: check if acme-dns auth hook already exists stat: path: "{{ acme_directory }}/acme-dns-auth.py" register: result - name: download acme-dns auth hook get_url: url: "https://raw.githubusercontent.com/RangeForce/acme-dns-certbot-joohoi/master/acme-dns-auth.py" dest: "{{ acme_directory }}/acme-dns-auth.py" force: no mode: "+x" when: result.stat.exists == false - name: update python interpreter in acme-dns-auth to python3 lineinfile: path: "{{ acme_directory }}/acme-dns-auth.py" regexp: '^#!\/usr\/bin\/env python\s*$' line: '#!/usr/bin/env python3' - name: clear acme fqdn list set_fact: acme_domain_list: "{{ [] }}" - name: build acme fqdn list set_fact: acme_domain_list: "{{ (acme_domain_list | d([])) + ([item.fqdn | d((item.hostname | d(host_name)) ~ '.' ~ (item.tld | d(host_tld)))] if item is mapping else [item]) }}" loop: "{{ acme_hosts if (acme_hosts | type_debug == 'list') else [] }}" - name: build single acme fqdn set_fact: acme_domain_list: "{%- if acme_fqdn is defined and acme_fqdn != None -%}\ {{ [ acme_fqdn ] }}\ {%- elif (acme_hostname is defined and acme_hostname != None) or (acme_tld is defined and acme_tld != None) -%}\ {{ [((acme_hostname is defined and acme_hostname != None) | ternary(acme_hostname, host_name)) ~ '.' ~ ((acme_tld is defined and acme_tld != None) | ternary(acme_tld, host_tld))] }}\ {%- else -%}\ {{ [ host_fqdn ] }}\ {%- endif -%}" when: (acme_hosts is not defined) or (acme_hosts | type_debug != 'list') - name: set acme parameters set_fact: acme_cert_name: "{{ acme_id if (acme_id is defined) and (acme_id != None) else (host_name ~ ('-ecc' if (acme_ecc | d(false) == true) else '')) }}" acme_target_server: "{%- if (acme_server is defined) and (acme_server != None) -%}\ {{ acme_server }}\ {%- else -%}\ {{ (services.acme_dns.protocol | d('https')) ~ '://' ~ services.acme_dns.hostname ~ '.' ~ (services.acme_dns.tld | d(int_tld)) ~ ((':' ~ services.acme_dns.port) if services.acme_dns.port is defined else '') }}\ {%- endif -%}" - name: set certbot parameters set_fact: acme_params: "{{ ['--manual', '--manual-auth-hook ' ~ ((acme_directory ~ '/acme-dns-auth.py') | quote), '--preferred-challenges dns', '--debug-challenges', ('--key-type ecdsa' if (acme_ecc | d(false) == true) else ''), ('--staging' if (acme_staging | d(false) == true) else ''), ('--force-renewal' if (acme_force | d(false) == true) else ''), ('--must-staple' if (acme_stapling | d(false) == true) else ''), '--cert-name ' ~ (acme_cert_name | quote), '--non-interactive', '--agree-tos', '--email ' ~ ((acme_email | d(maintainer_email)) | quote), '--no-eff-email', (('--preferred-chain ' ~ (acme_preferred_chain | quote)) if acme_preferred_chain is defined else ''), '--max-log-backups ' ~ (acme_max_log_backups | quote) ] | select() | list | join(' ') }}" - block: - name: issue cert with dns mode shell: cmd: "certbot certonly {{ acme_params }} -d {{ acme_domain_list | map('quote') | join(' -d ') }}" chdir: /usr/bin environment: ACMEDNS_URL: "{{ acme_target_server }}" register: result changed_when: ('Successfully received certificate' in result.stdout) notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}" rescue: - name: wait for user interaction (CNAME record must be set manually) pause: prompt: "{{ result.stdout }}" - name: try again to issue cert with dns mode shell: cmd: "certbot certonly {{ acme_params }} -d {{ acme_domain_list | map('quote') | join(' -d ') }}" chdir: /usr/bin environment: ACMEDNS_URL: "{{ acme_target_server }}" register: result changed_when: ('Successfully received certificate' in result.stdout) notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}" - name: create symlinks file: path: "{{ item.dest }}" src: "{{ acme_directory ~ '/live/' ~ acme_cert_name ~ '/' ~ item.src }}" state: link force: yes when: (item.dest is string) and (item.dest | length > 0) and (acme_use_symlinks | d(true) == true) loop: - { src: 'fullchain.pem', dest: "{{ acme_cert | d(None) }}" } - { src: 'privkey.pem', dest: "{{ acme_key | d(None) }}" } - { src: 'cert.pem', dest: "{{ acme_cert_single | d(None) }}" } - { src: 'chain.pem', dest: "{{ acme_chain | d(None) }}" } notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}" - name: fix ownership on archive dir file: path: "{{ acme_directory ~ '/archive/' ~ acme_cert_name }}" follow: no recurse: yes owner: "{{ acme_owner if (acme_owner is defined) and (acme_owner != None) else omit }}" group: "{{ acme_group if (acme_group is defined) and (acme_group != None) else omit }}" - name: copy certs copy: src: "{{ acme_directory ~ '/live/' ~ acme_cert_name ~ '/' ~ item.src }}" dest: "{{ item.dest }}" remote_src: yes mode: 0600 owner: "{{ acme_owner if (acme_owner is defined) and (acme_owner != None) else omit }}" group: "{{ acme_group if (acme_group is defined) and (acme_group != None) else omit }}" when: (item.dest is string) and (item.dest | length > 0) and (acme_use_symlinks | d(true) == false) loop: - { src: 'fullchain.pem', dest: "{{ acme_cert | d(None) }}" } - { src: 'privkey.pem', dest: "{{ acme_key | d(None) }}" } - { src: 'cert.pem', dest: "{{ acme_cert_single | d(None) }}" } - { src: 'chain.pem', dest: "{{ acme_chain | d(None) }}" } notify: "{{ acme_notify | d(omit) }}" - name: edit renewal file lineinfile: path: "{{ acme_directory ~ '/renewal/' ~ acme_cert_name ~ '.conf' }}" regexp: '^{{ item.name | regex_escape }}(\s+)=' line: '{{ item.name }} = {{ item.value }}' insertafter: '^\[renewalparams\]' create: no firstmatch: yes when: (item.value is string) and (item.value | length > 0) and ((item.extra_condition is not defined) or (item.extra_condition | d(true))) loop: - { name: 'renew_hook', value: "{{ acme_directory ~ '/cron/' ~ acme_cert_name ~ '.sh' }}" } - name: create custom renewal hook file template: src: renewal.j2 dest: "{{ acme_directory ~ '/cron/' ~ acme_cert_name ~ '.sh' }}" force: yes mode: 0500 lstrip_blocks: yes - name: add certbot to crontab cron: name: "certbot renewal ({{ acme_cert_name ~ ' on ' ~ acme_target_server }})" job: "ACMEDNS_URL={{ acme_target_server | quote }} \ /usr/bin/certbot renew --cert-name {{ acme_cert_name | quote }} --max-log-backups {{ acme_max_log_backups | quote }}" hour: "{{ 4 | random(start=1, seed=(host_name ~ acme_cert_name)) }}" minute: "{{ 59 | random(seed=(host_name ~ acme_cert_name)) }}"