diff --git a/files/acme-primitives.py b/files/acme-primitives.py new file mode 100755 index 0000000..c59127b --- /dev/null +++ b/files/acme-primitives.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import datetime +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography import x509 +from acme import client, messages, crypto_util, challenges +from acme.errors import ConflictError, ValidationError +import josepy +import click + +@click.group() +def cli(): + pass + +@cli.command(name="extract_domains") +@click.argument('csr_file', type=click.File('rb')) +def extract_domains(csr_file): + csr = x509.load_pem_x509_csr(csr_file.read(), default_backend()) + san_ext = csr.extensions.get_extension_for_oid(x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + sans = san_ext.value.get_values_for_type(x509.DNSName) + print(' '.join(sans)) + +@cli.command(name="remaining_days") +@click.argument('crt_file', type=click.File('rb')) +def remaining_days(crt_file): + crt = x509.load_pem_x509_certificate(crt_file.read(), default_backend()) + print((crt.not_valid_after - datetime.datetime.now()).days) + +@cli.command(name="get_cert") +@click.option('--directory', default="https://acme-staging-v02.api.letsencrypt.org/directory") +@click.option('--acc', required=True, type=click.File('rb')) +@click.option('--csr', required=True, type=click.File('rb')) +@click.argument('challenge_hook', nargs=-1) +def get_cert(directory, acc, csr, challenge_hook): + acc_key = serialization.load_pem_private_key(acc.read(), None, default_backend()) + jose = josepy.JWKRSA(key=acc_key) + net = client.ClientNetwork(jose, user_agent="acme-primitives") + directory = messages.Directory.from_json(net.get(directory).json()) + client_acme = client.ClientV2(directory, net=net) + try: + regr = client_acme.new_account(messages.NewRegistration.from_data(terms_of_service_agreed=True)) + except ConflictError as e: + regr = client_acme.update_registration(messages.RegistrationResource(body=messages.Registration.from_data(terms_of_service_agreed=True))) + csr_data = csr.read() + orderr = client_acme.new_order(csr_data) + for authz in orderr.authorizations: + domain = authz.body.identifier.value + for i in authz.body.challenges: + if isinstance(i.chall, challenges.DNS01): + response, validation = i.response_and_validation(jose) + subprocess.run([*challenge_hook, i.validation_domain_name(domain), validation], env=os.environ.copy(), check=True) + client_acme.answer_challenge(i, response) + try: + finalized_orderr = client_acme.poll_and_finalize(orderr) + except ValidationError as e: + for auth in e.failed_authzrs: + msg += '\n Authorization for identifier %s failed.' % ( + auth.body.identifier) + msg += '\n Here are the challenges that were not fulfilled:' + for challenge in auth.body.challenges: + msg += \ + '\n Challenge Type: %s' \ + '\n Error information: ' \ + '\n Type: %s' \ + '\n Details: %s \n\n' % ( + challenge.chall.typ, + challenge.error.typ if challenge.error else '', + challenge.error.detail if challenge.error else '', + ) + print(msg, file=sys.stderr, flush=True) + sys.exit(1) + print(finalized_orderr.fullchain_pem) + +if __name__ == '__main__': + cli() diff --git a/files/letsencrypt_deploy_challenge.sh b/files/letsencrypt_deploy_challenge.sh index 4280872..8c8fde0 100755 --- a/files/letsencrypt_deploy_challenge.sh +++ b/files/letsencrypt_deploy_challenge.sh @@ -1,5 +1,13 @@ #!/bin/bash +set -euo pipefail + +CHALLENGE_RECORD="$1" +CHALLENGE_VALUE="$2" + +logger -t letsencrypt "deploying challenge for record ${CHALLENGE_RECORD} with value ${CHALLENGE_VALUE}" for i in $LETSENCRYPT_CHALLENGE_SERVERS; do - ssh -i /etc/letsencrypt/renewkey -o "StrictHostKeyChecking no" letsencrypt@$i $(< $LETSENCRYPT_TOKEN ) $1 $2 + logger -t letsencrypt "deploying to ${i}" + { ssh -i /etc/letsencrypt/renewkey -o "StrictHostKeyChecking no" letsencrypt@$i "$(cat "$LETSENCRYPT_TOKEN")" "${CHALLENGE_RECORD}" "${CHALLENGE_VALUE}" | logger -t letsencrypt -e; } || + { logger -t letsencrypt "deploying failed with exit code $?"; exit 1; } done diff --git a/files/letsencrypt_renew.sh b/files/letsencrypt_renew.sh index 25c53e0..fb118b3 100755 --- a/files/letsencrypt_renew.sh +++ b/files/letsencrypt_renew.sh @@ -3,12 +3,14 @@ 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 +logger -t letsencrypt "Checking certificate ${LETSENCRYPT_CRT}" +daysleft=$(/usr/local/bin/acme-primitives.py remaining_days "${LETSENCRYPT_CRT}" || echo "0") 2>/dev/null +[ "$daysleft" -lt "$LETSENCRYPT_REMAININGDAYS" ] || { logger -t letsencrypt "Cert has ${LETSENCRYPT_REMAININGDAYS} days remaining, not renewing" 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 +cd "${folder}" +logger -t letsencrypt "Renewing certificate" +/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 @@ -25,6 +27,10 @@ mv full.pem "$LETSENCRYPT_FULL" cd rm -r "$folder" -for i in $LETSENCRYPT_SERVICES; do - /bin/systemctl "$i" restart +logger -t letsencrypt "Success, restarting services ( ${LETSENCRYPT_SERVICES} )..." + +for i in ${LETSENCRYPT_SERVICES}; do + /bin/systemctl "${i}" restart done + +logger -t letsencrypt "done" diff --git a/tasks/letsencrypt_cert.yml b/tasks/letsencrypt_cert.yml index f44fe73..eb75355 100644 --- a/tasks/letsencrypt_cert.yml +++ b/tasks/letsencrypt_cert.yml @@ -36,23 +36,26 @@ src: "/etc/letsencrypt/cert_{{ certname }}.token" register: tokenfile - name: add renew ssh key to backend server - delegate_to: "{{ item }}" + delegate_to: "{{ challengeserver }}" loop: "{{ cert_backend.challengeserver }}" + loop_control: + loop_var: 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 }}" + delegate_to: "{{ serverchallengepair.0 }}" loop: "{{ cert_backend.challengeserver|product(challenge.challenge_data.keys()|list)|list }}" + loop_control: + loop_var: serverchallengepair command: argv: - "/usr/local/bin/pdns.py" - "add_token" - - "--" - "{{ tokenfile.content | b64decode }}" - - "{{ challenge.challenge_data[item.1]['dns-01'].record }}" + - "{{ challenge.challenge_data[serverchallengepair.1]['dns-01'].record }}" - name: create cert renew config template: src: letsencrypt_renew_config.j2 @@ -71,23 +74,27 @@ when: - challenge is changed - cert_backend.challenge == "dns-01" - delegate_to: "{{ item.0 }}" + delegate_to: "{{ serverchallengepair.0 }}" loop: "{{ cert_backend.challengeserver|product(challenge.challenge_data.keys()|list)|list }}" + loop_control: + loop_var: serverchallengepair 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 }}" + - "{{ challenge.challenge_data[serverchallengepair.1]['dns-01'].record }}" + - "{{ challenge.challenge_data[serverchallengepair.1]['dns-01'].resource_value }}" - name: "setup challenge server for {{ certname }} (manual dns challenge)" when: - challenge is changed - cert_backend.challenge == "dns-01-manual" loop: "{{ challenge.challenge_data_dns|d({})|dict2items }}" + loop_control: + loop_var: challengedata debug: - msg: "add the following dns record: '{{ item.key }}.': { TXT: {{ item.value }} }" + msg: "add the following dns record: '{{ challengedata.key }}.': { TXT: {{ challengedata.value }} }" - name: wait for challenges in dns (manual dns challenge) pause: @@ -100,11 +107,14 @@ when: - challenge is changed - cert_backend.challenge == "http-01" - delegate_to: "{{ item.0 }}" + delegate_to: "{{ serverchallengepair.0 }}" loop: "{{ cert_backend.challengeserver|product(challenge.challenge_data.keys()|list)|list }}" + loop_control: + loop_var: serverchallengepair copy: - dest: "/var/www/letsencrypt/{{ challenge.challenge_data[item.1]['http-01'].resource | basename }}" - content: "{{ challenge.challenge_data[item.1]['http-01'].resource_value }}" + dest: "/var/www/letsencrypt/{{ challenge.challenge_data[serverchallengepair.1]['http-01'].resource | basename }}" + content: "{{ challenge.challenge_data[serverchallengepair.1]['http-01'].resource_value }}" + mode: 0666 - name: "get certificate {{ certname }}" acme_certificate: diff --git a/tasks/letsencrypt_setup.yml b/tasks/letsencrypt_setup.yml index d9a76fb..434dba0 100644 --- a/tasks/letsencrypt_setup.yml +++ b/tasks/letsencrypt_setup.yml @@ -48,9 +48,9 @@ mode: 0755 - name: copy acme primitives - get_url: + copy: + src: acme-primitives.py 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"