%

OpenBSD: Manage DNS, DNSSEC (to automate TLSA records)

Article published the ; modified the
17 minutes to read

This article has 3522 words.
RAW source of the article:
Commit version: c5a2380

Description

Since 2018, I asked me about how to manage TLSA records, according to the DANE and DNSSEC protocols, for my DNS. (I wroted one article in french, on March 2018, about creating TLSA records in shell or PHP languages; if you read french, see: DNS: Générer un enregistrement TLSA…)

Someone prefers using knot, as package on OpenBSD, because they thing that’s complex to manage. Here, we’ll see some tips to manage this, an automated way, in shell, on OpenBSD.

My DNS service run since 4 years, under OpenBSD native tool named nsd. I manage DNSSEC with ldns tools, a package into ports. In the facts, I use ldnscript tool to create all needed keys and manage DNSSEC.

⇒ Starting Juin 2022, I decided to switch from RSA to use ECDSA.

Before going any further in this direction, let’s move on to the installation of the necessary prerequisites necessary:

Installation

ldnscript

To remind myself how to do this, I made myself the following little memo:

  1. Install ldns-utils: $ doas pkg_add ldns-utils git

  2. Download and install ldnscripts

$ cd /usr/local/src/
$ doas mkdir ldnscripts
$ doas chown $USER ldnscripts
$ git clone https://framagit.org/22decembre/ldnscripts.git
$ cd ldnscripts
$ doas make install
  1. Configure /etc/ns/ldnscript.conf
; SHA256 est largement suffisant et sécuritaire
ALG=ECDSAP256SHA256 
NSEC3_ALG=SHA-256
  1. Init the domain name: $ doas ldnscript init domain.tld

  2. You need to create symbolic link from /usr/bin/dig to /usr/sbin/dig: $ doas ln -sf /usr/bin/dig /usr/sbin/dig (without this, the ldnscript tool will not be able to find the binary and therefore will refuse to run, displaying error message).

Configuration

/etc/monthly.local

Into the /etc/monthly.local script, I include this shellcode to create the needed rollover keys:

### ldnscript
printf '%s\n' "⇒ ldnscript rollover"
/usr/local/sbin/ldnscript rollover all

Let’s Encrypt

Next, the shell script queries the Let’s Encrypt services to know if it’s necessary to renew the certificates for the domain names I use.

Normally, one would use the native acme client on OpenBSD, but I had some problems, I decided to switch to certbot.

To renew, on shell, that’s enough: /usr/local/bin/certbot renew --pre-hook "rcctl stop nginx" --post-hook "rcctl start nginx"

(Yesss, I known; I use nginx, because I prefer… but, I you like httpd, the native webserver, replace by his name service).

In fact, this part is not directly related to the DNS(SEC) management. This is important to take carefull because during certificate renewall, it’s necessary to regenerate TLSA records. We’ll see that later…

DNS Zone Example

Here, for instance, a minimilastic DNS Zone, managed by ns server:

$TTL 1H
$ORIGIN domain.tld.
@   IN  SOA domain.tld. dns.domain.tld. (
    2022090101 ;
    3H ; refresh
    1H ; retry
    2W ; expire
    1H ; negative
)
                    
@   IN NS   ns1.domain.tld.
@   IN NS   ns2.domain.tld.

@   IN A    46.23.90.29
    IN AAAA 2a03:6000:6e65:619::29

; enregistrement CAA
@    IN CAA  0 iodef "mailto:mail@domain.tld"
@    IN CAA  0 issue "letsencrypt.org"
@    IN CAA  0 issuewild "letsencrypt.org"

www IN A    46.23.90.29
    IN AAAA 2a03:6000:6e65:619::29

; TLSA
_443._tcp.domain.tld. IN TLSA 3 1 2 5c8fdd68178ce4cd8d88bd90b82a96df41674d555340b88283c24a0b3416aa375144cd6c16a58160ba3b168e59f5003bff656ce67cb24931b462fe4910bd62f5

shell

Generate TLSA Record

On shellcode, managing a TLSA record is simple — need to use openssl: openssl x509 -noout -pubkey -in "${cert}" | openssl "${command}" -pubin -outform der 2>/dev/null | "${algo}" | tr "a-z" "A-Z"

