added acmetool roles
authorChristian Pointner <equinox@realraum.at>
Sat, 6 Jul 2019 00:45:49 +0000 (02:45 +0200)
committerBernhard Tittelbach <bernhard@tittelbach.org>
Wed, 23 Jul 2025 00:23:28 +0000 (02:23 +0200)
ansible/group_vars/all/main.yml
ansible/host_playbooks/vex2.yml
ansible/roles/acmetool/base/defaults/main.yml [new file with mode: 0644]
ansible/roles/acmetool/base/tasks/main.yml [new file with mode: 0644]
ansible/roles/acmetool/base/tasks/selfsigned.yml [new file with mode: 0644]
ansible/roles/acmetool/base/templates/acme-reload.j2 [new file with mode: 0644]
ansible/roles/acmetool/base/templates/responses.j2 [new file with mode: 0644]
ansible/roles/acmetool/base/templates/systemd-override.conf.j2 [new file with mode: 0644]
ansible/roles/acmetool/cert/filter_plugins/acme_certs.py [new file with mode: 0644]
ansible/roles/acmetool/cert/handlers/main.yml [new file with mode: 0644]
ansible/roles/acmetool/cert/tasks/main.yml [new file with mode: 0644]

index 2d6e172..f1272b0 100644 (file)
@@ -12,3 +12,5 @@ root_password: "{{ vault_root_password }}"
 ## SSH keys for root, default to NOC's
 
 ssh_users_root: "{{ user_groups.noc }}"
+
+acmetool_account_email: noc@realraum.at
index 6de0a29..8ab2aef 100644 (file)
@@ -3,3 +3,4 @@
   hosts: vex2
   roles:
   - role: base
