Alex 'AdUser' Z
3 years ago
commit
c7e44146ab
2 changed files with 494 additions and 0 deletions
@ -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 |
@ -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…
Reference in new issue