Explains :

⇒ The above command generate a tlsa_cert_associated variable, where:

  • $cert: the absolute pathname for the server TLS cert, on the filesystem, about one domain name.
  • $command: the command name used by openssl, either rsa or ec (reciprocally for RSA or ECDSA encryption records).
  • $algo: sha256, or sha512 tool to use.

⇒ Writing the TLSA record is also simple: tlsa_record="_${tls_port}._${tls_proto}.${domain}. IN TLSA ${tlsa_usage} ${tlsa_selector} ${tlsa_method} ${tlsa_cert_associated}"

  • ${tls_port}: port number of the webservice; by default 443
  • ${tls_proto}: protocol name used; by default tls
  • ${domain}: the domain name
  • ${tlsa_usage}: the number according the DANE-EE constraint; 3 is recommended by Let’s Encrypt,
  • ${tlsa_selector}: the number according to the SPKI selector; 1 is recommended by Let’s Encrypt,
  • ${tlsa_method}: the method segun the choosed algorythm; by default: SHA256, which offers currently a secure level of encryption, egually recommended by Let’s Encrypt.
  • finishing with the previous tlsa_cert_associated variable.

Check TLSA Record

Check a TLSA record is more complex; you need to:

  1. 1/ query the DNS server to known the actual TLSA record, with dig tool, for example.
  2. 2/ compare with the openssl output; OpenSSL requesting the cert to the webserver, linked to the domain name.

  • queries the DNS server, like:
tlsa="$(dig TLSA _443._tcp."${domain}" +short)"
d_tlsa="$(echo "${tlsa}" | awk '{ for(i=4;i<=NF;++i) printf "%s", tolower($i); print "" }')"
  • use openssl to request TLS certificate used on the webserver:
tlsa="$(echo | openssl s_client -servername "${domain}" -showcerts -connect "${domain}":443 2>/dev/null | openssl x509 -noout -pubkey | openssl pkey -outform der -pubin 2>/dev/null | openssl dgst -"${algo}" 2>/dev/null )"
o_tlsa="$(echo "${tlsa}" | awk -F'=' '{ print $2 }' | tr -d ' ')"

Finally, it enoughs to compare both shell variables, d_tlsa and o_tlsa. In normal time, that should be case. Except in case of renewal certificate, which will require the renewal of the TLSA record in the domain name zone, on the DNS server.

NOTE: if this is not done, a DNS server query on the DNSSEC protocol will generate an error since the TLSA record will not match a freshly used, or renewed TLS cert, which will result in that consequence: access to the server, linked to the target domain name, will be impossible. It will be necessary to generate a new TLSA record corresponding to the renewal of the TLS certificate, then egual to regenerate the signature of the DNSSEC records, for the DNS zone of the target domain.

Shell Scripts

Note: Put the tlsa.sh, dns.conf, dns.ksh shell scripts on your home. If you change their location on the filesystem, think to modify your monthly local.

monthly.local

Here, a monthly.local:

#!/bin/sh

### ldnscript
printf '%s\n' "⇒ ldnscript rollover"
/usr/local/sbin/ldnscript rollover all

### renew ssl by certbot
printf '%s\n' "⇒ renew letsencrypt certs"
/usr/local/bin/certbot renew --pre-hook "rcctl stop nginx" --post-hook "rcctl start nginx"

### check tlsa records for domain; only for tcp:443
for domain in "sub.domain.tld" "domain.tld" "www.domain.tld" "sub.domain2.tld" "domain2.tld" "www.domain2.tld"; do
	printf '%s\n' "⇒ Test TLSA for ${domain}"
	/home/-your-user-/dns-tools/tlsa.sh "${domain}"
done

()

tlsa.sh

Here, the tlsa.sh script:

#!/bin/sh
set -e
#set -x

########################################################################
#
# Author: Stéphane HUC
# mail: devs@stephane-huc.net
# gpg:fingerprint: CE2C CF7C AB68 0329 0D20  5F49 6135 D440 4D44 BD58
#
# License: BSD Simplified
#
# Github: 
#
# Date: 2022/07/01 06:45
#
########################################################################
#
# Purpose: tool  to test TLSA record
#  - for the geek: DANE-TLSA...
#
# Needed tools: dig, openssl
#
# OS: Tested on OpenBSD, Devuan
#
########################################################################
###
##
# see: https://www.bortzmeyer.org/monitor-dane.html
##
###
########################################################################

