diff --git a/README.md b/README.md index 4416192..bb480f3 100644 --- a/README.md +++ b/README.md @@ -74,14 +74,21 @@ backend_override: {} # days of validity left on a certificate bevore it is renewed remainingdays: 28 -# challange type to use, can be: -# 'dns-01': use the dns challange and a custom power dns backend -# 'dns-01-manual': use the dns challange and manualy set the dns record -# 'http-01: use the http challange and deploy the challanges to a webserver -challange: dns-01 +# challenge type to use, can be: +# 'dns-01': use the dns challenge and a custom powerdns backend +# 'dns-01-manual': use the dns challenge and manualy set the dns record +# 'http-01: use the http challenge and deploy the challenges to a webserver +challenge: dns-01 -# servers to deploy a challange to -challangeserver: [] +# servers to deploy a challenge to +challengeserver: [] + +# Automaticly renew certificates using a cronjob +# Only supports the following cases: +# * 'dns-01' challenge with the custom powerdns backend +# This setting musst be set the first time the certificate is requested, it can not be enabled later without first deleting the certificates. +# Requires a working mail setup with some sort of sendmail binary to send warnings if a certificate can not be renewed. +autorenew: False ``` #### Selfsigned diff --git a/defaults/main.yml b/defaults/main.yml index afe31d8..1d39e8f 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -2,8 +2,9 @@ certificates: backends: letsencrypt: remainingdays: 28 - challange: dns-01 - challangeserver: [] + challenge: dns-01 + challengeserver: [] + autorenew: False selfsigned: not_after: "+3650d" ownca: diff --git a/files/letsencrypt_deploy_challenge.sh b/files/letsencrypt_deploy_challenge.sh new file mode 100755 index 0000000..4280872 --- /dev/null +++ b/files/letsencrypt_deploy_challenge.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for i in $LETSENCRYPT_CHALLENGE_SERVERS; do + ssh -i /etc/letsencrypt/renewkey -o "StrictHostKeyChecking no" letsencrypt@$i $(< $LETSENCRYPT_TOKEN ) $1 $2 +done diff --git a/files/letsencrypt_renew.sh b/files/letsencrypt_renew.sh new file mode 100755 index 0000000..25c53e0 --- /dev/null +++ b/files/letsencrypt_renew.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +source $1 + +daysleft=$(/usr/local/bin/acme-primitives.py remaining_days "$LETSENCRYPT_CRT" || echo "0") 2>/dev/null +[ "$daysleft" -lt "$LETSENCRYPT_REMAININGDAYS" ] || exit 0 + +folder="$(mktemp -d)" +cd "$folder" +/usr/local/bin/acme-primitives.py get_cert --directory 'https://acme-v02.api.letsencrypt.org/directory' --acc /etc/ssl/letsencrypt_account.key --csr $LETSENCRYPT_CSR /usr/local/bin/letsencrypt_deploy_challenge.sh > chained.pem + +cat chained.pem "$LETSENCRYPT_KEY" > full.pem +openssl x509 -in chained.pem > cert.pem + +chown -R root:ssl-cert . +chmod 0644 chained.pem +chmod 0644 cert.pem +chmod 0640 full.pem + +mv chained.pem "$LETSENCRYPT_CHAIN" +mv cert.pem "$LETSENCRYPT_CRT" +mv full.pem "$LETSENCRYPT_FULL" + +cd +rm -r "$folder" + +for i in $LETSENCRYPT_SERVICES; do + /bin/systemctl "$i" restart +done diff --git a/tasks/letsencrypt_cert.yml b/tasks/letsencrypt_cert.yml index 5a4bee7..f44fe73 100644 --- a/tasks/letsencrypt_cert.yml +++ b/tasks/letsencrypt_cert.yml @@ -1,9 +1,9 @@ - include_tasks: common_cert.yml - set_fact: - external_challange_type: "{{ map_challange_type_letsencrypt[cert_backend.challange]|d(cert_backend.challange) }}" + external_challenge_type: "{{ map_challenge_type_letsencrypt[cert_backend.challenge]|d(cert_backend.challenge) }}" -- name: "get challange for {{ certname }}" +- name: "get challenge for {{ certname }}" acme_certificate: &acmetask force: "{{ task_generate_csr is changed }}" acme_version: 2 @@ -14,43 +14,94 @@ dest: "{{ cert.certpath }}" fullchain_dest: "{{ cert.chainpath }}" remaining_days: "{{ cert_backend.remainingdays }}" - challenge: "{{ external_challange_type }}" + challenge: "{{ external_challenge_type }}" deactivate_authzs: yes register: challenge -- name: "setup challenge server for {{ certname }} (dns challange)" +- name: "setup autorenew for {{ certname }} (dns challenge)" + when: + - cert_backend.autorenew + - cert_backend.challenge == "dns-01" + block: + - name: create token + copy: + dest: "/etc/letsencrypt/cert_{{ certname }}.token" + mode: 0640 + owner: root + group: root + content: "{{ lookup('password', '/dev/null length=128 chars=ascii_letters,digits,hexdigits') }}" + force: no + - name: slurp up token + slurp: + src: "/etc/letsencrypt/cert_{{ certname }}.token" + register: tokenfile + - name: add renew ssh key to backend server + delegate_to: "{{ item }}" + loop: "{{ cert_backend.challengeserver }}" + authorized_key: + user: letsencrypt + key: "{{ letsencrypt_renewkey.public_key }}" + - name: add server token to record whitelist on backend server + when: + - challenge is changed + delegate_to: "{{ item.0 }}" + loop: "{{ cert_backend.challengeserver|product(challenge.challenge_data.keys()|list)|list }}" + command: + argv: + - "/usr/local/bin/pdns.py" + - "add_token" + - "--" + - "{{ tokenfile.content | b64decode }}" + - "{{ challenge.challenge_data[item.1]['dns-01'].record }}" + - name: create cert renew config + template: + src: letsencrypt_renew_config.j2 + dest: "/etc/letsencrypt/renew_{{ certname }}.config.sh" + mode: 0750 + owner: root + group: root + - name: setup renew cronjob + cron: + job: "/usr/local/bin/letsencrypt_renew.sh /etc/letsencrypt/renew_{{ certname }}.config.sh" + name: "letsencrypt: renew {{ certname }}" + hour: "{{ 23 | random(seed=inventory_hostname + certname + 'renew') }}" + minute: "{{ 59 | random(seed=inventory_hostname + certname + 'renew') }}" + +- name: "setup challenge server for {{ certname }} (dns challenge)" when: - challenge is changed - - cert_backend.challange == "dns-01" + - cert_backend.challenge == "dns-01" delegate_to: "{{ item.0 }}" - loop: "{{ cert_backend.challangeserver|product(challenge.challenge_data.keys()|list)|list }}" + loop: "{{ cert_backend.challengeserver|product(challenge.challenge_data.keys()|list)|list }}" command: argv: - "/usr/local/bin/pdns.py" + - "add_challenge" + - "--" - "{{ challenge.challenge_data[item.1]['dns-01'].record }}" - "{{ challenge.challenge_data[item.1]['dns-01'].resource_value }}" -- name: "setup challenge server for {{ certname }} (manual dns challange)" +- name: "setup challenge server for {{ certname }} (manual dns challenge)" when: - challenge is changed - - cert_backend.challange == "dns-01-manual" + - cert_backend.challenge == "dns-01-manual" loop: "{{ challenge.challenge_data_dns|d({})|dict2items }}" debug: msg: "add the following dns record: '{{ item.key }}.': { TXT: {{ item.value }} }" -- name: wait for challenges in dns (manual dns challange) +- name: wait for challenges in dns (manual dns challenge) pause: prompt: "When the relevant lines were added to dns and synced, press enter" when: - challenge is changed - - cert_backend.challange == "dns-01-manual" + - cert_backend.challenge == "dns-01-manual" -- name: "setup challenge server for {{ certname }} (http challange)" +- name: "setup challenge server for {{ certname }} (http challenge)" when: - challenge is changed - - cert_backend.challange == "http-01" + - cert_backend.challenge == "http-01" delegate_to: "{{ item.0 }}" - loop: "{{ cert_backend.challangeserver|product(challenge.challenge_data.keys()|list)|list }}" + loop: "{{ cert_backend.challengeserver|product(challenge.challenge_data.keys()|list)|list }}" copy: dest: "/var/www/letsencrypt/{{ challenge.challenge_data[item.1]['http-01'].resource | basename }}" content: "{{ challenge.challenge_data[item.1]['http-01'].resource_value }}" diff --git a/tasks/letsencrypt_setup.yml b/tasks/letsencrypt_setup.yml index f2e6833..d9a76fb 100644 --- a/tasks/letsencrypt_setup.yml +++ b/tasks/letsencrypt_setup.yml @@ -5,3 +5,52 @@ owner: root group: root mode: 0600 + +- name: register letsencrypt account + acme_account: + account_key_src: /etc/ssl/letsencrypt_account.key + state: present + terms_agreed: yes + acme_version: 2 + acme_directory: "https://acme-v02.api.letsencrypt.org/directory" + +- name: ensure config folders exist + file: + path: /etc/letsencrypt/ + state: directory + owner: root + group: root + mode: 0755 + +- name: generate letsencrypt auto renew ssh key + register: letsencrypt_renewkey + openssh_keypair: + owner: root + group: root + path: /etc/letsencrypt/renewkey + type: ed25519 + comment: "letsencrypt-renew@{{ inventory_hostname }}" + +- name: copy challenge deployment script + copy: + src: letsencrypt_deploy_challenge.sh + dest: /usr/local/bin/letsencrypt_deploy_challenge.sh + owner: root + group: root + mode: 0755 + +- name: copy letsencrypt renew skript + copy: + src: letsencrypt_renew.sh + dest: /usr/local/bin/letsencrypt_renew.sh + owner: root + group: root + mode: 0755 + +- name: copy acme primitives + get_url: + dest: /usr/local/bin/acme-primitives.py + owner: root + group: root + mode: 0755 + url: "https://git.notandy.de/ansible/acme-primitives/-/raw/master/acme-primitives.py" diff --git a/tasks/main.yml b/tasks/main.yml index 3b7bec6..607033d 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -3,6 +3,8 @@ pkg: - openssl - python3-cryptography + - python3-acme + - python3-click - name: add group ssl-cert group: diff --git a/templates/letsencrypt_renew_config.j2 b/templates/letsencrypt_renew_config.j2 new file mode 100644 index 0000000..07ac7ba --- /dev/null +++ b/templates/letsencrypt_renew_config.j2 @@ -0,0 +1,11 @@ +#!/bin/bash + +export LETSENCRYPT_CHALLENGE_SERVERS="{{ cert_backend.challengeserver|join(' ') }}" +export LETSENCRYPT_CSR="{{ cert.csrpath }}" +export LETSENCRYPT_CRT="{{ cert.certpath }}" +export LETSENCRYPT_KEY="{{ cert.keypath }}" +export LETSENCRYPT_CHAIN="{{ cert.chainpath }}" +export LETSENCRYPT_FULL="{{ cert.fullpath }}" +export LETSENCRYPT_TOKEN="/etc/letsencrypt/cert_{{ certname }}.token" +export LETSENCRYPT_SERVICES="{{ cert.depending_services }}" +export LETSENCRYPT_REMAININGDAYS="{{ cert_backend.remainingdays }}" diff --git a/vars/main.yml b/vars/main.yml index 168ac56..8d4f1a1 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -1,3 +1,3 @@ -map_challange_type_letsencrypt: +map_challenge_type_letsencrypt: 'dns-01-manual': 'dns-01'