diff --git a/playbooks/roles/cert/defaults/main.yml b/playbooks/roles/cert/defaults/main.yml new file mode 100644 index 0000000..bcff4d3 --- /dev/null +++ b/playbooks/roles/cert/defaults/main.yml @@ -0,0 +1,3 @@ +cert__handlers: [] +cert__owner: root +cert__group: root diff --git a/playbooks/roles/cert/meta/argument_specs.yml b/playbooks/roles/cert/meta/argument_specs.yml new file mode 100644 index 0000000..c79ff1a --- /dev/null +++ b/playbooks/roles/cert/meta/argument_specs.yml @@ -0,0 +1,46 @@ +--- +argument_specs: + main: + short_description: Orders and renews certificates from Let's Encrypt + options: + cert__domains: + description: Domains for which to issue a certificate. Must be in the same DNS zone. + required: true + type: list + elements: str + cert__owner: + description: Owner of the certificate files. + required: false + type: str + default: root + cert__group: + description: Group of the certificate files. + required: false + type: str + default: root + cert__acme_account: + description: ACME account details + required: true + type: dict + options: + email: + description: E-mail address to send certificate expiary notifications to + required: true + type: str + key: + description: Private RSA or Elliptic Curve key of the ACME account + required: true + type: str + cert__cloudflare_dns: + description: Cloudflare DNS API details + required: true + type: dict + options: + api_token: + description: Cloudflare API token + required: true + type: str + zone: + description: DNS zone the domain is in + required: true + type: str diff --git a/playbooks/roles/cert/meta/main.yml b/playbooks/roles/cert/meta/main.yml new file mode 100644 index 0000000..f730c82 --- /dev/null +++ b/playbooks/roles/cert/meta/main.yml @@ -0,0 +1,7 @@ +dependencies: # noqa meta-no-info + - role: distribution_check + vars: + distribution_check__supported_distributions: + - name: Debian + versions: + - "11" diff --git a/playbooks/roles/cert/tasks/deploy_cert.yml b/playbooks/roles/cert/tasks/deploy_cert.yml new file mode 100644 index 0000000..eeb5e30 --- /dev/null +++ b/playbooks/roles/cert/tasks/deploy_cert.yml @@ -0,0 +1,110 @@ +- name: Ensure certs directory exists + ansible.builtin.file: + path: /certs + state: directory + owner: root + group: root + mode: "755" +- name: Ensure sub-directory for the certificate exists + ansible.builtin.file: + path: "/certs/{{ item }}" + state: directory + owner: "{{ cert__owner }}" + group: "{{ cert__group }}" + mode: "755" +- name: Ensure private key is generated + community.crypto.openssl_privatekey: + path: "/certs/{{ item }}/key.pem" + size: 4096 + type: RSA + owner: "{{ cert__owner }}" + group: "{{ cert__group }}" + mode: "0600" +- name: Ensure certificate signing request is created + community.crypto.openssl_csr: + path: "/certs/{{ item }}/csr.pem" + privatekey_path: "/certs/{{ item }}/key.pem" + common_name: "{{ item }}" + owner: "{{ cert__owner }}" + group: "{{ cert__group }}" + mode: "0660" + register: cert__csr_result +- name: Check certificate status and create ACME challenge if needed + community.crypto.acme_certificate: + account_email: "{{ cert__acme_account.email }}" + account_key_content: "{{ cert__acme_account.key }}" + acme_directory: https://acme-v02.api.letsencrypt.org/directory + acme_version: 2 + remaining_days: 28 + terms_agreed: true + challenge: dns-01 + csr: "/certs/{{ item }}/csr.pem" + dest: "/certs/{{ item }}/cert.pem" + fullchain_dest: "/certs/{{ item }}/fullchain.pem" + register: cert__acme_challenge +- name: Retrieve certificate and fulfill challenge if needed # noqa no-handler + when: cert__acme_challenge.changed # Can't be put in a handler, because then the block "always" tasks won't be executed for some reason + block: + - name: Set DNS record via Cloudflare API to fulfill the challenge + when: not cert__acme_challenge.authorizations[item].status == "valid" + community.general.cloudflare_dns: + api_token: "{{ cert__cloudflare_dns.api_token }}" + zone: "{{ cert__cloudflare_dns.zone }}" + record: "{{ cert__acme_challenge.challenge_data[item]['dns-01'].record }}" + value: "{{ cert__acme_challenge.challenge_data[item]['dns-01'].resource_value }}" + type: TXT + ttl: 60 + solo: true + state: present + - name: Retrieve certificate + community.crypto.acme_certificate: + account_email: "{{ cert__acme_account.email }}" + account_key_content: "{{ cert__acme_account.key }}" + acme_directory: https://acme-v02.api.letsencrypt.org/directory + acme_version: 2 + terms_agreed: true + remaining_days: 28 + challenge: dns-01 + csr: "/certs/{{ item }}/csr.pem" + dest: "/certs/{{ item }}/cert.pem" + fullchain_dest: "/certs/{{ item }}/fullchain.pem" + data: "{{ cert__acme_challenge }}" + notify: "{{ cert__handlers }}" + always: + - name: Ensure DNS record is removed + when: not cert__acme_challenge.authorizations[item].status == "valid" + community.general.cloudflare_dns: + api_token: "{{ cert__cloudflare_dns.api_token }}" + zone: "{{ cert__cloudflare_dns.zone }}" + record: "{{ cert__acme_challenge.challenge_data[item]['dns-01'].record }}" + value: "{{ cert__acme_challenge.challenge_data[item]['dns-01'].resource_value }}" + type: TXT + ttl: 60 + state: absent +- name: Ensure correct permissions for certificate are set + ansible.builtin.file: + path: "/certs/{{ item }}/cert.pem" + owner: "{{ cert__owner }}" + group: "{{ cert__group }}" + mode: "0660" +- name: Ensure correct permissions for fullchain cert are set + ansible.builtin.file: + path: "/certs/{{ item }}/fullchain.pem" + owner: "{{ cert__owner }}" + group: "{{ cert__group }}" + mode: "0660" +- name: Get content of cert.pem + ansible.builtin.slurp: + src: "/certs/{{ item }}/cert.pem" + register: cert__cert_slurp +- name: Get content of fullchain.pem + ansible.builtin.slurp: + src: "/certs/{{ item }}/fullchain.pem" + register: cert__fullchain_slurp +- name: Ensure ca.pem is created + ansible.builtin.copy: + content: "{{ cert__fullchain_slurp.content | b64decode | replace(cert__cert_slurp.content | b64decode, '') }}" + dest: "/certs/{{ item }}/ca.pem" + owner: "{{ cert__owner }}" + group: "{{ cert__group }}" + mode: "0660" diff --git a/playbooks/roles/cert/tasks/main.yml b/playbooks/roles/cert/tasks/main.yml new file mode 100644 index 0000000..5141e00 --- /dev/null +++ b/playbooks/roles/cert/tasks/main.yml @@ -0,0 +1,3 @@ +- name: Deploy cert + ansible.builtin.include_tasks: deploy_cert.yml + loop: "{{ cert__domains }}"