ROOT="$(dirname "$(readlink -f -- "$0")")"

. "${ROOT}/dns.conf"

dir_admin="/home/-your-user-/dns-tools"
domain="$1"
### DO NOT TOUCH!
d_tlsa=''	# TLSA record by dig
o_tlsa=''	# TLSA record by openssl
tlsa_record=''	# TLSA record 
tlsa_method=2

########################################################################
####
##
#   All needed functions! DO NOT TOUCH-IT!
##
###
########################################################################

byebye() {

    mssg "KO" "Script stop here!"
    mssg "KO" "Please, search to understand reasons."
    exit 1

}

check_uid() {

    if [ "$(id -u)" -ne 0 ]; then

        mssg "KO" "ERROR: Script not launch with rights admin!"
        byebye

    fi

}

_dig() {
	
	tlsa="$(dig TLSA _443._tcp."${domain}" +short)"
	d_tlsa="$(echo $tlsa | awk '{ for(i=4;i<=NF;++i) printf "%s", tolower($i); print "" }')"
	
}

_openssl() {
	
	tlsa="$(echo | openssl s_client -servername "${domain}" -showcerts -connect "${domain}":443 2>/dev/null | openssl x509 -noout -pubkey | openssl pkey -outform der -pubin 2>/dev/null | openssl dgst -"${algo}" 2>/dev/null )"
	o_tlsa="$(echo $tlsa | awk -F'=' '{ print $2 }' | tr -d ' ')"
	
}

mssg() {

    typeset statut info text
    statut="$1" info="$2"

    case "${statut}" in
        "KO") text="[ ${red}${statut}${neutral} ]    ${info}" ;;
        "OK") text="[ ${green}${statut}${neutral} ]   ${info}" ;;
        #*) mssg="${text}" ;;
    esac

    printf "%s \n" "${text}"

    unset info statut text

}

new_tlsa() {
	
	cert="/etc/letsencrypt/live/${domain}/cert.pem"
	
	case "${le_key_type}" in 
		"ecdsa")
			tlsa_cert_associated="$(openssl x509 -noout -pubkey -in "${cert}" | openssl ec -pubin -outform der 2>/dev/null | "${algo}")"
		;;
		"rsa")
			tlsa_cert_associated="$(openssl x509 -noout -pubkey -in "${cert}" | openssl rsa -pubin -outform der 2>/dev/null | "${algo}")"
		;;
	esac
	
	tlsa_record="_${tls_port}._${tls_proto}.${domain}. IN TLSA ${tlsa_usage} ${tlsa_selector} ${tlsa_method} ${tlsa_cert_associated}"
	unset tlsa_cert_associated
	
}


########################################################################
####
##
#   Execution
##
###
########################################################################

if [ -z "${domain}" ]; then printf '%s\n' "[ KO ] Script stops here; no domain!"; exit; fi

_dig
_openssl

if [ "${d_tlsa}" = "${o_tlsa}" ]; then
	mssg "OK" "Similar TLSA records! :D"
else
	mssg "KO" "There seems to be a problem with the TLSA records of the domain: ${domain}!"
	printf '%s\n' "Have you renew recently the TLS certs for the domain? If yes, change the TLSA record into the DNS zone relevent!"
	printf '%s\n%s\n' "⇒ Perhaps, the dns.sh script shell can help you. ;-)"
	
	check_uid
	
	printf '%s\n' "⇒ Display new TLSA record:"
	new_tlsa
	printf '%s\n%s\n' "Add/modify tlsa into your DNS zone for ${domain}: " "${tlsa_record}"
	
	printf '%s\n' "⇒ Modify TLSA record into the domain zone for ${domain}"
	"${dir_admin}"/dns.ksh tlsa "${domain}"
	
fi

ATTENTION: You need to modify the dir_admin variable, at the top of script!

Explains:

  • it call the dns.conf file config; see below,
  • and, if the TLSA records does not match, it call the pdksh dns.ksh script, with tlsa and targeted domain name as arguments.

