diff --git a/inventory/host_vars/linse.freifunk-mwu.de b/inventory/host_vars/linse.freifunk-mwu.de index 3c33b7e..737e123 100644 --- a/inventory/host_vars/linse.freifunk-mwu.de +++ b/inventory/host_vars/linse.freifunk-mwu.de @@ -6,9 +6,23 @@ magic: 71 nodejs_major_version: "10" http_dns_prefix: "dns-ext" -pdns_limit_api_access: - - 94.130.21.214 - - 2a01:4f8:10b:1b29::1 + +dehydrated_accept_letsencrypt_terms: yes +dehydrated_contactemail: hostmaster@freifunk-mwu.de +dehydrated_domains: ffmwu.org *.ffmwu.org freifunk-mwu.de *.freifunk-mwu.de freifunk-mainz.de *.freifunk-mainz.de freifunk-wiesbaden.de *.freifunk-wiesbaden.de mainz.freifunk.net *.mainz.freifunk.net wiesbaden.freifunk.net *.wiesbaden.freifunk.net +pdns_host: http://localhost:8081 +dehydrated_install_root: "{{ git_path }}/dehydrated" +dehydrated_deploycert: + ffmwu.org: | + mkdir -p /home/cert/certificates/$DOMAIN/ + cp $KEYFILE /home/cert/certificates/$DOMAIN/ + cp $CERTFILE /home/cert/certificates/$DOMAIN/ + cp $CHAINFILE /home/cert/certificates/$DOMAIN/ + cp $FULLCHAINFILE /home/cert/certificates/$DOMAIN/ + chmod 750 $(find /home/cert/certificates -mindepth 1 -type d ) + chmod 740 $(find /home/cert/certificates -mindepth 1 -type f ) + chown root.cert -R /home/cert/certificates/* +dehydrated_authorized_keys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHC4MutFCH6xgzwiarEjnASS5PpG3b3UEYDa8XNxCpy8 kumpir\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDzIaCJpcNlrwKAt+XAQEPOKvfV2xK5rhzyFez42rdmY84pMvAIbogI+HYZCgwUklcRA8/cpzX93sGFC9Gg/7drtRIL/3wQwf55UdeY+W+PVUkLGC/v+D6vSsxoYBn3eiHrbkZJOIJfLfiPrEUkQEAW98KRheySyhXAHpF+71jd/ZlD3DJtinXrexeHthX8APbgzoP6lQCQsH/XtqlO3bqSchTSj3MEl2ylRVWZHdgfc8+daX3s78T8C/zsue9AbXlWyjtL6n9fkYQ2kPkA+/0ymbHCFxq/rBnTq0CaIu/kiQjkI8oTi/tk3SpgNKnY2CKnTj1X62lEuAKgk2WfsjaVqH4K3xNJ4Ugl9kWCUPjJm+EMQ4rLhyi6lH10r2yYG1oBKGGOa+jYevHRqWmHmKuYTiHSFIQoPvd59MXE/3cQslt1RtcoxVl4E2U/S+s3ph8J4nGDa980oE+VMRKG4RsJs9H/1XvWSUVo14xWBufbJR/PnxNicjOhImbfN2rPL1YmwllaqQovRoE4BuwU05iVlT5KwKErHcBO+tz8Gm9IjBuUk6pbUNAi/8Of7/g4BUIvL78/JiAaWBlrGxA169L+r+d9urDibdfVbILrWGI61MX7S2v7KCu5yXT3+UOS2p6oJoahO1mWOMQXcwn1Yf1OlHg5RNUFqcNvw8u119H08Q== wasserfloh" mysql_root_password: "{{ lookup('passwordstore', inventory_hostname_short + '/mysql_root subkey=secret') }}" mysql_databases: diff --git a/playbooks/dns.yml b/playbooks/dns.yml index acb21fb..591f3ce 100755 --- a/playbooks/dns.yml +++ b/playbooks/dns.yml @@ -21,4 +21,4 @@ - geerlingguy.mysql - powerdns.pdns - pdns-admin - - pdns-api + - service-dehydrated diff --git a/roles/service-dehydrated/LICENSE b/roles/service-dehydrated/LICENSE new file mode 100644 index 0000000..72232d9 --- /dev/null +++ b/roles/service-dehydrated/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2018 Alexander Zielke +Copyright (c) 2019 Sebastian Schmachtel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/roles/service-dehydrated/README.md b/roles/service-dehydrated/README.md new file mode 100644 index 0000000..543515f --- /dev/null +++ b/roles/service-dehydrated/README.md @@ -0,0 +1,141 @@ +# service-dehydrated + +Install, configure and run dehydrated Let's Encrypt client using powerdns api hook + +- Based upon clutterbox.dehydrated (https://github.com/clutterbox/ansible-dehydrated) by Alexander Zielke +- Stripped down for simplicity: removed http-01 challenge, hooks +- Added pdns_api.sh (https://github.com/silkeh/pdns_api.sh) for powerdns api handling + + +- [service-dehydrated](#clutterboxdehydrated) + * [Role Variables](#role-variables) + * [Using dns-01 challenges](#using-dns-01-challenges) + * [using systemd timers](#using-systemd-timers) + * [Overriding per certificate config](#overriding-per-certificate-config) + * [dehydrated_deploycert](#dehydrated-deploycert) + + [Variables](#variables) + * [Example Playbooks](#example-playbooks) + + [Using dns-01 with cloudflare](#using-dns-01-with-cloudflare) + + [Using dehydrated_deploycert with multiple certificates](#using-dehydrated-deploycert-with-multiple-certificates) + * [License](#license) + * [Author Information](#author-information) + + + +## Role Variables + +Variable | Function | Default +--- | --- | --- +dehydrated_accept_letsencrypt_terms | Set to yes to automatically register and accept Let's Encrypt terms | no +dehydrated_contactemail | E-Mail address (required) | +dehydrated_domains | List of domains to request SSL certificates for | +dehydrated_deploycert | Script to run to deploy a certificate (see below) | +dehydrated_install_root | Where to install dehydrated | /opt/dehydrated +dehydrated_update | Update dehydrated sources on ansible run | yes +dehydrated_version | Which version to check out from github | HEAD +dehydrated_key_algo | Keytype to generate (rsa, prime256v1, secp384r1) | rsa +dehydrated_keysize | Size of Key (only for rsa Keys) | 4096 +dehydrated_ca | CA to use | https://acme-v02.api.letsencrypt.org/directory +dehydrated_cronjob | Install cronjob for certificate renewals | yes +dehydrated_systemd_timer | Use systemd timer for certificate renewals | no +dehydrated_run_on_changes | If dehydrated should run if the list of domains changed | yes +dehydrated_systemd_timer_onfailure | If set, an OnFailure-Directive will be added to the systemd unit | +dehydrated_cert_config | Override configuration for certificates | [] +dehydrated_repo_url | Specify URL to git repository of dehydrated | https://github.com/lukas2511/dehydrated.git +pdns_api_repo_url | Specify URL to git repository of pdns_api.sh | https://github.com/silkeh/pdns_api.sh +pdns_api_update | Update pdns_api.sh sources on ansible run | yes +pdns_api_version | Powerdns api version (v>=4 ? 1 : 0) | 1 + +## Using dns-01 challenges + +Due to simplicity only dns-01 is supported. See [Example Playbooks](#example-playbooks) + +## using systemd timers + +It is possible to use a systemd-timer instead of a cronjob to renew certificates. + +**Note**: Enabling the systemd timer does *not* disable the cronjob. This might change in the future. + +```yaml +dehydrated_systemd_timer: yes +dehydrated_cronjob: no +``` + +## Overriding per certificate config + + +The Configration for single certificates can be overridden using `dehydrated_cert_config`. + +`dehydrated_cert_config` must be a list of dicts. Only the elemenent `name:` is mandatory ans must match a certificate name. The certificate name is either the first domain listed in domains.txt or the certificate alias, if defined. + +Format is as follows: + +```yaml +dehydrated_cert_config: + - name: # certificate name or alias (mandatory) + state: present # present or absent (optional) + wellknown: # override WELLKNOWN (optional) + key_algo: # override KEY_ALGO (optional) + keysize: # override KEYSIZE (optional) +``` + +## dehydrated_deploycert + +The variable dehydrated_deploycert contains a shellscript fragment to be executed when a certificate has successfully been optained. This variable can either be a multiline string or a hash of multiline strings. + +```yaml +dehydrated_deploycert: | + service nginx reload +``` + +In this example, for ever certificate obtained, nginx will be reloaded + +```yaml +dehydrated_deploycert: + example.com: | + service nginx reload + service.example.com: | + cat ${FULLCHAINFILE} ${KEYFILE} > /etc/somewhere/ssl/full.pem + service someservice reload +``` + +Here, for certificates with the primary domain example.com, nginx will be reloaded and for service.example.com the certificate, intermediate and key will be written to another file and someservice is reloaded. + +### Variables + +Variable | Function +--- | --- +DOMAIN | (Primary) Domain of the certificate +KEYFILE | Full path to the keyfile +CERTFILE | Full path to certificate file +FULLCHAINFILE | Full path to file containing both certificate and intermediate +CHAINFILE | Full path to intermediate certificate file +TIMESTAMP | Timestamp when the certificate was created. + +## Example Playbooks + +### Using dns-01 with powerdns (only supported use case) +```yaml +- hosts: servers + vars: + dehydrated_accept_letsencrypt_terms: yes + dehydrated_contactemail: hostmaster@example.com + dehydrated_domains: example.com + pdns_host: https://powerdns-api.url.com:port + + dehydrated_deploycert: | + service nginx reload + roles: + - service-dehydrated +``` + + +# License + +MIT License + +# Author Information + +Alexander Zielke - mail@alexander.zielke.name + +Sebastian Schmachtel - prisma_freifunk@oimel.net diff --git a/roles/service-dehydrated/defaults/main.yml b/roles/service-dehydrated/defaults/main.yml new file mode 100644 index 0000000..6c3a7ba --- /dev/null +++ b/roles/service-dehydrated/defaults/main.yml @@ -0,0 +1,21 @@ +--- +dehydrated_dependencies: + - git + - openssl + - curl +dehydrated_repo_url: https://github.com/lukas2511/dehydrated.git +dehydrated_install_root: /opt/dehydrated +dehydrated_update: yes +dehydrated_version: HEAD +dehydrated_key_algo: rsa +dehydrated_keysize: 4096 +dehydrated_ca: "https://acme-v02.api.letsencrypt.org/directory" +dehydrated_cronjob: yes +dehydrated_run_on_changes: yes +dehydrated_systemd_timer: no +dehydrated_hook_scripts: [] +dehydrated_cert_config: [] +# dehydrated_systemd_timer_onfailure: some_unit.service +pdns_api_version: HEAD +pdns_api_repo_url: https://github.com/silkeh/pdns_api.sh.git +pdns_api_update: yes diff --git a/roles/service-dehydrated/handlers/main.yml b/roles/service-dehydrated/handlers/main.yml new file mode 100644 index 0000000..371a6f2 --- /dev/null +++ b/roles/service-dehydrated/handlers/main.yml @@ -0,0 +1,14 @@ +--- +- name: run dehydrated + command: "{{ dehydrated_install_root }}/dehydrated -c" + when: dehydrated_run_on_changes + +- name: Reload systemd + systemd: + daemon_reload: true + +- name: Remove timer + systemd: + name: dehydrated.timer + enabled: no + state: stopped diff --git a/roles/service-dehydrated/tasks/domain_config.yml b/roles/service-dehydrated/tasks/domain_config.yml new file mode 100644 index 0000000..72e6585 --- /dev/null +++ b/roles/service-dehydrated/tasks/domain_config.yml @@ -0,0 +1,28 @@ +--- +- name: Ensure certificate directory exists + file: + path: "/etc/dehydrated/certs/{{ item.name }}" + state: directory + owner: root + group: root + mode: 0700 + loop: "{{ dehydrated_cert_config }}" + +- name: Generate per certificate configs + template: + dest: "/etc/dehydrated/certs/{{ item.name }}/config" + src: certconfig.j2 + owner: root + group: root + mode: 0600 + loop: "{{ dehydrated_cert_config }}" + when: item.state|default('present') == "present" + notify: run dehydrated + +- name: Remove per certificate configs + file: + path: "/etc/dehydrated/certs/{{ item.name }}/config" + state: absent + loop: "{{ dehydrated_cert_config }}" + when: item.state|default('present') == "absent" + notify: run dehydrated diff --git a/roles/service-dehydrated/tasks/main.yml b/roles/service-dehydrated/tasks/main.yml new file mode 100644 index 0000000..39cb1fd --- /dev/null +++ b/roles/service-dehydrated/tasks/main.yml @@ -0,0 +1,131 @@ +--- +- name: Install dehydrated dependencies + apt: name={{ dehydrated_dependencies }} + +- name: Checkout dehydrated from github + git: + repo: "{{ dehydrated_repo_url }}" + update: "{{ dehydrated_update }}" + dest: "{{ dehydrated_install_root }}" + version: "{{ dehydrated_version }}" + +- name: Checkout pdns_api.sh from github + git: + repo: "{{ pdns_api_repo_url }}" + update: "{{ pdns_api_update }}" + dest: "{{ dehydrated_install_root }}/pdns_api" + version: "{{ pdns_api_version }}" + +- name: Create /etc/dehydrated + file: dest=/etc/dehydrated state=directory owner=root group=root mode=0700 + +- name: Generate dehydrated config + template: + dest: /etc/dehydrated/config + src: config.j2 + owner: root + group: root + mode: 0600 + +- name: Generate dehydrated domains.txt + copy: + dest: /etc/dehydrated/domains.txt + content: "{{ dehydrated_domains }}" + owner: root + group: root + mode: 0600 + notify: run dehydrated + +- import_tasks: domain_config.yml + +- name: Generate hookwrapper.sh + template: + src: hookwrapper.j2 + dest: /etc/dehydrated/hookwrapper.sh + owner: root + group: root + mode: "0700" + when: dehydrated_deploycert is defined + +- name: Generate deploycert.sh + template: + src: deploycert.j2 + dest: /etc/dehydrated/deploycert.sh + owner: root + group: root + mode: "0700" + when: dehydrated_deploycert is defined + +- name: Remove deploycert.sh + file: dest=/etc/dehydrated/deploycert.sh state=absent + when: dehydrated_deploycert is not defined + +- name: Remove hookwrapper.sh + file: dest=/etc/dehydrated/hookwrapper.sh state=absent + when: dehydrated_deploycert is not defined + +- name: Install cronjob + cron: + name: dehydrated-renew + minute: "{{ 59|random(seed=inventory_hostname) }}" + hour: "{{ 4|random(seed=inventory_hostname) }}" + user: root + job: "{{ dehydrated_install_root }}/dehydrated -c > /dev/null" + cron_file: dehydrated + state: "{{ 'present' if dehydrated_cronjob else 'absent' }}" + +- import_tasks: systemd.yml + +# /opt/dehydrated/dehydrated --register --accept-terms +- name: Check if already registered + stat: + path: "/etc/dehydrated/accounts/{{ ((dehydrated_ca + '\n')|b64encode).rstrip('=').replace('+', '-').replace('/', '_') }}" + register: ca_stat + +- block: + - name: "assert dehydrated_accept_letsencrypt_terms is true" + assert: + that: dehydrated_accept_letsencrypt_terms + + - name: Register to CA + command: "{{ dehydrated_install_root }}/dehydrated --register --accept-terms" + # \end block register + when: "not ca_stat.stat.exists or (ca_stat.stat.isdir is defined and not ca_stat.stat.isdir)" + +- meta: flush_handlers + + +- name: Add the cert user for distributing certs + user: + name: cert + +- name: Create cert/bin directory if it does not exist + file: + path: /home/cert/bin + state: directory + owner: cert + group: cert + mode: '0700' + +- name: Create certificates directory if it does not exist + file: + path: /home/cert/certificates + state: directory + owner: cert + group: cert + mode: '0700' + +- name: generate authorized_keys + authorized_key: + key: "{{ dehydrated_authorized_keys }}" + key_options: command="$HOME/bin/rrsync -ro ~/certificates",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding + user: cert + exclusive: true + +- name: Download rrsync + get_url: + url: http://ftp.samba.org/pub/unpacked/rsync/support/rrsync + dest: /home/cert/bin/rrsync + owner: cert + group: cert + mode: '0700' diff --git a/roles/service-dehydrated/tasks/systemd.yml b/roles/service-dehydrated/tasks/systemd.yml new file mode 100644 index 0000000..d4208be --- /dev/null +++ b/roles/service-dehydrated/tasks/systemd.yml @@ -0,0 +1,29 @@ +--- + +- name: Upload systemd service files + template: + src: "{{ item }}.j2" + dest: /etc/systemd/system/{{ item }} + loop: + - dehydrated.service + - dehydrated.timer + when: dehydrated_systemd_timer + notify: Reload systemd + +- name: Remove system service files + file: + path: /etc/systemd/system/{{ item }} + state: absent + loop: + - dehydrated.service + - dehydrated.timer + when: not dehydrated_systemd_timer + notify: Remove timer + +- name: Activate systemd timer + systemd: + daemon_reload: yes + name: dehydrated.timer + enabled: yes + state: started + when: dehydrated_systemd_timer diff --git a/roles/service-dehydrated/templates/certconfig.j2 b/roles/service-dehydrated/templates/certconfig.j2 new file mode 100644 index 0000000..2577c07 --- /dev/null +++ b/roles/service-dehydrated/templates/certconfig.j2 @@ -0,0 +1,5 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +{% if item.challengetype is defined %}CHALLENGETYPE={{ item.challengetype }}{% endif %} +{% if item.key_algo is defined %}KEY_ALGO={{ item.key_algo }}{% endif %} +{% if item.keysize is defined %}KEYSIZE={{ item.keysize }}{% endif %} +{% if item.wellknown is defined %}WELLKNOWN={{ item.wellknown }}{% endif %} diff --git a/roles/service-dehydrated/templates/config.j2 b/roles/service-dehydrated/templates/config.j2 new file mode 100644 index 0000000..e21d8d3 --- /dev/null +++ b/roles/service-dehydrated/templates/config.j2 @@ -0,0 +1,14 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +CA="{{ dehydrated_ca }}" +CHALLENGETYPE="dns-01" +CONTACT_EMAIL="{{ dehydrated_contactemail | mandatory }}" +KEY_ALGO={{ dehydrated_key_algo }} +KEYSIZE={{ dehydrated_keysize }} +{% if dehydrated_deploycert is defined %} +HOOK=/etc/dehydrated/hookwrapper.sh +{% else %} +HOOK={{ dehydrated_install_root }}/pdns_api/pdns_api.sh +{% endif %} +HOOK_CHAIN="yes" +PDNS_HOST={{ pdns_host}} +PDNS_KEY={{ lookup('passwordstore', 'linse/pdns_apikey') }} diff --git a/roles/service-dehydrated/templates/dehydrated.service.j2 b/roles/service-dehydrated/templates/dehydrated.service.j2 new file mode 100644 index 0000000..e8587be --- /dev/null +++ b/roles/service-dehydrated/templates/dehydrated.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=ACME Cert renewal +ConditionFileNotEmpty=/etc/dehydrated/domains.txt +{% if dehydrated_systemd_timer_onfailure is defined %} +OnFailure={{ dehydrated_systemd_timer_onfailure }} +{% endif %} + +[Service] +User=root +ExecStart={{ dehydrated_install_root }}/dehydrated --cron +Type=oneshot diff --git a/roles/service-dehydrated/templates/dehydrated.timer.j2 b/roles/service-dehydrated/templates/dehydrated.timer.j2 new file mode 100644 index 0000000..6d4e8ce --- /dev/null +++ b/roles/service-dehydrated/templates/dehydrated.timer.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=ACME Cert renewal timer + +[Timer] +OnCalendar=*-*-* {{ 4|random(seed=inventory_hostname) }}:{{ 59|random(seed=inventory_hostname) }}:00 +Persistent=true +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target diff --git a/roles/service-dehydrated/templates/deploycert.j2 b/roles/service-dehydrated/templates/deploycert.j2 new file mode 100644 index 0000000..922d067 --- /dev/null +++ b/roles/service-dehydrated/templates/deploycert.j2 @@ -0,0 +1,25 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +deploy_cert() { + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" + +{% if dehydrated_deploycert is string %} + {{ dehydrated_deploycert }} +{% else %} +{% for domain, script in dehydrated_deploycert.items() %} + if [[ "${DOMAIN}" = "{{ domain }}" ]]; then + {{ script }} + fi +{% endfor %} +{% endif %} +} + +HANDLER="$1"; shift +if [[ "${HANDLER}" =~ ^(deploy_cert)$ ]]; then + "$HANDLER" "$@" +fi diff --git a/roles/service-dehydrated/templates/hookwrapper.j2 b/roles/service-dehydrated/templates/hookwrapper.j2 new file mode 100644 index 0000000..5232269 --- /dev/null +++ b/roles/service-dehydrated/templates/hookwrapper.j2 @@ -0,0 +1,7 @@ +#jinja2: trim_blocks: True, lstrip_blocks: True +#!/usr/bin/env bash + +{{ dehydrated_install_root }}/pdns_api/pdns_api.sh "$@" +{% if dehydrated_deploycert is defined %} +/etc/dehydrated/deploycert.sh "$@" +{% endif %}