Browse Source

+ initial

master
Alex 'AdUser' Z 3 years ago
commit
c7e44146ab
  1. 36
      config.sample
  2. 458
      hook.sh

36
config.sample

@ -0,0 +1,36 @@
########################################################
# This is the main config file for the Dehydrated #
# AlibabaCloud DNS-01 challenge Hook script #
# #
# Default values of this config are in comments #
########################################################
# requirements: openssl, curl, jq, dig
# Alicloud Public API Credentials
API_KEY_ID='' # required
API_KEY_SECRET='' # required
# Debugging (yes/no)
DEBUG="no"
# Extra options passed to the curl binary (default: <unset>)
CURL_OPTS=""
# Configure notifications
MAIL_FROM=""
MAIL_RCPT="ad_user@runbox.com"
# Configure certificate and key locations for deployment
DEPLOYED_CERTDIR=/etc/ssl/certs
DEPLOYED_KEYDIR=/etc/ssl/private
# Email Sending Method Options
# + SENDMAIL (default)
# + SMTP
MAIL_METHOD=SENDMAIL
# SMTP options
#SMTP_DOMAIN=localhost
#SMTP_SERVER=
#SMTP_PORT=25

458
hook.sh

@ -0,0 +1,458 @@
#!/usr/bin/env bash
# See these links for docs:
# - https://www.alibabacloud.com/help/en/doc-detail/29774.htm
# - https://docs-aliyun.cn-hangzhou.oss.aliyun-inc.com/pdf/alidns-intl-api-intl-en-2018-06-22.pdf
rawurlencode() {
local string="${1}"
local strlen=${#string}
local encoded=""
local pos c o
for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-_.~a-zA-Z0-9] ) o="${c}" ;;
* ) printf -v o '%%%02X' "'$c"
esac
encoded+="${o}"
done
echo "${encoded}"
}
function make_api_request {
local API_BASEURL='https://dns.aliyuncs.com/'
local TIMESTAMP=$(date --utc +%Y-%m-%dT%H:%M:%SZ)
local NONCE=$(echo "$(date +%s)+$$" | openssl dgst -md5 -binary | openssl base64)
declare -A QUERY=(
[Format]="JSON"
[Version]="2015-01-09"
[SignatureMethod]="HMAC-SHA1"
[SignatureVersion]="1.0"
[SignatureNonce]=$(rawurlencode "$NONCE")
[AccessKeyId]=$(rawurlencode "${API_KEY_ID}")
[Timestamp]=$(rawurlencode "${TIMESTAMP}")
)
# append request parameters
while (( "$#" >= 2 )); do
PARAM=${1}; shift
VALUE=${1}; shift
[ "x$PARAM" = "x" ] && break
QUERY[$PARAM]=$(rawurlencode "$VALUE")
done
# sort query array
readarray -td '' SORTED < <(printf '%s\0' "${!QUERY[@]}" | sort -z)
# gen signature
local QUERYSTR=""
for P in "${SORTED[@]}"; do
if [ ${#QUERYSTR} -gt 0 ]; then QUERYSTR="$QUERYSTR&"; fi
QUERYSTR="${QUERYSTR}${P}=${QUERY[$P]}"
done
local SIGNSTR=$(printf "GET&%%2F&%s" $(rawurlencode "$QUERYSTR"))
local SIGN=$(echo -n "$SIGNSTR" | openssl dgst -sha1 -hmac "${API_KEY_SECRET}&" -binary | openssl base64)
local URL="$API_BASEURL?${QUERYSTR}&Signature=${SIGN}"
if [ "x$DEBUG" = "xyes" ]; then
echo "QUERY: ${QUERY[@]}"
echo "SORTED: ${SORTED[@]}"
echo "SIGNSTR: $SIGNSTR"
echo "SIGN: $SIGN"
echo "NONCE: $NONCE"
echo "URL: $URL"
fi
# make request
RES=$(curl --silent --show-error $CURL_OPTS "$URL")
echo "$RES"
}
function deploy_challenge {
local FIRSTDOMAIN="${1}"
local SLD=`sed -E 's/(.*\.)*([^.]+)\..*/\2/' <<< "${FIRSTDOMAIN}"`
local TLD=`sed -E 's/.*\.([^.]+)/\1/' <<< "${FIRSTDOMAIN}"`
# add challenge records to post data
local count=0
while (( "$#" >= 3 )); do
# 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]}"`
local RES=$(make_api_request \
"Action" "AddDomainRecord" \
"DomainName" "$DOMAIN" \
"Type" "TXT" \
"RR" "_acme-challenge${CHALLENGE_HOSTNAME:+.$CHALLENGE_HOSTNAME}" \
"Value" "${TOKEN_VALUE[$count]}")
echo "$RES" | grep -q '"RecordId":'
if [[ $? != 0 ]]; then
echo "API REQ ERROR: $RES"
return 1
fi
echo "Deployed challenge for $CHALLENGE_HOSTNAME -- ${TOKEN_VALUE[$count]}"
(( count++ ))
done
local items=$count
# 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 120 ]]; then
# time has exceeded 2 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}" 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.
local FIRSTDOMAIN="${1}"
local SLD=`sed -E 's/(.*\.)*([^.]+)\..*/\2/' <<< "${FIRSTDOMAIN}"`
local TLD=`sed -E 's/.*\.([^.]+)/\1/' <<< "${FIRSTDOMAIN}"`
local RES=$(make_api_request \
"Action" "DescribeDomainRecords" \
"Domain" "$FIRSTDOMAIN" \
"SearchMode" "ADVANCED" \
"Type" "TXT")
echo "$RES" | grep -q '"RecordId":'
if [[ $? != 0 ]]; then
echo "API REQ ERROR: $RES"
return 1
fi
declare -A RECORDS
echo "$RES" | jq -r -c '.DomainRecords.Record | .[] | [ .RecordId, .Value ] | join(" ")' | while read RECID VALUE; do
RECORDS[$RECID]=$VALUE
done
# drop expired records
while (( "$#" >= 3 )); do
# 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
for RECID in "${!RECORDS[@]}"; do
[[ "x${RECORDS[$RECID]}" != "x$TOKEN_VALUE" ]] && continue
RES=$(make_api_request \
"Action" "DeleteDomainRecord" \
"RecordId" "$RECID"
)
echo "$RES" | grep -q '"RecordId":'
if [[ $? != 0 ]]; then
echo "API REQ ERROR: $RES"
return 1
fi
echo "Dropped record if $RECID -- $TOKEN_VALUE"
done # for
done # while
}
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 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.
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"
# reload services
echo " + Reloading Services"
"$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/reload.sh" "$@"
# 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
API_KEY_ID=""
API_KEY_SECRET=""
DEBUG="no"
CURL_OPTS=""
MAIL_FROM="dehydrated@example.com"
MAIL_RCPT="admin@example.com"
DEPLOYED_CERTDIR=/etc/pki/tls/certs
DEPLOYED_KEYDIR=/etc/pki/tls/private
MAIL_METHOD=SENDMAIL
SMTP_DOMAIN=localhost
SMTP_SERVER=""
SMTP_PORT=25
# 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
}
function send_error {
local DOMAIN="${1}" TODAYS_DATE=`date`
# set message content
MSG_CONTENT=$(cat << EOF
Content-Type:text/html;charset='UTF-8'
Content-Transfer-Encoding:7bit
From:SSL Certificate Renewal Script<$MAIL_FROM>
To:<$MAIL_RCPT>
Subject: New Certificate Deployment Failed - $DOMAIN - $TODAYS_DATE
<html>
<p>A new certificate for the domain, <b>${FIRSTDOMAIN}</b>, has failed.</p>
<p>DNS record did not propagate. Unable to verify domain is yours.</p>
</html>
EOF
)
IFS='
'
if [ "${MAIL_METHOD}" == "SENDMAIL" ]; then
# send notification email
cat << EOF | /usr/sbin/sendmail -t -f $MAIL_FROM
$MSG_CONTENT
EOF
else
# prepare notification email message
a=$(cat << EOF
HELO $SMTP_DOMAIN
MAIL FROM: <$MAIL_FROM>
RCPT TO: <$MAIL_RCPT>
DATA
$MSG_CONTENT
.
QUIT
.
EOF
)
IFS='
'
# send notification email
exec 1<>/dev/tcp/$SMTP_SERVER/$SMTP_PORT
declare -a b=($a)
for x in "${b[@]}"
do
echo $x
sleep .1
done
fi
}
function send_notification {
local DOMAIN="${1}" TODAYS_DATE=`date`
# set message content
MSG_CONTENT=$(cat << EOF
Content-Type:text/html;charset='UTF-8'
Content-Transfer-Encoding:7bit
From:SSL Certificate Renewal Script<$MAIL_FROM>
To:<$MAIL_RCPT>
Subject: New Certificate Deployed - $TODAYS_DATE
<html>
<p>A new certificate for the domain, <b>${DOMAIN}</b>, has been deployed.</p>
<p>Please confirm certificate is working as expected.</p>
</html>
EOF
)
IFS='
'
if [ "${MAIL_METHOD}" == "SENDMAIL" ]; then
# send notification email
cat << EOF | /usr/sbin/sendmail -t -f $MAIL_FROM
$MSG_CONTENT
EOF
else
# prepare notification email message
a=$(cat << EOF
HELO $SMTP_DOMAIN
MAIL FROM: <$MAIL_FROM>
RCPT TO: <$MAIL_RCPT>
DATA
$MSG_CONTENT
.
QUIT
.
EOF
)
IFS='
'
# send notification email
exec 1<>/dev/tcp/$SMTP_SERVER/$SMTP_PORT
declare -a b=($a)
for x in "${b[@]}"
do
echo $x
sleep .1
done
fi
}
# load config values
load_config
CURL="curl ${CURL_OPTS} -s"
if [[ "${DEBUG}" == "yes" ]]; then
CURL="$CURL -v"
fi
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 $?
Loading…
Cancel
Save