From d02975289c2872d5578fe276e1410d59b8642ff1 Mon Sep 17 00:00:00 2001
From: Thomas Schneider <thomas@fsmpi.rwth-aachen.de>
Date: Fri, 8 Mar 2024 20:00:54 +0100
Subject: [PATCH] Add role for lego ACME client

---
 lego/defaults/main.yml                | 44 +++++++++++++++++++++++
 lego/handlers/main.yml                |  5 +++
 lego/tasks/certificate.yml            | 51 ++++++++++++++++++++++++++
 lego/tasks/main.yml                   | 41 +++++++++++++++++++++
 lego/templates/99-services.sh.j2      | 11 ++++++
 lego/templates/_macros.j2             | 52 +++++++++++++++++++++++++++
 lego/templates/env-inst.j2            | 24 +++++++++++++
 lego/templates/lego-renew@.service.j2 |  6 ++++
 lego/templates/lego-renew@.timer.j2   | 11 ++++++
 lego/templates/lego-run@.service.j2   |  6 ++++
 lego/vars/main.yml                    | 10 ++++++
 11 files changed, 261 insertions(+)
 create mode 100644 lego/defaults/main.yml
 create mode 100644 lego/handlers/main.yml
 create mode 100644 lego/tasks/certificate.yml
 create mode 100644 lego/tasks/main.yml
 create mode 100644 lego/templates/99-services.sh.j2
 create mode 100644 lego/templates/_macros.j2
 create mode 100644 lego/templates/env-inst.j2
 create mode 100644 lego/templates/lego-renew@.service.j2
 create mode 100644 lego/templates/lego-renew@.timer.j2
 create mode 100644 lego/templates/lego-run@.service.j2
 create mode 100644 lego/vars/main.yml

