From 73d864990ce5526cdb7ccfd87ccecd1b8cea4fcc Mon Sep 17 00:00:00 2001 From: wvr Date: Sun, 14 May 2023 16:01:35 -0500 Subject: [PATCH] initial --- .gitignore | 4 + config | 7 + first_run.sh | 3 + namecheap_dns_api_hook.sh | 355 ++++++++++++++++++++++++++++++++++++++ run.sh | 4 + 5 files changed, 373 insertions(+) create mode 100644 .gitignore create mode 100644 config create mode 100644 first_run.sh create mode 100644 namecheap_dns_api_hook.sh create mode 100644 run.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e517072 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +apicreds.txt +backup +certs +private diff --git a/config b/config new file mode 100644 index 0000000..027bd0f --- /dev/null +++ b/config @@ -0,0 +1,7 @@ +source "$PWD/apicreds.sh" + +DEBUG=no + +RECORDS_BACKUP=$PWD/backup +DEPLOYED_CERTDIR=$PWD/certs +DEPLOYED_KEYDIR=$PWD/private diff --git a/first_run.sh b/first_run.sh new file mode 100644 index 0000000..1076d38 --- /dev/null +++ b/first_run.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +dehydrated --register --accept-terms diff --git a/namecheap_dns_api_hook.sh b/namecheap_dns_api_hook.sh new file mode 100644 index 0000000..a87148f --- /dev/null +++ b/namecheap_dns_api_hook.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +# +# shellcheck disable=2317,2086,2178,2001,2004,2181,2068 + +function deploy_challenge { + local FIRSTDOMAIN="${1}" + local SLD=$(sed -E 's/(.*\.)*([^.]+)\..*/\2/' <<< "${FIRSTDOMAIN}") + local TLD=$(sed -E 's/.*\.([^.]+)/\1/' <<< "${FIRSTDOMAIN}") + + local POSTDATA=( "--data-urlencode" "apiuser=${apiusr}" + "--data-urlencode" "apikey=${apikey}" + "--data-urlencode" "username=${apiusr}" + "--data-urlencode" "ClientIp=${cliip}" + "--data-urlencode" "SLD=${SLD}" + "--data-urlencode" "TLD=${TLD}" + ) + local HOSTS_URI="https://api.namecheap.com/xml.response" + + local num=0 + + # get list of current records for domain + local GET_HOSTS=( "--data-urlencode" "Command=namecheap.domains.dns.getHosts" ) + local records_list=$(curl -s "${POSTDATA[@]}" "${GET_HOSTS[@]}" "${HOSTS_URI}" | sed -En 's/= 3 )); do + ((num++)) + + # DOMAIN + # The domain name (CN or subject alternative name) being validated. + DOMAIN="${1}"; shift + # 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_FILENAME="${1}"; shift + # 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. + TOKEN_VALUE[$count]="${1}"; shift + + SUB[$count]=$(sed -E "s/$SLD.$TLD//" <<< "${DOMAIN}") + CHALLENGE_HOSTNAME=$(sed -E "s/\.$//" <<< "${SUB[$count]}") + + POSTDATA+=( + "--data-urlencode" "hostname${num}=_acme-challenge${CHALLENGE_HOSTNAME:+.$CHALLENGE_HOSTNAME}" + "--data-urlencode" "recordtype${num}=TXT" + "--data-urlencode" "address${num}=${TOKEN_VALUE[$count]}" + "--data-urlencode" "ttl${num}=60" + ) + ((count++)) + done + local items=$count + + api_return=$(curl -s --request POST "${POSTDATA[@]}" "${HOSTS_URI}") + echo ${api_return} | grep -qi 'Status="OK"' + if [[ $? != 0 ]]; then + echo "ERROR: Post to API failed: ${api_return}" + return 1 + fi + + # get nameservers for domain + IFS=$'\n' read -r -d '' -a nameservers < <( dig @1.1.1.1 +short ns $SLD.$TLD && printf '\0' ) + + # wait up to 2 minutes for DNS updates to be provisioned (check at 15 second intervals) + timer=0 + count=0 + while [ $count -lt $items ]; do + # check for DNS propagation + while true; do + for ns in ${nameservers[@]}; do + dig @${ns} txt "_acme-challenge${SUB[$count]:+.${SUB[$count]%.}}.$SLD.$TLD" | grep -qe "${TOKEN_VALUE[$count]}" + if [[ $? == 0 ]]; then + break 2 + fi + done + if [[ "$timer" -ge 180 ]]; then + # time has exceeded 3 minutes + send_error $FIRSTDOMAIN + return 1 + else + echo " + DNS not propagated. Waiting 15s for record creation and replication... Total time elapsed has been $timer seconds." + ((timer+=15)) + sleep 15 + fi + done + ((count++)) + done + return 0 +} + +function clean_challenge { + local DOMAIN="${1}" + local TOKEN_FILENAME="${2}" + local 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. + + local FIRSTDOMAIN="${1}" + local SLD=$(sed -E 's/(.*\.)*([^.]+)\..*/\2/' <<< "${FIRSTDOMAIN}") + local TLD=$(sed -E 's/.*\.([^.]+)/\1/' <<< "${FIRSTDOMAIN}") + + local POSTDATA=( "--data-urlencode" "apiuser=${apiusr}" + "--data-urlencode" "apikey=${apikey}" + "--data-urlencode" "username=${apiusr}" + "--data-urlencode" "ClientIp=${cliip}" + "--data-urlencode" "SLD=${SLD}" + "--data-urlencode" "TLD=${TLD}" + ) + local HOSTS_URI="https://api.namecheap.com/xml.response" + local num=0 + + # get list of current records for domain + local GET_HOSTS=( "--data-urlencode" "Command=namecheap.domains.dns.getHosts" ) + local records_list=$(curl -s "${POSTDATA[@]}" "${GET_HOSTS[@]}" "${HOSTS_URI}" | sed -En 's//dev/null 2>&1 +} + +function 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 + # + # 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. + + echo "Deploying certificate for ${DOMAIN}..." + # copy new certificate to /etc/pki/tls/certs folder + cp "${CERTFILE}" "${DEPLOYED_CERTDIR}/${DOMAIN}.crt" + echo " + certificate copied" + + # copy new key to /etc/pki/tls/private folder + cp "${KEYFILE}" "${DEPLOYED_KEYDIR}/${DOMAIN}.key" + echo " + key copied" + + # copy new chain file which contains the intermediate certificate(s) + cp "${CHAINFILE}" "${DEPLOYED_CERTDIR}/letsencrypt-intermediate-certificates.pem" + echo " + intermediate certificate chain copied" + + # combine certificate and chain file (used by Nginx) + cat "${CERTFILE}" "${CHAINFILE}" > "${DEPLOYED_CERTDIR}/${DOMAIN}-chain.crt" + echo " + combine certificate and intermediate certificate chain" + + # send email notification + send_notification $DOMAIN + return 0 +} + +function 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). +} + +function 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 +} + +function request_failure() { + local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" + + # 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...) +} + +function startup_hook() { + # This hook is called before the cron command to do some initial tasks + # (e.g. starting a webserver). + + : +} + +function 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. + + : +} + +# Setup default config values, load configuration file +function load_config() { + # Default values + apiusr= + apikey= + RECORDS_BACKUP=${BASEDIR}/records_backup + DEPLOYED_CERTDIR=/etc/pki/tls/certs + DEPLOYED_KEYDIR=/etc/pki/tls/private + + # Check if config file exists + if [[ ! -f "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/config" ]]; then + echo "#" >&2 + echo "# !! WARNING !! No main config file found, using default config!" >&2 + echo "#" >&2 + else + . "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/config" + fi +} + +send_error() { + >&2 echo "Failed to recertify ${FIRSTDOMAIN}" +} + +send_notification() { + echo "SUCCESS for ${FIRSTDOMAIN}" +} + +# load config values +load_config + +mkdir -p "$RECORDS_BACKUP" "$DEPLOYED_CERTDIR" "$DEPLOYED_KEYDIR" + +# get this client's ip address +cliip=$(curl -s -4 https://ifconfig.co/ip) + +HANDLER="$1"; shift +if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|startup_hook|exit_hook)$ ]]; then + "$HANDLER" "$@" +fi +exit $? diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..bbc36ae --- /dev/null +++ b/run.sh @@ -0,0 +1,4 @@ +DOMAIN='*.wvr.sh' +ALIAS='wildcard.wvr.sh' + +dehydrated --force --cron --domain "$DOMAIN" --challenge dns-01 --hook "$PWD/namecheap_dns_api_hook.sh" --alias "$ALIAS"