From b3d8e46b3d8fb69f76df99dc6a80872ada209784 Mon Sep 17 00:00:00 2001
From: Lukas Koenen <lukas.koenen@h-da.de>
Date: Thu, 20 Apr 2023 18:27:14 +0200
Subject: [PATCH] feat: dns challenge

---
 README.md                                     | 16 ++-
 defaults/main.yml                             | 15 ++-
 ...ages.yml => CentOS-packages-webserver.yml} |  0
 ...S-timer.yml => CentOS-timer-webserver.yml} |  0
 ...ages.yml => Debian-packages-webserver.yml} |  0
 ...n-timer.yml => Debian-timer-webserver.yml} |  0
 ...ages.yml => Fedora-packages-webserver.yml} |  0
 ...a-timer.yml => Fedora-timer-webserver.yml} |  0
 ...ages.yml => Ubuntu-packages-webserver.yml} |  0
 ...u-timer.yml => Ubuntu-timer-webserver.yml} |  0
 tasks/dns-challenge.yml                       | 99 +++++++++++++++++++
 tasks/main.yml                                | 62 ++----------
 tasks/webserver.yml                           | 54 ++++++++++
 templates/dns-challenge.env.j2                |  5 +
 templates/dns-challenge.service.j2            | 12 +++
 templates/dns-challenge.timer.j2              | 13 +++
 vars/main.yml                                 |  6 +-
 17 files changed, 220 insertions(+), 62 deletions(-)
 rename tasks/{CentOS-packages.yml => CentOS-packages-webserver.yml} (100%)
 rename tasks/{CentOS-timer.yml => CentOS-timer-webserver.yml} (100%)
 rename tasks/{Debian-packages.yml => Debian-packages-webserver.yml} (100%)
 rename tasks/{Debian-timer.yml => Debian-timer-webserver.yml} (100%)
 rename tasks/{Fedora-packages.yml => Fedora-packages-webserver.yml} (100%)
 rename tasks/{Fedora-timer.yml => Fedora-timer-webserver.yml} (100%)
 rename tasks/{Ubuntu-packages.yml => Ubuntu-packages-webserver.yml} (100%)
 rename tasks/{Ubuntu-timer.yml => Ubuntu-timer-webserver.yml} (100%)
 create mode 100644 tasks/dns-challenge.yml
 create mode 100644 tasks/webserver.yml
 create mode 100644 templates/dns-challenge.env.j2
 create mode 100644 templates/dns-challenge.service.j2
 create mode 100644 templates/dns-challenge.timer.j2