dns.conf

Here, the file config — needed for both dns.ksh and tlsa.sh shell scripts:

########################################################################
#
# Author: Stéphane HUC
# mail: devs@stephane-huc.net
# gpg:fingerprint: CE2C CF7C AB68 0329 0D20  5F49 6135 D440 4D44 BD58
#
# License: BSD Simplified
#
# Github: https://framagit.org/hucste/AH.git
#
# Date: 2022/06/01 07:20
#
########################################################################
###
##
# Config file to dns.ksh script
##
###
########################################################################

### Algorithm
## values: sha256, sha512; choose-it segun TLSA Method
algo="sha256"
### SOA Serial type
## values: date, timestamp
## DNS recommandation: prefer date
SOA_serial_type="date"
### Port number
tls_port=443
### Protocols
## values: stcp, tcp, udp
tls_proto="tcp"

### TLSA 
## Lets Encrypt Recommandation; 
## 	see: https://community.letsencrypt.org/t/please-avoid-3-0-1-and-3-0-2-dane-tlsa-records-with-le-certificates/7022
## usage: Lets Encrypt recommands 3, at least 2
## values: 0 => 3; or (PKIX-TA, PKIX-EE, DANE-TA, DANE-EE; respectivly: 0 -> 3)
tlsa_usage=3
## selector: Lets Encrypt recommands 1
## values: 0 or 1; or (CERT, SPKI; respectively: O or 1)
tlsa_selector=1
## method: Lets Encrypt recommands 1
## values: 0 => 2; or (FULL, SHA256, SHA512; respectively: 0 -> 2)
# this change segun algo
tlsa_method=1

### Key Type Letsencrypt
## rsa or ecdsa
## if ecdsa, specify elliptic curve: secp256r1, secp384r1, secp512r1 (256 is enough)
le_key_type=ecdsa
le_curve=secp256r1

I personally choose to use:

  • the sha512 algorythm
  • the ecdsa to use the ec command with openssl. Of course, it’s possible to use rsa; in this case, those shell scripts will not use the $le_curve variable.

dns.ksh

This complex and large script is available to:

  • generate DNSSEC signs for the DNS zone.
  • modify TLSA records and regenerate DNSSEC signs; in this case:
    • find the SOA record
    • create a new file for the DNS zone and backup the actual
    • if the new file is available, the script will write:
      • a new SOA record
      • replace the oldier TLSA record with the new
      • try to sign the DNS zone with the DNSSEC protocol:
        • if succeeded, it destroy the oldier DNS zone file,
        • if it fails, it warns, stops the execution and this case you need to rename manually the backuped file. You can modify the debug variable to 1, and relaunch the script; it logs the differents steps. This helps to review and analyse why…

Maybe, ./dns.ksh help will show you more information.

#!/bin/ksh
set -e
#set -x

################################################################################
#
# Author: Stéphane HUC
# mail: devs@stephane-huc.net
# gpg:fingerprint: CE2C CF7C AB68 0329 0D20  5F49 6135 D440 4D44 BD58
#
# License: BSD Simplified
#
# Github: 
#
# Date: 2022/06/01 07:25
#
################################################################################
#
# Purpose: to add a TLSA Record into DNS zone, segun your cert TLS (LE)
#  - for the geek: DANE-TLSA...
#### IMPORTANT: recreate your TLSA Record after (re?)new cert...
#
# Needed tools: nsd* and ldnscript
## ldnscript is a tool to sign dns zone. (DNSSEC)
## https://framagit.org/22decembre/ldnscripts.git
#
# OS: Tested on OpenBSD
#
################################################################################

ROOT="$(dirname "$(readlink -f -- "$0")")"

. "${ROOT}/dns.conf"

################################################################################
###
##
#   DONT TOUCH THOSES VARIABLES!
##
###
################################################################################

debug=0
dir_le="/etc/letsencrypt/live"
dir_ns_cfg="/etc/ns"    # folder config ns
dir_sbin="/usr/local/sbin"
log="${ROOT}/dns-script.log"
nsd_cfg="/var/nsd/etc/nsd.conf"

