-
-
Save owenthewizard/3396d994963bef91b73316a894684cfb to your computer and use it in GitHub Desktop.
Automatic DKIM key rotation with CloudFlare
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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' '[email protected]' | |
#} | |
# | |
#trap notify SIGHUP SIGINT SIGTERM | |
cf_prefix="https://api.cloudflare.com/client/v4" | |
cf_email="[email protected]" | |
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=$(( ${1:0:4} * 12 + ${1: -2} )) | |
months2=$(( ${2:0:4} * 12 + ${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