add dehydrated role with pdns-api.sh support (#25)

* add dehydrated role with pdns-api.sh support

* Minor changes to Readme

* Remove Meta

* move dehydrated to linse

* Remove Zuckerwatte from PR (nothing to do with dehydrated)

* Add other domains to dehydrated config, added hook_chain

* Add authorized keys for cert user, add structures in /home/cert/ for checking out certs

* Send dehydrated ouput to /dev/null

* user authorized_keys module, add kumpir key

* Fix typo. Use \\n for each ssh-key

* remove unnecessary .ssh creation (done by authorized_key module)

* Added wrapper script to execute two hooks: pdns_api.sh + deploy certificates

* Remove challengetype variable, as only dns-01 is supported anyway.

* Add freifunk-mainz.de domain

* fix cert deploy script.
This commit is contained in:
prisma01 2019-09-08 20:44:26 +02:00 committed by GitHub
parent b564d8113c
commit 7611fb9d76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 474 additions and 4 deletions

View file

@ -6,9 +6,23 @@ magic: 71
nodejs_major_version: "10" nodejs_major_version: "10"
http_dns_prefix: "dns-ext" http_dns_prefix: "dns-ext"
pdns_limit_api_access:
- 94.130.21.214 dehydrated_accept_letsencrypt_terms: yes
- 2a01:4f8:10b:1b29::1 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_root_password: "{{ lookup('passwordstore', inventory_hostname_short + '/mysql_root subkey=secret') }}"
mysql_databases: mysql_databases:

View file

@ -21,4 +21,4 @@
- geerlingguy.mysql - geerlingguy.mysql
- powerdns.pdns - powerdns.pdns
- pdns-admin - pdns-admin
- pdns-api - service-dehydrated

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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 %}

View file

@ -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') }}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 %}