timestamp="$(date +%s)"
today="$(date +"%Y%m%d")"

server="nsd"

SOA_ns=""

tlsa_record=""
set -A tlsa_records	# if X509 DNS Alternative Names > 1

set -A tlsa_method_names -- "FULL" "SHA256" "SHA512"
set -A tlsa_selector_names -- "CERT" "SPKI"
set -A tlsa_usage_names -- "PKIX-TA" "PKIX-EE" "DANE-TA" "DANE-EE"

NB_PARAMS="$#"
set -A PARAMS -- "$@"

ROOT="$(dirname "$(readlink -f -- "$0")")"

if [ -z "${PARAMS[0]}" ]; then MENU_CHOICE="help"
else
    PARAMS[0]="$(printf '%s' "${PARAMS[0]}" | tr -s "[:upper:]" "[:lower:]")"

    MENU_CHOICE=${PARAMS[0]}
fi

[ -n "${PARAMS[1]}" ] && domain="$(printf '%s' "${PARAMS[1]}" | tr -s "[:upper:]" "[:lower:]")"

################################################################################
####
##
#   All needed functions! DO NOT TOUCH-IT!
##
###
################################################################################

_add_tlsa() {
	
	check_var_algo
	check_var_soa_serial_type
	check_var_tls_port
	check_var_tls_proto
	
	check_tlsa_methods
	check_tlsa_selectors
	check_tlsa_usages
	
	get_soa_ns
	
	danefile="${zonefile}.dane"
	newzonefile="${zonefile}.${today}"
	oldzonefile="${zonefile}.${OLD_SOA_sn}"
	
    create_new_filezone
    
    if [ -f "${newzonefile}" ]; then

		write_soa_serial_number
		
		build_tlsa_record
		write_tlsa_record

		mv_new_file_zone

		if _resign; then del_old_zonefile; fi
		
	fi
	
	unset danefile newzonefile oldzonefile
}

build_needed_variables() {
	
    check_var_domain
    
    printf '%s\n' "*** Build needed variables:"

    # build cert variable if menu 'tlsa'
    if [ "${MENU_CHOICE}" = "tlsa" ]; then
        cert="${dir_le}/${domain}/cert.pem"
        
        if [ ! -f "${cert}" ]; then
			display_mssg "KO" "*** It seems cert file not exists!"
			byebye
		else
			printf '%s\n' "cert: ${cert}"
		fi
    fi

    # build zonedir and zonefile variables
    zonedir="$(awk -F '"' '/zonesdir/ { print substr($2,-1) }' "${nsd_cfg}")"
    if [ -z "${zonedir}" ]; then zonedir="/var/nsd/zones/"; fi
    printf '%s\n' "zonedir: ${zonedir}"

    #zonefile="$(awk -F '"' '/zonefile: "[a-z]*\/'"${domain}"'"/ { print substr($2, -1) }' "${nsd_cfg}")"    
    #if [ "$(printf '%s' "${zonefile}" | awk -F'/' '{ print $1}')" == "signed" ]; then
        ##zonefilesigned=$zonefile        
        #zonefile="$(find "${dir_ns_cfg}" -name "${domain}")"
        #if [ -z "${zonefile}" ]; then zonefile="$(find "${zonedir}" -name "${domain}")"; fi
        #if [ -z "${zonefile}" ]; then
            #display_mssg "KO" "ERROR: It seems zonefile for domain: '${domain}' not exists!"
            #byebye
        #fi
    #else
        #zonefile="${zonedir}${zonefile}"
    #fi
    zonefile="${dir_ns_cfg}/${domain}"
    printf '%s\n' "zonefile: ${zonefile}"
	
}