+  - role: acmetool/base
diff --git a/ansible/roles/acmetool/base/defaults/main.yml b/ansible/roles/acmetool/base/defaults/main.yml
new file mode 100644 (file)
index 0000000..c9a7107
--- /dev/null
@@ -0,0 +1,23 @@
+---
+acmetool_directory_server_le_live: "https://acme-v01.api.letsencrypt.org/directory"
+acmetool_directory_server_le_staging: "https://acme-staging.api.letsencrypt.org/directory"
+
+## this can't be changed after the account as been created (aka after the first run)
+## and it's not recommended to keep this empty so we don't define it here which will lead to an error
+# acmetool_account_email:
+acmetool_directory_server: "{{ acmetool_directory_server_le_staging }}"
+
+#### optionally set http(s)_proxy
+# acmetool_http_proxy:
+# acmetool_https_proxy:
+
+acmetool_default_key_type: rsa
+acmetool_default_rsa_key_size: 4096
+acmetool_default_ecdsa_curve: nistp256
+
+### this defaults to '/var/run/acme/acme-challenge'
+# acmetool_challenge_webroot_path: "/path/to/acme-challenge"
+
+### by default a number of daemons are tried to be reloaded
+### an empty list disables reloading of any service
+# acmetool_reload_services: []
diff --git a/ansible/roles/acmetool/base/tasks/main.yml b/ansible/roles/acmetool/base/tasks/main.yml
new file mode 100644 (file)
index 0000000..e46b9d9
--- /dev/null
@@ -0,0 +1,65 @@
+---
+- name: check if acmetool package is new enough
+  debug:
+    msg: "Check distribution_release"
+  failed_when: (ansible_distribution == 'Debian' and ansible_distribution_major_version < 9) or (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version < 17) or (ansible_distribution != 'Debian' and ansible_distribution != 'Ubuntu')
+
+- name: install needed packages
+  apt:
+    name:
+      - acmetool
+      - python-openssl
+    state: present
+
+- name: create initial directory structure
+  command: acmetool --batch
+  args:
+    creates: /var/lib/acme/conf
+
+- name: create acmetool response file
+  template:
+    src: responses.j2
+    dest: /var/lib/acme/conf/responses
+
+- name: create non-standard acmetool webroot path
+  file:
+    name: "{{ acmetool_challenge_webroot_path }}"
+    state: directory
+  when: acmetool_challenge_webroot_path is defined
+
+- name: run quickstart to create account and default target configuration
+  command: acmetool --batch quickstart
+  environment:
+    http_proxy: "{{ acmetool_http_proxy | default('') }}"
+    https_proxy: "{{ acmetool_https_proxy | default('') }}"
+  args:
+    creates: /var/lib/acme/conf/target
+
+- name: generate selfsigned interim certificate
+  include_tasks: selfsigned.yml
+
+- name: install service reload configuration
+  template:
+    src: acme-reload.j2
+    dest: /etc/default/acme-reload
+    owner: root
+    group: root
+    mode: 0644
+  when: acmetool_reload_services is defined
+
+- name: create system unit snippet directory
+  file:
+    path: /etc/systemd/system/acmetool.service.d/
+    state: directory
+
+- name: install systemd unit snippet
+  template:
+    src: systemd-override.conf.j2
+    dest: /etc/systemd/system/acmetool.service.d/override.conf
+
+- name: enable/start systemd timer for acmetool
+  systemd:
+    name: acmetool.timer
+    state: started
+    enabled: yes
+    daemon_reload: yes
diff --git a/ansible/roles/acmetool/base/tasks/selfsigned.yml b/ansible/roles/acmetool/base/tasks/selfsigned.yml
new file mode 100644 (file)
index 0000000..7ba829e
--- /dev/null
@@ -0,0 +1,145 @@
+---
+- name: get id of existing selfsigned interim certificate
+  shell: cat /var/lib/acme/.selfsigned-interim-cert || true
+  changed_when: false
+  check_mode: false
+  register: existing_selfsigned_interim_cert_id
+
+- name: set existing_selfsigned_interim_cert_id variable
+  set_fact:
+    existing_selfsigned_interim_cert_id: "{{ existing_selfsigned_interim_cert_id.stdout }}"
+
+- name: check if selfsigned interim certificate does exist
+  stat:
+    path: "/var/lib/acme/certs/{{ existing_selfsigned_interim_cert_id }}"
+  register: existing_selfsigned_interim_cert_stat
+
+- name: create selfsigned interim certificate
+  when: not existing_selfsigned_interim_cert_id or not existing_selfsigned_interim_cert_stat.stat.exists
+  block:
+    - name: create temporary directory
+      tempfile:
+        path: /var/lib/acme/tmp
+        prefix: selfsigned-interim-cert-
+        state: directory
+      register: tmpdir
+
+    - name: set tmpdir variable
+      set_fact:
+        tmpdir: "{{ tmpdir.path }}"
+
+    - name: generate private key for selfsigned interim certificate
+      openssl_privatekey:
+        path: "{{ tmpdir }}/privkey"
+        mode: 0600
+
+    - name: generate csr for selfsigned interim certificate
+      openssl_csr:
+        path: "{{ tmpdir }}/csr"
+        privatekey_path: "{{ tmpdir }}/privkey"
+        common_name: "{{ ansible_fqdn }}"
+
+
+    ### this is needed because strftime filter in ansible is exceptionally stupid
+    ### see: https://github.com/ansible/ansible/issues/39835
+    - name: get remote date-time 10s ago
+      command: date -d '10 seconds ago' -u '+%Y%m%d%H%M%SZ'
+      register: remote_datetime_10sago
+
+    - name: get remote date-time now
+      command: date -u '+%Y%m%d%H%M%SZ'
+      register: remote_datetime_now
+
+    - name: generate selfsigned interim certificate
+      openssl_certificate:
+        path: "{{ tmpdir }}/cert"
+        privatekey_path: "{{ tmpdir }}/privkey"
+        csr_path: "{{ tmpdir }}/csr"
+        provider: selfsigned
+        ## make sure the certificate is not valid anymore to force acmetool to create a new cert
+        selfsigned_not_before: "{{ remote_datetime_10sago.stdout }}"
+        selfsigned_not_after: "{{ remote_datetime_now.stdout }}"
+
+    - name: remove csr for selfsigned interim certificate
+      file:
+        path: "{{ tmpdir }}/csr"
+        state: absent
+
+    - name: copy selfsigned interim certificate for fullchain
+      command: "cp '{{ tmpdir }}/cert' '{{ tmpdir }}/fullchain'"
+
+    - name: create additional empty files
+      loop:
+        - chain
+        - selfsigned
+      copy:
+        content: ""
+        dest: "{{ tmpdir }}/{{ item }}"
+
+    ### TODO: remove this once acmetool respects it's own storage layout
+    ###       see: https://github.com/hlandau/acme/blob/master/_doc/SCHEMA.md#temporary-use-of-self-signed-certificates
+    - name: generate fake url file
+      copy:
+        content: "https://acme.example.com/acme/cert/self-signed\n"
+        dest: "{{ tmpdir }}/url"
+
+    - name: get key id
+      shell: "openssl x509 -in '{{ tmpdir }}/cert' -noout -pubkey | openssl enc -base64 -d | openssl sha256 -binary | base32 | tr -d '=' | tr '[:upper:]' '[:lower:]'"
+      register: selfsigned_interim_key_id
+
+    - name: set selfsigned_interim_key_id variable
+      set_fact:
+        selfsigned_interim_key_id: "{{ selfsigned_interim_key_id.stdout }}"
+
+    - name: create directory for private key of selfsigned interim certificate
+      file:
+        path: "/var/lib/acme/keys/{{ selfsigned_interim_key_id }}"
+        state: directory
+        mode: 0700
+
+    - name: move private key to its directory
+      command: "mv '{{ tmpdir }}/privkey' '/var/lib/acme/keys/{{ selfsigned_interim_key_id }}/privkey'"
+
+    - name: create symlink to privkey
+      file:
+        src: "../../keys/{{ selfsigned_interim_key_id }}/privkey"
+        dest: "{{ tmpdir }}/privkey"
+        state: link
+
+    # - name: get certificate id
+    #   shell: "openssl x509 -in '{{ tmpdir }}/cert' -outform der | openssl sha256 -binary | base32 | tr -d '=' | tr '[:upper:]' '[:lower:]'"
+    #   register: selfsigned_interim_cert_id
+
+    # - name: set selfsigned_interim_cert_id variable
+    #   set_fact:
+    #     selfsigned_interim_cert_id: "selfsigned-{{ selfsigned_interim_cert_id.stdout }}"
+
+    ### TODO: replace with the above once acmetool respects it's own storage layout
+    ###       see: https://github.com/hlandau/acme/blob/master/_doc/SCHEMA.md#temporary-use-of-self-signed-certificates
+    - name: get certificate id
+      shell: "cat '{{ tmpdir }}/url' | tr -d '\n' | openssl sha256 -binary | base32 | tr -d '=' | tr '[:upper:]' '[:lower:]'"
+      register: selfsigned_interim_cert_id
+
+    - name: set selfsigned_interim_cert_id variable
+      set_fact:
+        selfsigned_interim_cert_id: "{{ selfsigned_interim_cert_id.stdout }}"
+
+    - name: set permissions for selfsigned interim certificate directory
+      file:
+        path: "{{ tmpdir }}"
+        mode: 0755
+        state: directory
+
+    - name: move selfsigned interim certificate directory into place
+      command: "mv '{{ tmpdir }}' '/var/lib/acme/certs/{{ selfsigned_interim_cert_id }}'"
+
+    - name: write cert-id of selfsigned interim certificate to state directory
+      copy:
+        content: "{{ selfsigned_interim_cert_id }}"
+        dest: /var/lib/acme/.selfsigned-interim-cert
+
+  rescue:
+    - name: remove temporary directory for selfsigned interim certificate
+      file:
+        path: "{{ tmpdir }}"
+        state: absent
diff --git a/ansible/roles/acmetool/base/templates/acme-reload.j2 b/ansible/roles/acmetool/base/templates/acme-reload.j2
new file mode 100644 (file)
index 0000000..a679bc7
--- /dev/null
@@ -0,0 +1,7 @@
+# This should contain a space-seperated list of services to be
+# reloaded after new certificates are generated. An empty list
+# disables reloading of any service
+#
+# example: SERVICES="apache2 nginx postfix"
+
+SERVICES="{{ acmetool_reload_services | join(' ') }}"
diff --git a/ansible/roles/acmetool/base/templates/responses.j2 b/ansible/roles/acmetool/base/templates/responses.j2
new file mode 100644 (file)
index 0000000..411455b
--- /dev/null
@@ -0,0 +1,12 @@
+"acme-enter-email": "{{ acmetool_account_email }}"
+"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf": true
+"acmetool-quickstart-choose-server": {{ acmetool_directory_server }}
+"acmetool-quickstart-choose-method": webroot
+"acmetool-quickstart-webroot-path": "{{ acmetool_challenge_webroot_path | default('/var/run/acme/acme-challenge') }}"
+"acmetool-quickstart-complete": true
+"acmetool-quickstart-install-cronjob": false
+"acmetool-quickstart-install-haproxy-script": true
+"acmetool-quickstart-install-redirector-systemd": false
+"acmetool-quickstart-key-type": {{ acmetool_default_key_type }}
+"acmetool-quickstart-rsa-key-size": {{ acmetool_default_rsa_key_size }}
+"acmetool-quickstart-ecdsa-curve": {{ acmetool_default_ecdsa_curve }}
diff --git a/ansible/roles/acmetool/base/templates/systemd-override.conf.j2 b/ansible/roles/acmetool/base/templates/systemd-override.conf.j2
new file mode 100644 (file)
index 0000000..aec6f03
--- /dev/null
@@ -0,0 +1,10 @@
+[Service]
+{% if acmetool_http_proxy is defined %}
+Environment=http_proxy={{ acmetool_http_proxy }}
+{% endif %}
+{% if acmetool_https_proxy is defined %}
+Environment=https_proxy={{ acmetool_https_proxy }}
+{% endif %}
+{% if acmetool_challenge_webroot_path is defined %}
+ReadWritePaths={{ acmetool_challenge_webroot_path }}
+{% endif %}
diff --git a/ansible/roles/acmetool/cert/filter_plugins/acme_certs.py b/ansible/roles/acmetool/cert/filter_plugins/acme_certs.py
new file mode 100644 (file)
index 0000000..179f71e
--- /dev/null
@@ -0,0 +1,24 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from functools import partial
+
+from ansible import errors
+
+
+def acme_cert_nonexistent(data, hostnames):
+    try:
+        return [hostnames[i] for i, d in enumerate(data) if d['stat']['exists'] == False]
+    except Exception as e:
+        raise errors.AnsibleFilterError("acme_cert_nonexistent(): %s" % str(e))
+
+
+class FilterModule(object):
+
+    ''' acme certificate filters '''
+    filter_map = {
+        'acme_cert_nonexistent': acme_cert_nonexistent,
+    }
+
+    def filters(self):
+        return self.filter_map
diff --git a/ansible/roles/acmetool/cert/handlers/main.yml b/ansible/roles/acmetool/cert/handlers/main.yml
new file mode 100644 (file)
index 0000000..3d6f1b7
--- /dev/null
@@ -0,0 +1,5 @@
+---
+- name: reconcile acmetool
+  systemd:
+    name: acmetool.service
+    state: started
diff --git a/ansible/roles/acmetool/cert/tasks/main.yml b/ansible/roles/acmetool/cert/tasks/main.yml
new file mode 100644 (file)
index 0000000..c2f778f
--- /dev/null
@@ -0,0 +1,10 @@
+- name: add acmetool desired file
+  loop:
+    - satisfy:
+        names: "{{ acmetool_cert_hostnames | default([acmetool_cert_name]) }}"
+  loop_control:
+    label: "{{ item.satisfy.names | join(', ') }}"
+  copy:
+    content: "{{ item | to_nice_yaml }}"
+    dest: "/var/lib/acme/desired/{{ acmetool_cert_name }}"
+  notify: reconcile acmetool