diff --git a/lego/defaults/main.yml b/lego/defaults/main.yml
new file mode 100644
index 0000000..f37785c
--- /dev/null
+++ b/lego/defaults/main.yml
@@ -0,0 +1,44 @@
+---
+
+lego_acme_server_base: acme-v02.api.letsencrypt.org
+
+# lego_account_mail: root@example.org
+
+# The following two variables are defaults for the respective entries in a
+# `lego_certificates` entry.
+lego_method:
+  type: http
+  subtype: webroot
+lego_hooks:
+  services: {}
+  extra: {}
+
+lego_global_args: []
+
+# lego_certificates:
+#   # Default case, only one domain
+#   foo.example.org: {}
+#   bar.example.org:
+#     domains:
+#       # bar.example.org is already included
+#       - baz.example.org
+#       - qux.example.org
+#     account_mail: other@example.org
+#     method:
+#       type: dns
+#       subtype: rfc2136
+#     extra_args:
+#       - --foo
+#       - --bar baz
+#     extra_env:
+#       RFC2136_NAMESERVER: 127.0.0.1
+#     hooks:
+#       services:
+#         nginx.service: try-restart
+#         httpd.service: try-reload-or-restart
+#       extra:
+#         10-install.sh: |
+#           #!/bin/sh
+#           set -e
+#           install -u foo -g foo -m 0644 "$LEGO_CERT_PATH" /etc/foo/cert.pem
+#           install -u foo -g foo -m 0600 "$LEGO_CERT_KEY_PATH" /etc/foo/key.pem
diff --git a/lego/handlers/main.yml b/lego/handlers/main.yml
new file mode 100644
index 0000000..4df3968
--- /dev/null
+++ b/lego/handlers/main.yml
@@ -0,0 +1,5 @@
+---
+
+- name: Reload systemd
+  systemd:
+    daemon_reload: true
diff --git a/lego/tasks/certificate.yml b/lego/tasks/certificate.yml
new file mode 100644
index 0000000..d580e2a
--- /dev/null
+++ b/lego/tasks/certificate.yml
@@ -0,0 +1,51 @@
+---
+
+- name: Install instance config
+  template:
+    src: env-inst.j2
+    dest: /etc/lego/{{ item.key }}.env
+    owner: root
+    group: root
+    mode: "0600"
+  register: install_instance_config
+
+- name: Create hooks directory
+  file:
+    path: /etc/lego/hooks/{{ item.key }}
+    state: directory
+    owner: root
+    group: root
+    mode: "0700"
+
+- name: Install service hook
+  template:
+    src: 99-services.sh.j2
+    dest: /etc/lego/hooks/{{ item.key }}/99-services
+    owner: root
+    group: root
+    mode: "0700"
+  when: item.value.hooks.services != {}
+
+- name: Install additional hooks
+  copy:
+    content: "{{ j.value }}"
+    dest: /etc/lego/hooks/{{ item.key }}/{{ j.key }}
+    owner: root
+    group: root
+    mode: "0700"
+  loop: "{{ item.value.hooks.extra | dict2items }}"
+  loop_control:
+    loop_var: j
+
+# Yes, this should be a handler, but they cannot be as dynamic as we need it.
+- name: Start lego
+  systemd:
+    name: lego-run@{{ item.key }}.service
+    state: started
+  when: install_instance_config.changed
+
+- name: Enable renewal timer
+  systemd:
+    name: lego-renew@{{ item.key }}.timer
+    state: started
+    enabled: true
diff --git a/lego/tasks/main.yml b/lego/tasks/main.yml
new file mode 100644
index 0000000..d810a4c
--- /dev/null
+++ b/lego/tasks/main.yml
@@ -0,0 +1,41 @@
+---
+
+- name: Install lego
+  apt:
+    name: lego
+
+- name: Create config and hooks directory
+  file:
+    path: "{{ item }}"
+    state: directory
+    owner: root
+    group: root
+    mode: "0700"
+  loop:
+    - /etc/lego
+    - /etc/lego/hooks
+
+- name: Install systemd service files
+  template:
+    src: "{{ item }}.j2"
+    dest: /etc/systemd/system/{{ item }}
+    owner: root
+    group: root
+    mode: "0644"
+  loop:
+    - lego-run@.service
+    - lego-renew@.service
+    - lego-renew@.timer
+  notify:
+    - Reload systemd
+
+- meta: flush_handlers
+
+- name: Process individual certificate instances
+  loop: "{{ lego_certificates | dict2items }}"
+  loop_control:
+    label: "{{ item.key }}"
+    loop_var: _item
+  vars:
+    item: "{{ cert_skeleton | combine(_item, recursive=True) }}"
+  include_tasks: certificate.yml
diff --git a/lego/templates/99-services.sh.j2 b/lego/templates/99-services.sh.j2
new file mode 100644
index 0000000..8d50f7b
--- /dev/null
+++ b/lego/templates/99-services.sh.j2
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+{% for svc, action in item.value.hooks.services.items() %}
+set -- systemctl "{{ action }}" "{{ svc }}"
+"$@"
+ret=$?
+if [ $ret -ne 0 ]; then
+    echo "(command was: $*)"
+    exit $ret
+fi
+{% endfor %}
diff --git a/lego/templates/_macros.j2 b/lego/templates/_macros.j2
new file mode 100644
index 0000000..75bf041
--- /dev/null
+++ b/lego/templates/_macros.j2
@@ -0,0 +1,52 @@
+{# -*- systemd -*- #}
+{% macro execstart(typ) %}
+ExecStart=/usr/bin/lego $LEGO_GLOBAL_ARGS $LEGO_ARGS \
+			--server https://${LEGO_ACME_SERVER_BASE}/directory \
+			--path $STATE_DIRECTORY \
+			--accept-tos \
+			{{ typ }} \
+{% if typ == "renew" %}
+			--no-random-sleep \
+{% endif %}
+			--{{ typ }}-hook='run-parts --report /etc/lego/hooks/%i/'
+{% endmacro %}
+
+{% macro systemd_boilerplate(typ) %}
+[Unit]
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+EnvironmentFile=/etc/lego/%i.env
+StateDirectory=lego
+StateDirectoryMode=0700
+RuntimeDirectory=lego
+Type=oneshot
+
+{{ execstart(typ) }}
+
+# PrivateDevices=yes
+# NoNewPrivileges=yes
+# ProtectHome=yes
+
+TimeoutStartSec=5min
+#CapabilityBoundingSet=CAP_CHOWN
+# Empty = no capabilities at all
+CapabilityBoundingSet=
+NoNewPrivileges=yes
+PrivateTmp=yes
+PrivateDevices=yes
+ProtectSystem=strict
+ProtectHome=yes
+ProtectHostname=yes
+ProtectClock=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectKernelLogs=yes
+ProtectControlGroups=yes
+RestrictRealtime=yes
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+LockPersonality=yes
+SystemCallFilter=@system-service
+SystemCallArchitectures=native
+{% endmacro %}
diff --git a/lego/templates/env-inst.j2 b/lego/templates/env-inst.j2
new file mode 100644
index 0000000..e56f40f
--- /dev/null
+++ b/lego/templates/env-inst.j2
@@ -0,0 +1,24 @@
+{% if lego_global_args is iterable and not lego_global_args is string -%}
+LEGO_GLOBAL_ARGS="{{ lego_global_args | join(' ') }}"
+{% else %}
+LEGO_GLOBAL_ARGS="{{ lego_global_args }}"
+{% endif %}
+LEGO_ACME_SERVER_BASE="{{ lego_acme_server_base }}"
+LEGO_CERT_DOMAIN="{{ item.key }}"
+LEGO_ARGS="\
+--email {{ item.value.account_mail }} \
+--{{ item.value.method.type }} \
+{% if item.value.method.type == "http" and item.value.method.subtype == "webroot" %}
+--http.webroot /run/lego \
+{% endif %}
+--domains {{ item.key }} \
+{% for d in item.value.domains %}
+--domains {{ d }} \
+{% endfor %}
+{% for i in item.value.extra_args %}
+{{ i }} \
+{% endfor %}
+"
+{% for k, v in item.value.extra_env.items() %}
+{{ k }}="{{ v }}"
+{% endfor %}
diff --git a/lego/templates/lego-renew@.service.j2 b/lego/templates/lego-renew@.service.j2
new file mode 100644
index 0000000..5142a85
--- /dev/null
+++ b/lego/templates/lego-renew@.service.j2
@@ -0,0 +1,6 @@
+{# -*- systemd -*- #}
+{% from "_macros.j2" import systemd_boilerplate -%}
+[Unit]
+Description=lego ACME client -- renewal for %i
+
+{{ systemd_boilerplate("renew") }}
diff --git a/lego/templates/lego-renew@.timer.j2 b/lego/templates/lego-renew@.timer.j2
new file mode 100644
index 0000000..5e249c8
--- /dev/null
+++ b/lego/templates/lego-renew@.timer.j2
@@ -0,0 +1,11 @@
+{# -*- systemd -*- #}
+[Unit]
+Description=lego ACME client -- renewal for %i timer
+
+[Timer]
+OnCalendar=weekly
+RandomizedDelaySec=1d
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/lego/templates/lego-run@.service.j2 b/lego/templates/lego-run@.service.j2
new file mode 100644
index 0000000..db4acb1
--- /dev/null
+++ b/lego/templates/lego-run@.service.j2
@@ -0,0 +1,6 @@
+{# -*- systemd -*- #}
+{% from "_macros.j2" import systemd_boilerplate -%}
+[Unit]
+Description=lego ACME client -- setup for %i
+
+{{ systemd_boilerplate("run") }}
diff --git a/lego/vars/main.yml b/lego/vars/main.yml
new file mode 100644
index 0000000..e13a319
--- /dev/null
+++ b/lego/vars/main.yml
@@ -0,0 +1,10 @@
+---
+
+cert_skeleton:
+  value:
+    account_mail: "{{ lego_account_mail }}"
+    domains: []
+    method: "{{ lego_method }}"
+    extra_args: []
+    extra_env: {}
+    hooks: "{{ lego_hooks }}"
-- 
GitLab