build_tlsa_record() {

	# get TLSA by reading cert pem
	case "${le_key_type}" in 
		"ecdsa")
			command="ec"
		;;
		"rsa")
			command="rsa"
		;;
	esac
	
	tlsa_cert_associated="$(openssl x509 -noout -pubkey -in "${cert}" | openssl "${command}" -pubin -outform der 2>/dev/null | "${algo}")"
    _log "TLSA Cert Associated: ${tlsa_cert_associated}"
    unset command

    if [ -z "${tlsa_cert_associated}" ]; then
        display_mssg "KO" "ERROR: TLSA Cert Associated could not generated!"
        byebye

    else
        # rebuild tlsa method segun algo choosed; possible: 0 (no match), 1 (sha256), 2 (sha512)
        case "${algo}" in
            "sha256") tlsa_method=1 ;;
            "sha512") tlsa_method=2 ;;
            *) tlsa_method=0 ;;
        esac

		if [ "${tls_port}" = "443" ] && [ "${tls_proto}" = "tcp" ]; then
			get_dns_alternative_names
			count="${#domains[@]}"
			
			if [ "${count}" -eq 1 ]; then
				set_tlsa_record			
			else
				set_tlsa_records
			fi
			
        fi
    fi

    unset tlsa_cert_associated

}

byebye() {

    display_mssg "KO" "Script stop here!"
    display_mssg "KO" "Please, search to understand reasons."
    exit 1

}

check_domain_name() {

    pattern="^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" # like RFC 1123

    if [ ${#domain} -gt 67 ]; then    # Larg domain name <= 67
        display_mssg "KO" "Error: Domain Length: ${domain}; >= 67 carachters!"
        byebye
    fi

    if printf '%s\n' "${domain}" | grep -Eio "${pattern}"; then
        display_mssg "OK" "Domain Name: ${domain} is valid!"
        sleep 1

    else
        display_mssg "KO" "Error: Bad Domain Name: ${domain}"
        byebye

    fi

    unset pattern

    }

check_tlsa_methods() {

	case "${tlsa_method}" in 
		0|"FULL") 	tlsa_method=0 ;; 
		1|"SHA256") tlsa_method=1 ;;
		2|"SHA512")	tlsa_method=2 ;;
		*)
			display_mssg "KO" "/!\  The TLSA Method: not correctly configurated!"
			byebye
		::
	esac

	_log "TLSA Method: ${tlsa_method}"
	
}

check_tlsa_selectors() {

	case "${tlsa_selector}" in 
		0|"CERT") 	tlsa_selector=0 ;; 
		1|"SPKI") 	tlsa_selector=1 ;;
		*)
			display_mssg "KO" "/!\  The TLSA Selector: not correctly configurated!"
			byebye
		;;
	esac
	_log "TLSA Selector: ${tlsa_selector}"
	
}

check_tlsa_usages() {
	
	case "${tlsa_usage}" in 
		0|"PKIX-TA") 	tlsa_usage=0 ;; 
		1|"PKIX-EE") 	tlsa_usage=1 ;;
		2|"DANE-TA")	tlsa_usage=2 ;;
		3|"DANE-EE")	tlsa_usage=3 ;;
		*)
			display_mssg "KO" "/!\  The TLSA Usage: not correctly configurated!"
			byebye
		;;
	esac
	_log "TLSA Usage: ${tlsa_usage}"
	
}

check_uid() {

    if [ "$(id -u)" -ne 0 ]; then

        display_mssg "KO" "ERROR: Script not launch with rights admin!"
        byebye

    fi

}

check_var_algo () {
	
	if [ "${algo}" != "sha256" ] && [ "${algo}" != "sha512" ]; then
			display_mssg "KO" "/!\ Algorythm: not correctly configurated!"
			byebye
	fi
	_log "Algo: ${algo}"
	
}

check_var_domain() {
	
	if [ -z "${domain}" ]; then
        display_mssg "KO" "*** It seems fault informations!"
        help
        byebye
    fi
    _log "Domain: ${domain}"
	
}

check_var_soa_serial_type() {
	
	if [ "${SOA_serial_type}" != "date" ] && [ "${SOA_serial_type}" != "timestamp" ]; then
			display_mssg "KO" "/!\ SOA Serial Type: not correctly configurated!"
			byebye
	fi
	_log "SOA Serial Type: ${SOA_serial_type}"
	
}

check_var_tls_port(){
	
	if [ "${tls_port}" -lt 0 ]; then
		display_mssg "KO" "/!\ TLS port: not correctly configurated!"
		byebye
	fi 
	_log "TLS port: ${tls_port}"
	
}

