From e817fa4a1f98821b907a01191dbe5b10769b46aa Mon Sep 17 00:00:00 2001 From: Christian Pointner Date: Sat, 6 Jul 2019 02:45:49 +0200 Subject: [PATCH] added acmetool roles --- ansible/group_vars/all/main.yml | 2 + ansible/host_playbooks/vex2.yml | 1 + ansible/roles/acmetool/base/defaults/main.yml | 23 ++++ ansible/roles/acmetool/base/tasks/main.yml | 65 +++++++++ ansible/roles/acmetool/base/tasks/selfsigned.yml | 145 ++++++++++++++++++++ .../roles/acmetool/base/templates/acme-reload.j2 | 7 + ansible/roles/acmetool/base/templates/responses.j2 | 12 ++ .../base/templates/systemd-override.conf.j2 | 10 ++ .../acmetool/cert/filter_plugins/acme_certs.py | 24 ++++ ansible/roles/acmetool/cert/handlers/main.yml | 5 + ansible/roles/acmetool/cert/tasks/main.yml | 10 ++ 11 files changed, 304 insertions(+) create mode 100644 ansible/roles/acmetool/base/defaults/main.yml create mode 100644 ansible/roles/acmetool/base/tasks/main.yml create mode 100644 ansible/roles/acmetool/base/tasks/selfsigned.yml create mode 100644 ansible/roles/acmetool/base/templates/acme-reload.j2 create mode 100644 ansible/roles/acmetool/base/templates/responses.j2 create mode 100644 ansible/roles/acmetool/base/templates/systemd-override.conf.j2 create mode 100644 ansible/roles/acmetool/cert/filter_plugins/acme_certs.py create mode 100644 ansible/roles/acmetool/cert/handlers/main.yml create mode 100644 ansible/roles/acmetool/cert/tasks/main.yml diff --git a/ansible/group_vars/all/main.yml b/ansible/group_vars/all/main.yml index 2d6e172..f1272b0 100644 --- a/ansible/group_vars/all/main.yml +++ b/ansible/group_vars/all/main.yml @@ -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 diff --git a/ansible/host_playbooks/vex2.yml b/ansible/host_playbooks/vex2.yml index 6de0a29..8ab2aef 100644 --- a/ansible/host_playbooks/vex2.yml +++ b/ansible/host_playbooks/vex2.yml @@ -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 index 0000000..c9a7107 --- /dev/null +++ b/ansible/roles/acmetool/base/defaults/main.yml @@ -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 index 0000000..e46b9d9 --- /dev/null +++ b/ansible/roles/acmetool/base/tasks/main.yml @@ -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 index 0000000..7ba829e --- /dev/null +++ b/ansible/roles/acmetool/base/tasks/selfsigned.yml @@ -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 index 0000000..a679bc7 --- /dev/null +++ b/ansible/roles/acmetool/base/templates/acme-reload.j2 @@ -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 index 0000000..411455b --- /dev/null +++ b/ansible/roles/acmetool/base/templates/responses.j2 @@ -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 index 0000000..aec6f03 --- /dev/null +++ b/ansible/roles/acmetool/base/templates/systemd-override.conf.j2 @@ -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 index 0000000..179f71e --- /dev/null +++ b/ansible/roles/acmetool/cert/filter_plugins/acme_certs.py @@ -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 index 0000000..3d6f1b7 --- /dev/null +++ b/ansible/roles/acmetool/cert/handlers/main.yml @@ -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 index 0000000..c2f778f --- /dev/null +++ b/ansible/roles/acmetool/cert/tasks/main.yml @@ -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 -- 1.7.10.4