diff --git a/README.md b/README.md
index db51b01..2d91e95 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,26 @@ Role Variables
 --------------
 
 ```yaml
+---
 # certbot settings
+certbot_dns_challenge: false                                                          # default use webserver, true to obtain certificate using dns challenge
+certbot_lego_version: "4.11.0"                                                        # version of LEGO client
+certbot_dns_provider: "designate"                                                     # list of available providers https://go-acme.github.io/lego/dns/
+dns_provider_auth_env_variables:                                                      # variables required to authenticate dns provider
+  OS_AUTH_URL: "https://openstack.example.org"
+  OS_REGION_NAME: "RegionOne"
+  OS_AUTH_TYP: "v3applicationcredential"                                              # default is to use application credential rather than password
+  OS_APPLICATION_CREDENTIAL_ID: "{{ vault_os_application_credential_id }}"
+  OS_APPLICATION_CREDENTIAL_SECRET: "{{ vault_os_application_credential_secret }}"
+
+
 certbot_fqdn:
   - example.de
 certbot_admin_email: "admin@example.de"
-# certbot_webroot: "/var/www/example"                          # if undefined use --standalone
+# certbot_webroot: "/var/www/example"                                                 # if undefined use --standalone
 
 # application settings
-# certbot_application: "example"                               # if defined copy certs to application dir and setup deploy hooks
+# certbot_application: "example"                                                      # if defined copy certs to application dir and setup deploy hooks
 certbot_application_dir: "/etc/{{ certbot_application }}"
 certbot_application_deploy_hook: |
   #!/bin/sh
diff --git a/defaults/main.yml b/defaults/main.yml
index 0daf2e5..43d75cc 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -1,12 +1,23 @@
 ---
 # certbot settings
+certbot_dns_challenge: false                                                          # default use webserver, true to obtain certificate using dns challenge
+certbot_lego_version: "4.11.0"                                                        # lego version - Let's Encrypt client
+certbot_dns_provider: "designate"                                                     # list of available providers https://go-acme.github.io/lego/dns/
+dns_provider_auth_env_variables:                                                      # variables required to authenticate dns provider
+  OS_AUTH_URL: "https://openstack.example.org"
+  OS_REGION_NAME: "RegionOne"
+  OS_AUTH_TYP: "v3applicationcredential"                                              # default is to use application credential rather than password
+  OS_APPLICATION_CREDENTIAL_ID: "{{ vault_os_application_credential_id }}"
+  OS_APPLICATION_CREDENTIAL_SECRET: "{{ vault_os_application_credential_secret }}"
+
+
 certbot_fqdn:
   - example.de
 certbot_admin_email: "admin@example.de"
-# certbot_webroot: "/var/www/example"                          # if undefined use --standalone
+# certbot_webroot: "/var/www/example"                                                 # if undefined use --standalone
 
 # application settings
-# certbot_application: "example"                               # if defined copy certs to application dir and setup deploy hooks
+# certbot_application: "example"                                                      # if defined copy certs to application dir and setup deploy hooks
 certbot_application_dir: "/etc/{{ certbot_application }}"
 certbot_application_deploy_hook: |
   #!/bin/sh
diff --git a/tasks/CentOS-packages.yml b/tasks/CentOS-packages-webserver.yml
similarity index 100%
rename from tasks/CentOS-packages.yml
rename to tasks/CentOS-packages-webserver.yml
diff --git a/tasks/CentOS-timer.yml b/tasks/CentOS-timer-webserver.yml
similarity index 100%
rename from tasks/CentOS-timer.yml
rename to tasks/CentOS-timer-webserver.yml
diff --git a/tasks/Debian-packages.yml b/tasks/Debian-packages-webserver.yml
similarity index 100%
rename from tasks/Debian-packages.yml
rename to tasks/Debian-packages-webserver.yml
diff --git a/tasks/Debian-timer.yml b/tasks/Debian-timer-webserver.yml
similarity index 100%
rename from tasks/Debian-timer.yml
rename to tasks/Debian-timer-webserver.yml
diff --git a/tasks/Fedora-packages.yml b/tasks/Fedora-packages-webserver.yml
similarity index 100%
rename from tasks/Fedora-packages.yml
rename to tasks/Fedora-packages-webserver.yml
diff --git a/tasks/Fedora-timer.yml b/tasks/Fedora-timer-webserver.yml
similarity index 100%
rename from tasks/Fedora-timer.yml
rename to tasks/Fedora-timer-webserver.yml
diff --git a/tasks/Ubuntu-packages.yml b/tasks/Ubuntu-packages-webserver.yml
similarity index 100%
rename from tasks/Ubuntu-packages.yml
rename to tasks/Ubuntu-packages-webserver.yml
diff --git a/tasks/Ubuntu-timer.yml b/tasks/Ubuntu-timer-webserver.yml
similarity index 100%
rename from tasks/Ubuntu-timer.yml
rename to tasks/Ubuntu-timer-webserver.yml
diff --git a/tasks/dns-challenge.yml b/tasks/dns-challenge.yml
new file mode 100644
index 0000000..ff0cb99
--- /dev/null
+++ b/tasks/dns-challenge.yml
@@ -0,0 +1,99 @@
+- name: Fetch Binary
+  ansible.builtin.get_url:
+    url: "https://github.com/go-acme/lego/releases/download/v{{ certbot_lego_version }}/lego_v{{ certbot_lego_version }}_linux_amd64.tar.gz"
+    dest: "/tmp/lego_v{{ certbot_lego_version }}_linux_amd64.tar.gz"
+    mode: "0644"
+
+- name: Unpack Archive
+  ansible.builtin.unarchive:
+    src: "/tmp/lego_v{{ certbot_lego_version }}_linux_amd64.tar.gz"
+    dest: "/tmp"
+    remote_src: true
+
+- name: Copy Binary
+  ansible.builtin.copy:
+    src: /tmp/lego
+    dest: /usr/bin/lego
+    owner: root
+    group: root
+    mode: '0755'
+    remote_src: true
+  become: true
+
+- name: Check Whether Cert Exists
+  ansible.builtin.stat:
+    path: "{{ certbot_live_dir }}/fullchain.pem"
+  register: lecert
+  become: true
+
+- name: Ensure Deploy Hook Dir Exists
+  ansible.builtin.file:
+    path: "{{ item }}"
+    state: directory
+    mode: "0755"
+    owner: root
+    group: root
+  loop:
+    - /etc/letsencrypt/renewal-hooks
+    - /etc/letsencrypt/renewal-hooks/deploy
+  become: true
+
+- name: Request Cert If Necessary - DNS Challenge
+  block:
+    - name: Request Cert
+      ansible.builtin.command: >-
+        lego -a --dns {{ certbot_dns_provider }}
+        --email {{ certbot_admin_email }} -d {{ lego_dflag }}
+        --path {{ certbot_live_dir }}
+        run
+      environment: "{{ dns_provider_auth_env_variables }}"
+      register: lego
+      changed_when: lego.rc == 0
+
+    - name: Mirror Letsencrypt Structure
+      ansible.builtin.copy:
+        src: "{{ item.src }}"
+        dest: "{{ item.dest }}"
+        owner: root
+        group: root
+        mode: '0600'
+        remote_src: true
+      loop:
+        - { src: "{{ certbot_live_dir }}/certificates/{{ certbot_fqdn_first }}.crt", dest: "{{ certbot_live_dir }}/fullchain.pem" }
+        - { src: "{{ certbot_live_dir }}/certificates/{{ certbot_fqdn_first }}.key", dest: "{{ certbot_live_dir }}/privkey.pem" }
+
+  when: not lecert.stat.exists
+  become: true
+
+- name: Render Systemd Files
+  block:
+    - name: Render Systemd Environment File
+      ansible.builtin.template:
+        src: templates/dns-challenge.env.j2
+        dest: /etc/default/dns-challenge.env
+        mode: "0644"
+
+    - name: Render Systemd Service File
+      ansible.builtin.template:
+        src: templates/dns-challenge.service.j2
+        dest: /lib/systemd/system/dns-challenge.service
+        mode: "0644"
+
+    - name: Render Systemd Timer File
+      ansible.builtin.template:
+        src: templates/dns-challenge.timer.j2
+        dest: /lib/systemd/system/dns-challenge.timer
+        mode: "0644"
+  
+  become: true
+
+- name: Setup Certbot With Application
+  ansible.builtin.include_tasks: "application.yml"
+  when: certbot_application is defined
+
+- name: Enable LEGO Renew Timer
+  ansible.builtin.systemd:
+    name: dns-challenge.timer
+    state: started
+    enabled: true
+  become: true
diff --git a/tasks/main.yml b/tasks/main.yml
index 3cc640a..a97d708 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -1,58 +1,8 @@
 ---
-# TODO:
-#   - dns challenge
+- name: Obtain Cert Using Web Server
+  ansible.builtin.include_tasks: "webserver.yml"
+  when: not certbot_dns_challenge
 
-- name: Install Packages Based On Distribution
-  ansible.builtin.include_tasks: "{{ ansible_facts.distribution }}-packages.yml"
-
-- name: Check Whether Cert Exists
-  ansible.builtin.stat:
-    path: "{{ certbot_live_dir }}/cert.pem"
-  register: lecert
-  become: true
-
-- name: Ensure Deploy Hook Dir Exists
-  ansible.builtin.file:
-    path: "{{ item }}"
-    state: directory
-    mode: "0755"
-    owner: root
-    group: root
-  loop:
-    - /etc/letsencrypt/renewal-hooks
-    - /etc/letsencrypt/renewal-hooks/deploy
-  become: true
-
-- name: Request Cert If Necessary - Standalone
-  ansible.builtin.command: >-
-    certbot certonly --standalone --noninteractive --agree-tos
-    --email {{ certbot_admin_email }} -d {{ certbot_dflag }}
-  when: not lecert.stat.exists and not certbot_webroot is defined
-  register: cbstandalone
-  changed_when: cbstandalone.rc == 0
-  become: true
-
-- name: Ensure Webroot Path Exists
-  ansible.builtin.file:
-    path: "{{ certbot_webroot }}"
-    state: directory
-    mode: "0755"
-    owner: root
-    group: root
-  when: not lecert.stat.exists and certbot_webroot is defined
-
-- name: Request Cert If Necessary - Webroot
-  ansible.builtin.command: >-
-    certbot certonly --webroot --webroot-path {{ certbot_webroot }} --noninteractive --agree-tos
-    --email {{ certbot_admin_email }} -d {{ certbot_dflag }}
-  when: not lecert.stat.exists and certbot_webroot is defined
-  register: cbwebroot
-  changed_when: cbwebroot.rc == 0
-  become: true
-
-- name: Setup Certbot With Application
-  ansible.builtin.include_tasks: "application.yml"
-  when: certbot_application is defined
-
-- name: Enable Letsencrypt Renew Timer Based On Distribution
-  ansible.builtin.include_tasks: "{{ ansible_facts.distribution }}-timer.yml"
+- name: Obtain Cert DNS Challenge
+  ansible.builtin.include_tasks: "dns-challenge.yml"
+  when: certbot_dns_challenge
diff --git a/tasks/webserver.yml b/tasks/webserver.yml
new file mode 100644
index 0000000..96e6074
--- /dev/null
+++ b/tasks/webserver.yml
@@ -0,0 +1,54 @@
+- name: Install Packages Based On Distribution
+  ansible.builtin.include_tasks: "{{ ansible_facts.distribution }}-packages-webserver.yml"
+
+- name: Check Whether Cert Exists
+  ansible.builtin.stat:
+    path: "{{ certbot_live_dir }}/cert.pem"
+  register: lecert
+  become: true
+
+- name: Ensure Deploy Hook Dir Exists
+  ansible.builtin.file:
+    path: "{{ item }}"
+    state: directory
+    mode: "0755"
+    owner: root
+    group: root
+  loop:
+    - /etc/letsencrypt/renewal-hooks
+    - /etc/letsencrypt/renewal-hooks/deploy
+  become: true
+
+- name: Request Cert If Necessary - Standalone
+  ansible.builtin.command: >-
+    certbot certonly --standalone --noninteractive --agree-tos
+    --email {{ certbot_admin_email }} -d {{ certbot_dflag }}
+  when: not lecert.stat.exists and not certbot_webroot is defined
+  register: cbstandalone
+  changed_when: cbstandalone.rc == 0
+  become: true
+
+- name: Ensure Webroot Path Exists
+  ansible.builtin.file:
+    path: "{{ certbot_webroot }}"
+    state: directory
+    mode: "0755"
+    owner: root
+    group: root
+  when: not lecert.stat.exists and certbot_webroot is defined
+
+- name: Request Cert If Necessary - Webroot
+  ansible.builtin.command: >-
+    certbot certonly --webroot --webroot-path {{ certbot_webroot }} --noninteractive --agree-tos
+    --email {{ certbot_admin_email }} -d {{ certbot_dflag }}
+  when: not lecert.stat.exists and certbot_webroot is defined
+  register: cbwebroot
+  changed_when: cbwebroot.rc == 0
+  become: true
+
+- name: Setup Certbot With Application
+  ansible.builtin.include_tasks: "application.yml"
+  when: certbot_application is defined
+
+- name: Enable Letsencrypt Renew Timer Based On Distribution
+  ansible.builtin.include_tasks: "{{ ansible_facts.distribution }}-timer-webserver.yml"
diff --git a/templates/dns-challenge.env.j2 b/templates/dns-challenge.env.j2
new file mode 100644
index 0000000..efe3629
--- /dev/null
+++ b/templates/dns-challenge.env.j2
@@ -0,0 +1,5 @@
+# {{ ansible_managed }}
+
+{% for var, val in dns_provider_auth_env_variables.items() %}
+{{ var }}={{ val }}
+{% endfor %}
diff --git a/templates/dns-challenge.service.j2 b/templates/dns-challenge.service.j2
new file mode 100644
index 0000000..be5ca9e
--- /dev/null
+++ b/templates/dns-challenge.service.j2
@@ -0,0 +1,12 @@
+# {{ ansible_managed }}
+
+[Unit]
+Description=LEGO DNS challenge
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/lego -a --dns {{ certbot_dns_provider }} --email {{ certbot_admin_email }} -d {{ lego_dflag }} --path {{ certbot_live_dir }} renew
+ExecStartPost=cp {{ certbot_live_dir }}/certificates/{{ certbot_fqdn_first }}.crt {{ certbot_live_dir }}/fullchain.pem
+ExecStartPost=cp {{ certbot_live_dir }}/certificates/{{ certbot_fqdn_first }}.key {{ certbot_live_dir }}/privkey.pem
+{{ "ExecStartPost=/etc/letsencrypt/renewal-hooks/deploy/" + certbot_application if certbot_application is defined else "" }}
+EnvironmentFile=/etc/default/dns-challenge.env
diff --git a/templates/dns-challenge.timer.j2 b/templates/dns-challenge.timer.j2
new file mode 100644
index 0000000..382878a
--- /dev/null
+++ b/templates/dns-challenge.timer.j2
@@ -0,0 +1,13 @@
+# {{ ansible_managed }}
+
+[Unit]
+Description=Run LEGO DNS challenge twice every day
+
+[Timer]
+OnCalendar=*-*-* 00,12:00:00
+RandomizedDelaySec=43200
+Persistent=true
+Unit=dns-challenge.service
+
+[Install]
+WantedBy=timers.target
diff --git a/vars/main.yml b/vars/main.yml
index d12eca5..4a6a30f 100644
--- a/vars/main.yml
+++ b/vars/main.yml
@@ -1,9 +1,11 @@
 ---
-certbot_live_dir: >-
-  /etc/letsencrypt/live/{{
+certbot_fqdn_first: >-
+  {{
     certbot_fqdn
     | first
     | replace("*.", "")
     | trim(".")
   }}
+certbot_live_dir: "/etc/letsencrypt/live/{{ certbot_fqdn_first }}"
 certbot_dflag: "{{ certbot_fqdn | map('trim', '.') | join(',') }}"
+lego_dflag : "{{ certbot_fqdn | map('trim', '.') | join(' -d ') }}"
-- 
GitLab