#!/usr/bin/env bash # - Adds DKIM subdomain delegation for a given domain # - # - Return (Exit) Codes: # - success: # - 0: Master zone and zone file added / Access check successfully # - 1: Master zone already exists zonefile created # - 2: Master zone and zone file already exists # - error: # - 10: Missing option for zone definition # - 15: DKIM domain not supported by this nameserver # - 16: No responsible zone found # - 17: No Nameserver found # - 20: Adding Zone definition failed # - 21: Adding Zonefile failed # - 22: Change owner for newly created zonefile failed # - 23: Reload bind configuration failed # - 99: Fatal error # - # - Example: # - bind_create_dkim_delegation.sh oopen.de script_name="$(basename $(realpath $0))" working_dir="$(dirname $(realpath $0))" base_conf_file="${working_dir}/conf/bind.conf" script_conf_file="${working_dir}/conf/${script_name%%.*}.conf" log_file="$(mktemp)" backup_date="$(date +%Y-%m-%d-%H%M)" # ---------- # Setting Defaults # ---------- DEFAULT_CONF_FILE_DIR="/etc/bind" DEFAULT_BIND_USER="bind" DEFAULT_BIND_GROUP="bind" DEFAULT_ZONE_FILE_DYNAMIC_UPDATE_DIR="/var/lib/bind" DEFAULT_ZONE_FILE_SUFFIX="zone" DEFAULT_SOA_ADMIN_EMAIL="domreg@oopen.de" DEFAULT_TSIG_KEY_NAME="update-dkim" # ********** # Don't make changes after this # ********** # ---------- # Base Function(s) # ---------- usage() { [[ -n "$1" ]] && error "$1" [[ $verbose ]] && echo -e " \033[1mUsage:\033[m $(basename $0) [Options] | check \033[1mDescription\033[m \033[1mReturn (Exit) Codes\033[m success: 0: Master zone and zone file added / Access check successfully 1: Master zone already exists zonefile created 2: Master zone and zone file already exists error: 10: Missing option for zone definition 15: DKIM domain not supported by this nameserver 16: No responsible zone found 17: No Nameserver found 20: Add Zone definition failed 21: Adding Zonefile failed 22: Change owner for newly created zonefile failed 23: Reload bind configuration failed 99: Fatal error \033[1mOptions\033[m -h Prints this help. -k Name of the TSIG key used for dynamical updates. -t allow-transfer for zone declaration. Possible values are ip-address(es) or existing 'acl' defininition(s). -q Runs in silent mode. \033[1mFiles\033[m Basic Configuration file: $base_conf_file Script specific Configuration file: $script_conf_file \033[1mExample:\033[m " clean_up 100 } clean_up() { # Perform program exit housekeeping rm $log_file blank_line if [[ -n "$1" ]] ; then ret_val=$1 else ret_val=99 fi exit $ret_val } echononl(){ if $verbose ; then echo X\\c > /tmp/shprompt$$ if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then echo -e -n " $*\\c" 1>&2 else echo -e -n " $*" 1>&2 fi rm /tmp/shprompt$$ fi } warn (){ if $verbose ; then echo "" echo -e " [ \033[33m\033[1mWarning\033[m ]: $*" echo "" fi } info (){ if $verbose ; then echo "" echo -e " [ \033[32m\033[1mInfo\033[m ]: $*" echo "" fi } error(){ if $verbose ; then echo "" echo -e " [ \033[31m\033[1mFehler\033[m ]: $*" echo "" fi } echo_ok() { if $verbose ; then echo -e "\033[80G[ \033[32mok\033[m ]" fi } echo_failed(){ if $verbose ; then echo -e "\033[80G[ \033[1;31mfailed\033[m ]" fi } echo_skipped() { if $verbose ; then echo -e "\033[80G[ \033[33m\033[1mskipped\033[m ]" fi } backup_dir () { dir_to_backup=$1 echononl "Backup existing directory \"$dir_to_backup\" .." if [[ -d "$dir_to_backup" ]] ; then cp -a "$dir_to_backup" "${dir_to_backup}.$backup_date" > $log_file 2>&1 if [[ $? -eq 0 ]]; then echo_ok else echo_failed error "Backup directory \"$dir_to_backup\" failed!" clean_up 99 fi else echo_failed error "Directory \"$dir_to_backup\" not found. No Backup written!" clean_up 99 fi } blank_line() { if $verbose ; then echo "" fi } trim() { local var="$*" var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters echo -n "$var" } # ---------- # - Jobhandling # ---------- # - Run 'clean_up' for signals SIGHUP SIGINT SIGTERM # - trap clean_up SIGHUP SIGINT SIGTERM # ---------- # - Some checks .. # ---------- # - Running in a terminal? # - if [[ -t 1 ]] ; then verbose=true else verbose=false fi # -Is systemd supported on this system? # - systemd_supported=false systemd=$(which systemd) systemctl=$(which systemctl) if [[ -n "$systemd" ]] && [[ -n "$systemctl" ]] ; then systemd_supported=true fi # - Print help? # - if [[ "$(trim $*)" = " -h" ]] || [[ "$(trim $*)" = " --help" ]] ; then usage fi if [[ -z "$(which basename)" ]]; then fatal 'It seems "basename" is not installed, but needed!' fi if [[ -z "$(which realpath)" ]]; then fatal 'It seems "realpath" is not installed, but needed!' fi # ========== # - Begin Main Script # ========== # ---------- # - Headline # ---------- if $terminal ; then echo "" echo -e "\033[1m----------\033[m" echo -e "\033[32m\033[1mRunning script \033[m\033[1m$script_name\033[32m .. \033[m" echo -e "\033[1m----------\033[m" fi # ---------- # - Read commandline parameter # ---------- while getopts hk:qt: opt ; do case $opt in h) usage ;; k) TSIG_KEY_NAME="$OPTARG" ;; q) verbose=true ;; t) ALLOW_TRANSFER_OPTION="$OPTARG" ;; \?) usage ;; *) ;; esac done if [[ "$1" = "check" ]]; then info "Script \033[1m$(basename $0)\033[m was successfully invoked, but its only a test." clean_up 0 fi dkim_domain=$1 # ---------- # Read Configurations from $base_conf_file # ---------- blank_line echononl "Loading default basic configurations from $(basename ${base_conf_file}).." if [[ ! -f "$base_conf_file" ]]; then echo_skipped else source "${base_conf_file}" > $log_file 2>&1 if [[ $? -eq 0 ]]; then echo_ok else echo_failed fatal "$(cat $log_file)" fi fi echononl "Loading script specific configurations from $(basename ${script_conf_file}).." if [[ ! -f "$script_conf_file" ]]; then echo_skipped else source "${script_conf_file}" > $log_file 2>&1 if [[ $? -eq 0 ]]; then echo_ok else echo_failed fatal "$(cat $log_file)" fi fi [[ -n "$CONF_FILE_DIR" ]] || CONF_FILE_DIR="$DEFAULT_CONF_FILE_DIR" [[ -n "$ZONES_DECLARATION_FILE" ]] || ZONES_DECLARATION_FILE="${CONF_FILE_DIR}/named.conf.local" [[ -n "$BIND_USER" ]] || BIND_USER="$DEFAULT_BIND_USER" [[ -n "$BIND_GROUP" ]] || BIND_GROUP="$DEFAULT_BIND_GROUP" [[ -n "$ZONE_FILE_DYNAMIC_UPDATE_DIR" ]] || ZONE_FILE_DYNAMIC_UPDATE_DIR="$DEFAULT_ZONE_FILE_DYNAMIC_UPDATE_DIR" [[ -n "$ZONE_FILE_SUFFIX" ]] || ZONE_FILE_SUFFIX="$DEFAULT_ZONE_FILE_SUFFIX" [[ -n "$SOA_ADMIN_EMAIL" ]] || SOA_ADMIN_EMAIL="$DEFAULT_SOA_ADMIN_EMAIL" [[ -n "$SOA_PRIMARY_MASTER" ]] || SOA_PRIMARY_MASTER="$(hostname --long)" [[ -n "$TSIG_KEY_NAME" ]] || TSIG_KEY_NAME="$DEFAULT_TSIG_KEY_NAME" if [[ -z "$ALLOW_TRANSFER_OPTION" ]] ; then error "Missing 'allow-update' option for zone definition .." clean_up 10 else # - Eliminate trailing ';' characters # - ALLOW_TRANSFER_OPTION="${ALLOW_TRANSFER_OPTION%"${ALLOW_TRANSFER_OPTION##*[!;]}"}" # - Add one trailing ';' character # - ALLOW_TRANSFER_OPTION="${ALLOW_TRANSFER_OPTION};" fi # - replace '@' character with '.' # - SOA_ADMIN_EMAIL="${SOA_ADMIN_EMAIL/@/.}" new_dkim_zone="_domainkey.$dkim_domain" new_zone_file="${ZONE_FILE_DYNAMIC_UPDATE_DIR}/${new_dkim_zone}.${ZONE_FILE_SUFFIX}" _zone_configuration_exists=false if $(grep -q -E "^\s*zone\s+\"$new_dkim_zone\"" $ZONES_DECLARATION_FILE 2>/dev/null) ; then info "Zone file declaration for "$new_dkim_zone" already exists." _zone_configuration_exists=true # - Determine zonefile (by reading bind configuration) # - echononl "Check if zonefile already exists (by reading bind configuration)" _found=false declare -i _number=0 regex_zone="^[[:space:]]*zone[[:space:]]+\"$new_dkim_zone\"" regex_file="^[[:space:]]*file" while IFS='' read -r line || [[ -n "$line" ]] ; do if [[ $line =~ $regex_zone ]]; then _found=true fi if $_found ; then if [[ $line =~ $regex_file ]]; then zone_file=`echo $line | awk '{print$2}'` shopt -s extglob # - Eliminate trailing ';' # - if [[ $zone_file =~ \; ]]; then zone_file=${zone_file%%*(\;)} fi # - Eliminate double quotes # - if [[ $zone_file =~ ^\" ]]; then zone_file=${zone_file##*(\")} zone_file=${zone_file%%*(\")} fi shopt -u extglob let number++ break fi fi done < $ZONES_DECLARATION_FILE echo_ok if [[ $number -eq 0 ]] ; then error "Found zone file declaration but no filename definition inside." clean_up 17 else if [[ -n "$zone_file" ]] && [[ -f "$zone_file" ]]; then info "Also zone file '$zone_file' exists" clean_up 2 fi fi fi # - Get DNS server # - echononl "Get responsible zone for domain '$dkim_domain'.." found=true zone="${dkim_domain}" dns_servers="$(dig +short $zone NS 2>/dev/null)" while [[ -z "$dns_servers" ]] ; do zone=${zone#*.} if [[ ! $zone =~ \. ]]; then found=false break fi dns_servers="$(dig +short $zone NS 2>/dev/null)" done if $found ; then echo_ok info "Found responsible zone for '${dkim_domain}': \033[37m\033[1m${zone}\033[m" echononl "Get nameservers for domain '${zone}'.." declare -i _count=0 for _dns_server in $dns_servers ; do if [[ $_count -eq 0 ]]; then _tmp_dns_server="$_dns_server" else _tmp_dns_server="$_tmp_dns_server $_dns_server" fi dns_server_arr+=("$_dns_server") if [[ ${#dns_server_arr[@]} -eq 0 ]]; then echo_failed error "Determin DNS servers for domain '$zone' failed!" clean_up 17 else echo_ok info "Found nameservers \033[37m\033[1m${_tmp_dns_server}\033[m" fi done else echo_failed error "No responsible zone for '$dkim_domain' found!" clean_up 16 fi if ! $_zone_configuration_exists ; then # - Backup ZONES_DECLARATION_FILE (/etc/bind/named.conf.local) # - echononl "Backup file '$ZONES_DECLARATION_FILE'.." cp -a "$ZONES_DECLARATION_FILE" "${ZONES_DECLARATION_FILE}.$backup_date" > $log_file 2>&1 if [[ $? -eq 0 ]]; then echo_ok else echo_failed fatal "$(cat $log_file)" fi # - Add zone definition to ZONES_DECLARATION_FILE (/etc/bind/named.conf.local) # - echononl "Add zone definition to '$ZONES_DECLARATION_FILE' .." cat <> $ZONES_DECLARATION_FILE 2> $log_file zone "${new_dkim_zone}" { type master; file "${new_zone_file}"; allow-update { key ${TSIG_KEY_NAME}. ; }; allow-transfer {$ALLOW_TRANSFER_OPTION}; }; EOF if [[ $? -eq 0 ]]; then echo_ok else echo_failed error "$(cat $log_file)" clean_up 20 fi fi # - Write new zonefile # - echononl "Add zone definition to '$ZONES_DECLARATION_FILE' .." _failed=false cat < "${new_zone_file}" 2> $log_file \$TTL 43200 @ IN SOA ${SOA_PRIMARY_MASTER}. ${SOA_ADMIN_EMAIL}. ( 0 ; serial 14400 ; refresh (4 hours) 3600 ; retry (1 hour) 604800 ; expire (1 week) 86400 ; minimum (1 day) ) EOF if [[ $? -eq 0 ]]; then for _dns_server in ${dns_server_arr[@]} ; do echo "@ IN NS $_dns_server" >> "${new_zone_file}" 2> $log_file if [[ $? -ne 0 ]] ; then _failed=true fi done else _failed=true fi if $_failed ; then echo_failed error "$(cat $log_file)" clean_up 21 else echo_ok fi # - Change owner for newly created zone file # - echononl "Change owner for newly created zone file.." chown ${BIND_USER}:$BIND_GROUP "${new_zone_file}" > $log_file 2>&1 if [[ $? -eq 0 ]]; then echo_ok else echo_failed error "$(cat $log_file)" clean_up 22 fi # - (re)Load Bind base configuration # - echononl "Reload bind configuration.." rndc reconfig > $log_file 2>&1 if [[ $? -eq 0 ]]; then echo_ok else echo_failed error "$(cat $log_file)" clean_up 23 fi clean_up 0