Skip to content

Instantly share code, notes, and snippets.

@Frederick888
Last active October 27, 2023 15:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Frederick888/45ed1ecea4d25967188d6bfece657e96 to your computer and use it in GitHub Desktop.
Save Frederick888/45ed1ecea4d25967188d6bfece657e96 to your computer and use it in GitHub Desktop.
Automatic DKIM key rotation with Cloudflare
#!/usr/bin/env bash
# Read https://www.linode.com/docs/email/postfix/configure-spf-and-dkim-in-postfix-on-debian-8 first!
# Run this script at the beginning of each month
# (Optional) Email notification if the script is interrupted
#function notify() {
# printf 'Refer to the logs for further info.\n' | mail -s 'DKIM rotation process was interrupted' 'admin@example.com'
#}
#
#trap notify SIGHUP SIGINT SIGTERM
cf_prefix="https://api.cloudflare.com/client/v4"
cf_email="admin@example.com"
cf_api_key="my-cloudflare-api-key"
config_dir="/etc/opendkim"
declare -a cf_headers=(
"-H" "X-Auth-Email: $cf_email"
"-H" "X-Auth-Key: $cf_api_key"
"-H" "Content-Type: application/json"
)
declare -a domains=(
"example.com"
"instance.com"
)
declare -A zone_list=()
declare -A record_list=()
function get_zones() {
# $1: print results or not
for domain in ${domains[@]}; do
response=$(curl "${cf_headers[@]}" -sL "$cf_prefix/zones?name=$domain&status=active&match=all")
zone_id=$(jq -r '.result[0].id' <<< "$response")
zone_list["$domain"]="$zone_id"
if [[ "$1" -eq 1 ]]; then
printf 'Domain %s ID:\t%s\n' "$domain" "$zone_id"
fi
done
}
function get_txt_records() {
# $1: domain
record_list=()
zone_id="${zone_list[$1]}"
response=$(curl "${cf_headers[@]}" -sL "$cf_prefix/zones/$zone_id/dns_records?type=TXT&match=all")
while read line; do
name=$(sed "s/^\([^\t]*\)\t\(.*\)$/\1/" <<< "$line")
record_id=$(sed "s/^\([^\t]*\)\t\(.*\)$/\2/" <<< "$line")
record_list["$name"]="$record_id"
done < <(jq -r '.result[] | .name + "\t" + .id' <<< "$response")
}
function delete_record() {
# $1: domain, $2: record name
response=$(curl -X DELETE "${cf_headers[@]}" -sL "$cf_prefix/zones/${zone_list[$1]}/dns_records/${record_list[$2]}")
if [[ $? -ne 0 ]]; then
return 1
fi
if [[ $(jq -r '.success' <<< "$response") != "true" ]]; then
return 1
fi
return 0
}
function create_record() {
# $1: domain, $2: selector
content=$( (tr -d $'\n' | sed 's/^.*"\(v=DKIM.*\)".*$/\1/' | tr -d '"' | tr -d $'\t' | tr -d ' ' | sed 's/;/; /g' | sed 's/rsa-sha256/sha256/') < "$config_dir/keys/$1-$2.txt" )
data=$(jq -r ".name = \"$2._domainkey.$1\" | .content = \"$content\" | .type = \"TXT\"" <<< '{}')
curl -X POST "${cf_headers[@]}" -d "$data" -sL "$cf_prefix/zones/${zone_list[$1]}/dns_records" > /dev/null
if [[ $? -ne 0 ]]; then
return 1
fi
for i in $(seq 1 10); do
sleep 60
opendkim-testkey -d "$1" -s "$2" -k "$config_dir/keys/$1-$2.private" > /dev/null
if [[ $? -eq 0 ]]; then
return 0
fi
done
return 1
}
function generate_dkim_keys() {
# $1: domain, $2: selector
if [[ -f "$config_dir/keys/$1-$2.private" ]]; then
return 2
fi
opendkim-genkey -b 2048 -h rsa-sha256 -r -s "$2" -d "$1" -D "$config_dir/keys" 2>&1 > /dev/null
if [[ $? -ne 0 ]]; then
return 1
else
mv "$config_dir/keys/$2.private" "$config_dir/keys/$1-$2.private"
chown opendkim:opendkim "$_"
mv "$config_dir/keys/$2.txt" "$config_dir/keys/$1-$2.txt"
chown opendkim:opendkim "$_"
return 0
fi
}
function update_key_table() {
# $1: domain, $2: selector
sed "/$domain/d" -i "$config_dir/key.table"
printf '%s\t%s:%s:%s/keys/%s.private\n' "$domain" "$domain" "$selector" "$config_dir" "$domain" >> "$config_dir/key.table"
}
function diff_months() {
months1=$(( 10#${1:0:4} * 12 + 10#${1: -2} ))
months2=$(( 10#${2:0:4} * 12 + 10#${2: -2} ))
result=$(( $months1 - $months2 ))
if [[ $result -le 0 ]]; then
result=$(( - $result ))
fi
printf "$result"
}
function main() {
selector=$(date +%Y%m)
get_zones
for domain in ${domains[@]}; do
get_txt_records "$domain"
declare -a to_delete=()
for name in "${!record_list[@]}"; do
if [[ "$name" == *"._domainkey.$domain" ]] && [[ "$name" != "_adsp._domainkey.$domain" ]] && [[ $(diff_months "${name:0:6}" "$selector") -gt 3 ]]; then
to_delete+=("$name")
fi
done
if [[ ${#to_delete[@]} -gt 0 ]]; then
printf 'Records to delete for %s:\n' "$domain"
for name in ${to_delete[@]}; do
printf '\t%s\t...\t' "$name"
delete_record "$domain" "$name"
[[ $? -eq 0 ]] && printf 'deleted\n' || printf 'failed to delete\n'
done
fi
generate_dkim_keys "$domain" "$selector"
case $? in
2)
printf 'DKIM key %s for %s already exists\n' "$selector" "$domain"
;;
1)
printf 'Failed to generate DKIM key %s for %s\n' "$selector" "$domain"
;;
0)
create_record "$domain" "$selector"
if [[ $? -ne 0 ]]; then
printf 'Failed to create DKIM record %s for %s\n' "$selector" "$domain"
else
sleep 60
ln -sf "$config_dir/keys/$domain-$selector.private" "$config_dir/keys/$domain.private"
chown -h opendkim:opendkim "$_"
update_key_table "$domain" "$selector"
systemctl reload opendkim.service
printf 'DKIM record %s for %s created\n' "$selector" "$domain"
fi
;;
esac
done
}
if [[ "$USER" != "root" ]]; then
printf 'Must run as root\n'
exit 1
else
main
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment