From 59ce72229c1e27d61d2721f0c958aa7de2514133 Mon Sep 17 00:00:00 2001 From: Someone Date: Mon, 5 Aug 2024 19:34:51 +0200 Subject: [PATCH] [roles/server/letsencrypt-bot] install dehydrated bot - TODO: move to nginx? --- .../letsencrypt-bot/files/default/config | 14 ++ .../letsencrypt-bot/files/default/config-rsa | 12 ++ .../letsencrypt-bot/files/default/domains.txt | 8 + .../letsencrypt-bot/files/default/hook.sh | 196 ++++++++++++++++++ .../files/default/letsencrypt-dehydrated.cron | 11 + roles/server/letsencrypt-bot/tasks/main.yml | 151 ++++++++++++++ 6 files changed, 392 insertions(+) create mode 100644 roles/server/letsencrypt-bot/files/default/config create mode 100644 roles/server/letsencrypt-bot/files/default/config-rsa create mode 100644 roles/server/letsencrypt-bot/files/default/domains.txt create mode 100644 roles/server/letsencrypt-bot/files/default/hook.sh create mode 100644 roles/server/letsencrypt-bot/files/default/letsencrypt-dehydrated.cron create mode 100644 roles/server/letsencrypt-bot/tasks/main.yml diff --git a/roles/server/letsencrypt-bot/files/default/config b/roles/server/letsencrypt-bot/files/default/config new file mode 100644 index 0000000..808b330 --- /dev/null +++ b/roles/server/letsencrypt-bot/files/default/config @@ -0,0 +1,14 @@ +# +################################################ +### Managed by someone's ansible provisioner ### +################################################ +# Part of: https://git.somenet.org/root/pub/somesible.git +# 2017-2024 by someone +# + +CONFIG_D="" +BASEDIR="/var/lib/letsencrypt/" +WELLKNOWN="/var/www/html/dehydrated/" +CERTDIR="/etc/ssl/letsencrypt/" +DOMAINS_TXT="/etc/dehydrated/domains.txt" +HOOK="/etc/dehydrated/hook.sh" diff --git a/roles/server/letsencrypt-bot/files/default/config-rsa b/roles/server/letsencrypt-bot/files/default/config-rsa new file mode 100644 index 0000000..3833f68 --- /dev/null +++ b/roles/server/letsencrypt-bot/files/default/config-rsa @@ -0,0 +1,12 @@ +# +################################################ +### Managed by someone's ansible provisioner ### +################################################ +# Part of: https://git.somenet.org/root/pub/somesible.git +# 2017-2024 by someone +# + +. /etc/dehydrated/config + +KEY_ALGO=rsa +CERTDIR="/etc/ssl/letsencrypt-rsa/" diff --git a/roles/server/letsencrypt-bot/files/default/domains.txt b/roles/server/letsencrypt-bot/files/default/domains.txt new file mode 100644 index 0000000..a73bb65 --- /dev/null +++ b/roles/server/letsencrypt-bot/files/default/domains.txt @@ -0,0 +1,8 @@ +# +################################################ +### Managed by someone's ansible provisioner ### +################################################ +# Part of: https://git.somenet.org/root/pub/somesible.git +# 2017-2024 by someone +# + diff --git a/roles/server/letsencrypt-bot/files/default/hook.sh b/roles/server/letsencrypt-bot/files/default/hook.sh new file mode 100644 index 0000000..7acf60d --- /dev/null +++ b/roles/server/letsencrypt-bot/files/default/hook.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +################################################ +### Managed by someone's ansible provisioner ### +################################################ +# Part of: https://git.somenet.org/root/pub/somesible.git +# 2017-2024 by someone +# + +deploy_challenge() { + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + # This hook is called once for every domain that needs to be + # validated, including any alternative names you may have listed. + # + # Parameters: + # - DOMAIN + # The domain name (CN or subject alternative name) being + # validated. + # - TOKEN_FILENAME + # The name of the file containing the token to be served for HTTP + # validation. Should be served by your web server as + # /.well-known/acme-challenge/${TOKEN_FILENAME}. + # - TOKEN_VALUE + # The token value that needs to be served for validation. For DNS + # validation, this is what you want to put in the _acme-challenge + # TXT record. For HTTP validation it is the value that is expected + # be found in the $TOKEN_FILENAME file. + + # Simple example: Use nsupdate with local named + # printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key +} + +clean_challenge() { + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + # This hook is called after attempting to validate each domain, + # whether or not validation was successful. Here you can delete + # files or DNS records that are no longer needed. + # + # The parameters are the same as for deploy_challenge. + + # Simple example: Use nsupdate with local named + # printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key +} + +deploy_cert() { + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" + + # This hook is called once for each certificate that has been + # produced. Here you might, for instance, copy your new certificates + # to service-specific locations and reload the service. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - KEYFILE + # The path of the file containing the private key. + # - CERTFILE + # The path of the file containing the signed certificate. + # - FULLCHAINFILE + # The path of the file containing the full certificate chain. + # - CHAINFILE + # The path of the file containing the intermediate certificate(s). + # - TIMESTAMP + # Timestamp when the specified certificate was created. + + # Simple example: Copy file to nginx config + # cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl + # systemctl reload nginx +} + +deploy_ocsp() { + local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}" + + # This hook is called once for each updated ocsp stapling file that has + # been produced. Here you might, for instance, copy your new ocsp stapling + # files to service-specific locations and reload the service. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - OCSPFILE + # The path of the ocsp stapling file + # - TIMESTAMP + # Timestamp when the specified ocsp stapling file was created. + + # Simple example: Copy file to nginx config + # cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl + # systemctl reload nginx +} + + +unchanged_cert() { + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" + + # This hook is called once for each certificate that is still + # valid and therefore wasn't reissued. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - KEYFILE + # The path of the file containing the private key. + # - CERTFILE + # The path of the file containing the signed certificate. + # - FULLCHAINFILE + # The path of the file containing the full certificate chain. + # - CHAINFILE + # The path of the file containing the intermediate certificate(s). +} + +invalid_challenge() { + local DOMAIN="${1}" RESPONSE="${2}" + + # This hook is called if the challenge response has failed, so domain + # owners can be aware and act accordingly. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - RESPONSE + # The response that the verification server returned + + # Simple example: Send mail to root + # printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root +} + +request_failure() { + local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}" + + # This hook is called when an HTTP request fails (e.g., when the ACME + # server is busy, returns an error, etc). It will be called upon any + # response code that does not start with '2'. Useful to alert admins + # about problems with requests. + # + # Parameters: + # - STATUSCODE + # The HTML status code that originated the error. + # - REASON + # The specified reason for the error. + # - REQTYPE + # The kind of request that was made (GET, POST...) + # - HEADERS + # HTTP headers returned by the CA + + # Simple example: Send mail to root + # printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root +} + +generate_csr() { + local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}" + + # This hook is called before any certificate signing operation takes place. + # It can be used to generate or fetch a certificate signing request with external + # tools. + # The output should be just the cerificate signing request formatted as PEM. + # + # Parameters: + # - DOMAIN + # The primary domain as specified in domains.txt. This does not need to + # match with the domains in the CSR, it's basically just the directory name. + # - CERTDIR + # Certificate output directory for this particular certificate. Can be used + # for storing additional files. + # - ALTNAMES + # All domain names for the current certificate as specified in domains.txt. + # Again, this doesn't need to match with the CSR, it's just there for convenience. + + # Simple example: Look for pre-generated CSRs + # if [ -e "${CERTDIR}/pre-generated.csr" ]; then + # cat "${CERTDIR}/pre-generated.csr" + # fi +} + +startup_hook() { + # This hook is called before the cron command to do some initial tasks + # (e.g. starting a webserver). + + : +} + +exit_hook() { + # This hook is called at the end of the cron command and can be used to + # do some final (cleanup or other) tasks. + chmod -R u=rwX,g=rX,o-rwx "$CERTDIR" + : +} + +HANDLER="$1"; shift +if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then + "$HANDLER" "$@" +fi diff --git a/roles/server/letsencrypt-bot/files/default/letsencrypt-dehydrated.cron b/roles/server/letsencrypt-bot/files/default/letsencrypt-dehydrated.cron new file mode 100644 index 0000000..e415ab3 --- /dev/null +++ b/roles/server/letsencrypt-bot/files/default/letsencrypt-dehydrated.cron @@ -0,0 +1,11 @@ +# +################################################ +### Managed by someone's ansible provisioner ### +################################################ +# Part of: https://git.somenet.org/root/pub/somesible.git +# 2017-2024 by someone +# + +MAILTO=root + +0 21 * * 0 letsencrypt /usr/bin/dehydrated -c -g; /usr/bin/dehydrated -gcd; /usr/bin/dehydrated -f /etc/dehydrated/config-rsa -c -g; /usr/bin/dehydrated -f /etc/dehydrated/config-rsa -gcd diff --git a/roles/server/letsencrypt-bot/tasks/main.yml b/roles/server/letsencrypt-bot/tasks/main.yml new file mode 100644 index 0000000..5951035 --- /dev/null +++ b/roles/server/letsencrypt-bot/tasks/main.yml @@ -0,0 +1,151 @@ +##################################### +### someone's ansible provisioner ### +##################################### +# Part of: https://git.somenet.org/root/pub/somesible.git +# 2017-2024 by someone +# +--- +- name: install letsencrypt-bot + apt: + pkg: + - ssl-cert + - dehydrated + state: present + policy_rc_d: 101 + tags: "online" + ignore_errors: "{{ignore_online_errors | bool}}" + + +- name: create letsencrypt user + user: + name: "letsencrypt" + home: "/var/lib/letsencrypt" + shell: "/bin/bash" + createhome: no + system: yes + state: present + + +- name: create letsencrypt user's homedir + file: + path: "/var/lib/letsencrypt" + state: directory + mode: 0750 + owner: "letsencrypt" + group: "letsencrypt" + + +- name: create letsencrypt challenge dir + file: + path: "/var/www/html/dehydrated" + state: directory + mode: 0750 + owner: "letsencrypt" + group: "www-data" + + +- name: create letsencrypt cert dir + file: + path: "/etc/ssl/letsencrypt" + state: directory + mode: 0750 + owner: "letsencrypt" + group: "ssl-cert" + + +- name: create letsencrypt cert-rsa dir + file: + path: "/etc/ssl/letsencrypt-rsa" + state: directory + mode: 0750 + owner: "letsencrypt" + group: "ssl-cert" + + +- name: fix dehydrated dir permissions + file: + path: "/etc/dehydrated" + state: directory + mode: 0750 + owner: "letsencrypt" + group: "letsencrypt" + + +- name: copy config + copy: + src: "{{item}}" + dest: "/etc/dehydrated/config" + mode: 0640 + owner: "letsencrypt" + group: "letsencrypt" + with_first_found: + - "{{lookup('env','PWD')}}/host_files/{{inventory_hostname}}/{{role_name}}/config" + - "{{lookup('env','PWD')}}/group_files/{{group_files_group}}/{{role_name}}/config" + - "{{lookup('env','PWD')}}/group_files/all/{{role_name}}/config" + - "default/config" + + +- name: copy config-rsa + copy: + src: "{{item}}" + dest: "/etc/dehydrated/config-rsa" + mode: 0640 + owner: "letsencrypt" + group: "letsencrypt" + with_first_found: + - "{{lookup('env','PWD')}}/host_files/{{inventory_hostname}}/{{role_name}}/config-rsa" + - "{{lookup('env','PWD')}}/group_files/{{group_files_group}}/{{role_name}}/config-rsa" + - "{{lookup('env','PWD')}}/group_files/all/{{role_name}}/config-rsa" + - "default/config-rsa" + + +- name: copy hook.sh + copy: + src: "{{item}}" + dest: "/etc/dehydrated/hook.sh" + mode: 0750 + owner: "letsencrypt" + group: "letsencrypt" + with_first_found: + - "{{lookup('env','PWD')}}/host_files/{{inventory_hostname}}/{{role_name}}/hook.sh" + - "{{lookup('env','PWD')}}/group_files/{{group_files_group}}/{{role_name}}/hook.sh" + - "{{lookup('env','PWD')}}/group_files/all/{{role_name}}/hook.sh" + - "default/hook.sh" + + +- name: copy domains.txt header + copy: + src: "{{item}}" + dest: "/etc/dehydrated/domains.txt" + mode: 0640 + owner: "root" + group: "letsencrypt" + with_first_found: + - "{{lookup('env','PWD')}}/host_files/{{inventory_hostname}}/{{role_name}}/domains.txt" + - "{{lookup('env','PWD')}}/group_files/{{group_files_group}}/{{role_name}}/domains.txt" + - "{{lookup('env','PWD')}}/group_files/all/{{role_name}}/domains.txt" + - "default/domains.txt" + changed_when: False + + +- name: register with letsencrypt + command: "/usr/bin/dehydrated --register --accept-terms" + args: + creates: "/var/lib/letsencrypt/accounts/" + become: true + become_user: "letsencrypt" + tags: "online" + + +- name: copy crontab entry + copy: + src: "{{item}}" + dest: "/etc/cron.d/letsencrypt-dehydrated" + mode: 0644 + owner: "root" + group: "root" + with_first_found: + - "{{lookup('env','PWD')}}/host_files/{{inventory_hostname}}/{{role_name}}/letsencrypt-dehydrated.cron" + - "{{lookup('env','PWD')}}/group_files/{{group_files_group}}/{{role_name}}/letsencrypt-dehydrated.cron" + - "{{lookup('env','PWD')}}/group_files/all/{{role_name}}/letsencrypt-dehydrated.cron" + - "default/letsencrypt-dehydrated.cron" -- 2.43.0