check_var_tls_proto() {
	
	if [ "${tls_proto}" != "sctp" ] && [ "${tls_proto}" != "tcp" ] && [ "${tls_proto}" != "tcp" ]; then
			display_mssg "KO" "/!\ TLS proto: not correctly configurated!"
			byebye
	fi
	_log "TLS proto: ${tls_proto}"
	
}

checkconf() {

    nsd-checkconf "${nsd_cfg}"

}

checkzone() {

    nsd-checkzone "${domain}" "${zonefile}"

}

confirm () {

    read -r response?"${1} [y|n] "
    case "${response}" in
        # 'o', 'O': Oui and not 0!
        y|Y|o|O|1)  true ;;
        *)          false ;;
    esac
    unset response

}

create_new_filezone() {
	
	cp "${zonefile}" "${newzonefile}"
	
}

del_old_zonefile() {

    if [ -f "${oldzonefile}" ]; then
        rm -fP "${oldzonefile}"
    fi

}

display_mssg() {

    typeset statut info text
    statut="$1" info="$2"

    case "${statut}" in
        "KO") text="[ ${red}${statut}${neutral} ]    ${info}" ;;
        "OK") text="[ ${green}${statut}${neutral} ]   ${info}" ;;
        #*) mssg="${text}" ;;
    esac

    printf "%s \n" "${text}"

    unset info statut text

}

get_dns_alternative_names() {
	
	# get "X509 DNS Alternative Names" characters
	domains="$(echo | openssl x509 -text -noout -in "${cert}" | awk -F ',' '/DNS:/ { for(i=1;i<NF;i++) { p=match($i,":"); print substr($i,p+1) }}')"
	# convert into array; no double-quotes, else not run!
	set -A domains -- ${domains[@]}
	printf '%s\n' "domains: ${domains[*]}"
	_log "domains: ${domains[*]}"
	
}

get_soa_ns() {
	
	OLD_SOA_sn="$(grep -A1 "SOA" "${zonefile}" | tail -n1 | awk -F ' ' '{ print $1 }')"
	_log "Old SOA Serial Number: ${OLD_SOA_sn}!"
	
}

get_soa_serial_number() {

    OLD_SOA_sn="$(printf '%s\n' "${line}" | awk -F ' ' '{ print $1 }')"
	_log "OLD SOA Serial Number: ${OLD_SOA_sn}"
	
}

help() {

    printf '%s\n' "
    $0 sign domain      # to sign a domain
    $0 tlsa domain      # to add a tlsa record into domain zone
    ----
    when use tlsa, this script will resign the domain zone...
    "

}

init_zone() {

    "${dir_sbin}"/ldnscript init "${domain}"

}

in_array() {
    
    local i=0 need="$1" IFS=" "; shift; set -A array -- $*
    count="${#array[@]}"
    while [ $i -le $count ]; do
        if [ "${array[$i]}" = "${need}" ]; then return 0; fi # true
        #let "i=$i+1"
        (( i=i+1 ))
    done
    return 1
    unset i need IFS array

}

_log() {
	
	if [ "${debug}" -eq 1 ]; then printf '%s\n' "$1" >> "${log}"; fi
	
}

main() {
		
	check_uid
	verify_need_softs
	
	build_needed_variables
	
    check_domain_name

    case "${MENU_CHOICE}" in
        "help") help ;;
        "sign") _resign ;;
        "tlsa") _add_tlsa ;;
        *)
            display_mssg "KO" "ERROR: this option ${MENU_CHOICE} is not exists!"
            help
            byebye
        ;;
    esac

}

mv_new_file_zone() {

    if [ -f "${newzonefile}" ]; then
        mv "${zonefile}" "${oldzonefile}"
        mv "${newzonefile}" "${zonefile}"
    fi

}

_resign() {

    if checkzone && checkconf; then

        display_mssg "OK" "file config nsd and zone ${domain} are good! :D"
        sign_zone

    else

        display_mssg "KO" "ERROR: it exists a problem with file config nsd or zone ${domain}"
        byebye

    fi

}

restart_server() {
    printf '%s\n' "=> Restart Server: "

    stop_server

    start_server

    status_server
}

set_soa_serial_number() {

    case "${SOA_serial_type}" in
        "date")
            SOA_date="$(printf '%s' "${OLD_SOA_sn}" | awk '{print substr($0, 0, 8)}')";
            SOA_number="$(printf '%s' "${OLD_SOA_sn}" | awk '{print substr($0, 9)}')";

            if [ "${SOA_date}" == "${today}" ]; then
                #let SOA_number=$SOA_number+1
                (( SOA_number=${SOA_number}+1 )) || true
                if [ "${SOA_number}" -lt 10 ]; then SOA_number="0${SOA_number}"; fi
                SOA_sn="${SOA_date}${SOA_number}"
            else
                SOA_sn="${today}01"
            fi
        ;;
        "timestamp")
            SOA_sn="${timestamp}"
        ;;
        *)
			display_mssg "KO" "Invalid SOA Serial Type!"
			byebye
        ;;
    esac
    _log "New SOA Serial Number: ${SOA_sn}!"
    
}

set_tlsa_record() {
	
	# build tlsa record
	tlsa_records[0]="_${tls_port}._${tls_proto}.${domains[0]}. IN TLSA ${tlsa_usage} ${tlsa_selector} ${tlsa_method} ${tlsa_cert_associated}"
	_log "TLSA Record: ${tlsa_records[0]}"
	
}

set_tlsa_records() {

	# do not use domain variable here
	i=0
	for dom in "${domains[@]}"; do 
		tlsa_records[$i]="_${tls_port}._${tls_proto}.${dom}. IN TLSA ${tlsa_usage} ${tlsa_selector} ${tlsa_method} ${tlsa_cert_associated}"
		(( i=i+1 ))
	done
	unset i dom
	_log "TLSA Records: ${tlsa_records[*]}"
	
}

sign_zone() {

    "${dir_sbin}"/ldnscript signing "${domain}"

}

start_server() {
    printf '%s\n' "Start serveur: ${server}"
    rcctl start "${server}"
    sleep 1s
}

status_server() {

    printf '%s\n' "Check serveur: ${server}"
    rcctl check "${server}"

}

stop_server() {

    printf '%s\n' "Stop serveur: ${server}"
    rcctl stop "${server}"
    sleep 1s

}

verify_need_softs() {

    if [ ! -f "${dir_sbin}/ldnscript" ]; then
        display_mssg "KO" "ERROR: ldnscript seems not install!"
        byebye
    elif [ ! -x "${dir_sbin}/ldnscript" ]; then
        display_mssg "KO" "ERROR: ldnscript is not executable!"
        byebye
    fi

}

write_soa_serial_number() {

	set_soa_serial_number
	
	if sed -i -e "s#\(.*\)${OLD_SOA_sn} \;#\1${SOA_sn} \;#" "${newzonefile}"; then
		_log "SOA serial number changed!"
	
	else 
		display_mssg "KO" "/!\  Script cant change SOA serial number!"
	fi

}

write_tlsa_record() {
	
	if [ ! -f "${danefile}" ]; then touch "${danefile}"; fi
	
	i=0
    # add tlsa records into dns zone
    for tlsa_record in "${tlsa_records[@]}"; do
		dom="${domains[$i]}"
		_log "domain: $dom"
		### /!\ ERROR with $domain /!\ 
		if sed -i -e "s#_${tls_port}._${tls_proto}.${dom}. IN TLSA\(.*\)#${tlsa_record}#" "${newzonefile}"; then
			_log "TLSA Record rewrited!"
						
		else
			printf '%s\n' "${tlsa_record}" >> "${newzonefile}"
			_log "TLSA Record added!"
		fi
		
		(( i=i+1 ))
		# add record in first line into dane file
		printf '%s\n' "${timestamp}:${tlsa_record}" >> "${danefile}"
		_log "${timestamp}:${tlsa_record}"
		unset dom
    done
    unset i tlsa_record

}

################################################################################

main

EOD

End Of Documentation

Voila; this is my process to manage my DNS zones, with DNSSEC for the TLSA records.

From a large and complex process, I can manage simply with the two useful commands:

  • sign my DNS zones by DNSSEC:
./dns.ksh sign domain
  • check the TLSA records, at any time, and if necessary, regenerate them:
./tlsa.sh domain

But if you have understood, the monthly cron takes care of it all and makes the appropriate report, so I know how it was executed.