diff --git a/Sanity/caching-resolver-dnssec/main.fmf b/Sanity/caching-resolver-dnssec/main.fmf new file mode 100644 index 0000000..9fd6da0 --- /dev/null +++ b/Sanity/caching-resolver-dnssec/main.fmf @@ -0,0 +1,31 @@ +summary: Configure bind as caching and validating iterative resolver +test: ./test.sh +framework: beakerlib +description: | + Configures named as a caching iterative resolver. Keep dnssec-validation enabled and try to + start resolution from root servers hints. + + Test signed zones are validated and include ad bit. Check unsigned zones are + missing ad bit in responses. Requires public root-servers.net and other zone nameservers are directly accessible. +recommend+: + - sed + - awk +adjust: + - recommend+: + - bind + - bind-utils + environment+: + PACKAGE: bind + when: component is not defined or component == bind + - recommend+: + - bind9.16 + - bind9.16-utils + environment+: + PACKAGE: bind9.16 + when: component == bind9.16 + - recommend+: + - bind9-next + - bind9-next-utils + environment+: + PACKAGE: bind9-next + when: component == bind9-next diff --git a/Sanity/caching-resolver-dnssec/test.sh b/Sanity/caching-resolver-dnssec/test.sh new file mode 100755 index 0000000..adaf00a --- /dev/null +++ b/Sanity/caching-resolver-dnssec/test.sh @@ -0,0 +1,371 @@ +#!/bin/bash +# vim: dict+=/usr/share/beakerlib/dictionary.vim cpt=.,w,b,u,t,i,k +. /usr/share/beakerlib/beakerlib.sh || exit 1 + +: ${CLEAN_ANCHORS:=y} +: ${NAMED_OPTIONS:=} +# How many times to check basic root check +: ${BASIC_TRIES:=3} +# How many times to check host checks +: ${HOSTS_TRIES:=3} +# On which host check pass require successful responses? +: ${HOSTS_PASSING:=2} +# Delay after named service start +: ${DELAY_START:=5} +# Delay between tests +: ${DELAY_TEST:=5} + +bu_FALLBACK_SERVERS="8.8.8.8 8.8.4.4 9.9.9.9" +bu_DELV=$(type -p delv 2>/dev/null) +bu_DIG=$(type -p dig 2>/dev/null) +# Servers which failed DNSSEC-awareness check +bu_FAILED_SERVERS="" +bu_ROOT_HINTS=/var/named/named.ca + +buGetRootServerAddresses4() { + local HINTS="${1:-$bu_ROOT_HINTS}" + # a.root-servers.net. 518400 IN A 198.41.0.4 + # A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4 + awk -v IGNORECASE=1 -- '$1 ~ /[a-m]\.root-servers\.net\./ && $3 == "A" { print $4 } $3 == "IN" && $4 == "A" { print $5 }' "$HINTS" +} + +buGetRootServerAddresses6() { + local HINTS="${1:-$bu_ROOT_HINTS}" + awk -v IGNORECASE=1 -- '$1 ~ /[a-m]\.root-servers\.net\./ && $3 == "AAAA" { print $4 } $3 == "IN" && $4 == "AAAA" { print $5 }' "$HINTS" +} + +# Get list of nameservers from resolv.conf file on standard output +buGetServersConf() { + local RESOLV_CONF=${1:-/etc/resolv.conf} + awk '$1 == "nameserver" { printf "%s%s", DL, $2; DL=" " }' "$RESOLV_CONF" +} + +# Get list of resolv-conf (like) files, which might contain useful DNS servers +# Ordered in preference, tries to avoid DNSSEC-unaware servers +buGetCandidateResolvConf() { + local -a CONF_FILES=() + systemctl is-active --quiet NetworkManager && CONF_FILES+=("/run/NetworkManager/no-stub-resolv.conf") + systemctl is-active --quiet systemd-resolved && CONF_FILES+=("/run/systemd/resolve/resolv.conf") + CONF_FILES+=("/etc/resolv.conf") + echo "${CONF_FILES[@]}" +} + +# Print list of nameservers addresses, space separated. +# No check on them is done. +buGetNameservers() { + # avoids systemd-resolved breaking dnssec, prefer communication with remote servers directly + for CONF in $(buGetCandidateResolvConf) + do + local SERVERS + # intentionally do not prefer local resolv.conf, because systemd-resolved is breaking it often + if [ -r "$CONF" ] && SERVERS=$(buGetServersConf "$CONF") && [ -n "$SERVERS" ]; then + echo "$SERVERS" + break + fi + done +} + +# Test server IP addresses give as parameters to find those, who +# are security-aware. +# Param1: space separated IP addresses of DNS servers +buCheckSecureNameservers() { + local SERVERS="$1" + bu_SECURE_SERVERS="" + + if [ -z "$bu_DELV" ] && [ -z "$bu_DIG" ]; then + rlFail "Both delv and dig from bind-utils are missing!" + return 1 + fi + for NS in ${SERVERS}; do + # use tcp to fail faster on ipv6 global route not available + if [ -n "$bu_DELV" ]; then + if $bu_DELV +tcp @$NS | grep -q '^; fully validated'; then + bu_SECURE_SERVERS+="$NS " + else + bu_FAILED_SERVERS+="$NS " + fi + elif [ -n "$bu_DIG" ]; then + + if $bu_DIG +tcp +noall +answer +dnssec @$NS | grep -qw RRSIG; then + bu_SECURE_SERVERS+="$NS " + else + bu_FAILED_SERVERS+="$NS " + fi + fi + done + [ -n "$bu_SECURE_SERVERS" ] +} + +# Try to find DNSSEC capable network provided forwarders. +# If that fails try to verify $bu_FALLBACK_SERVERS works and use them. +# If no working server is found, call rlDie to stop the test. +# Outputs found servers into bu_SECURE_SERVERS and bu_FAILED_SERVERS +# variables +buGetSecureNameservers() { + bu_FAILED_SERVERS="" + local SERVERS="" + + # avoids systemd-resolved breaking dnssec, prefer communication with remote servers directly + for CONF in $(buGetCandidateResolvConf) + do + # intentionally do not prefer local resolv.conf, because systemd-resolved is breaking it often + if [ -r "$CONF" ] && SERVERS=$(buGetServersConf "$CONF") && [ -n "$SERVERS" ]; then + rlLogDebug "Checking servers from $CONF..." + buCheckSecureNameservers "$SERVERS" && break + fi + done + if [ -z "${bu_SECURE_SERVERS}${bu_FAILED_SERVERS}" ]; then + rlDie "No nameservers obtained, tried files: $(buGetCandidateResolvConf)" + return 1 + fi + if [ -z "$bu_SECURE_SERVERS" ]; then + rlLog "Found resolv files..." + for CONF in $(buGetCandidateResolvConf) + do + rlRun -l "cat $CONF" + done + rlLog "Versions of network provided nameservers..." + for NS in ${SERVERS} + do + rlRun -l "dig @$NS txt ch version.bind" 0-255 + done + if [ -n "$bu_FALLBACK_SERVERS" ]; then + # If we have access to public DNS servers, use them instead. They are known to support DNSSEC. + buCheckSecureNameservers "$bu_FALLBACK_SERVERS" + rlLogWarning "No network provided servers (${SERVERS}) support DNSSEC! Fix the infrastructure!" + SERVERS+=" $bu_FALLBACK_SERVERS" + fi + fi + if [ -z "$bu_SECURE_SERVERS" ]; then + rlDie "No servers from ${SERVERS} support DNSSEC! Fix the infrastructure!" + return 1 + fi + + if [ -n "$bu_FAILED_SERVERS" ]; then + rlLogWarning "Servers not supporting DNSSEC: ${bu_FAILED_SERVERS}" + fi + rlLogInfo "Found security-aware servers: $bu_SECURE_SERVERS" +} + +# Create bind forwarder configuration from servers entered as parameters +buMakeForwarders() +{ + echo 'forwarders {'; + for NS in "$@" + do + printf "\t%s;\n" $NS + done + echo '}; # autogenerated' +} + +# Prints formatted used options in bind config +buPrintOptions() +{ + named-checkconf -px "$@" | sed -ne '/^options {/,/^};/ p' +} + +# Check whether option in $1 is used in options {} global block +buHasOption() +{ + local OPTION="$1" + buPrintOptions | grep -qw "^\s*${OPTION}" +} + +# Filter dig to print only desired section +# Input is dig output +buDigGetSection() +{ + local SECTION="${1:-ANSWER}" + sed -ne "/^;; ${SECTION} SECTION:/,/^$/ p" | grep -vE '^(\s*$|;.*$)' +} + +# Filter dig to print only desired value from double comment lines +# Input is dig output +buDigGetField() +{ + local FIELD="$1" + grep "^;;.*\s${FIELD}:" | sed -e "s/.*\s${FIELD}:\s*\([^;,]*\)\([;,].*\|$\)/\1/" +} + +# Filter dig input to print pseudosection contents only +buDigPseudosection() +{ + sed -ne "/^;; OPT PSEUDOSECTION:/,/^;; QUESTION SECTION/ p" | grep -vE '^;; (OPT PSEUDO|QUESTION )SECTION:' +} + +# Filter dig to print only desired value from single comment lines +# Useful for pseudosection +# Input is dig output +buDigGetField1() +{ + local FIELD="$1" + grep "^;\s\(.*\s\)\?${FIELD}:" | sed -e "s/.*\s${FIELD}:\s*\([^;,]*\)\([;,].*\|$\)/\1/" +} + +# just receive any response, no matter what status +buDig() +{ + rlRun -s "dig $*" +} + +# Ensure reply has noerror status +buDigSuccess() +{ + rlRun -s "dig $*" + local STATUS="$(buDigGetField status < $rlRun_LOG)" + rlAssertEquals "Check result was positive" "$STATUS" NOERROR +} + +# Ensure reply is signed and verified +buDigSuccessSecure() +{ + rlRun -s "dig $*" + local STATUS="$(buDigGetField status < $rlRun_LOG)" + rlAssertEquals "Check dig result was positive" "$STATUS" NOERROR + local FLAGS="$(buDigGetField flags < $rlRun_LOG)" + rlRun "echo $FLAGS | grep -w ad" 0 "Check dig result has AD bit set" +} + +# Ensure reply is positive but insecure +buDigSuccessInsecure() +{ + rlRun -s "dig $*" + local STATUS="$(buDigGetField status < $rlRun_LOG)" + rlAssertEquals "Check dig result was positive" "$STATUS" NOERROR + local FLAGS="$(buDigGetField flags < $rlRun_LOG)" + rlRun "echo $FLAGS | grep -vw ad" 0 "Check dig result has AD bit unset" +} + +# Extract KSK key id from dig +buDigKskId() +{ + dig +nocrypto +short -t dnskey "$@" | awk '$1 == 257 { sub("]", "", $7); print $7 }' +} + + +rlJournalStart + rlPhaseStartSetup + rlRun "tmp=\$(mktemp -d)" 0 "Create tmp directory" + rlRun "pushd $tmp" + rlRun "set -o pipefail" + rlRun "named -V" + rlRun "dig -v" + rlFileBackup /etc/named.conf + rlRun "named-checkconf" 0 "Test generated configuration is acccepted" + rlRun "rlServiceStop named" + if [ "$CLEAN_ANCHORS" = y ]; then + rlFileBackup --clean /var/named/dynamic/managed-keys.bind{,.jnl} + rlRun "rm -f /var/named/dynamic/managed-keys.bind{,.jnl}" + fi + rlAssertExists $bu_ROOT_HINTS + # use buCheckSecureNameservers + rlRun "HINTS4=\"$(buGetRootServerAddresses4 $bu_ROOT_HINTS)\"" + rlRun "HINTS6=\"$(buGetRootServerAddresses6 $bu_ROOT_HINTS)\"" + rlRun "HINTS4_NUM=$(echo \"$HINTS4\" | wc -l)" + rlRun "HINTS6_NUM=$(echo \"$HINTS6\" | wc -l)" + HINTS4_WORKS='' + HINTS6_WORKS='' + if [ "$NAMED_OPTIONS" = auto ]; then + # TODO: is this complication desirable? + if [ "$HINTS4_NUM" -gt 0 ] && buCheckSecureNameservers "$HINTS4"; then + SECURE_SERVERS4="$bu_SECURE_SERVERS" + FAILED_SERVERS4="$bu_FAILED_SERVERS" + HINTS4_WORKS=y + fi + if [ "$HINTS6_NUM" -gt 0 ] && buCheckSecureNameservers "$HINTS6"; then + SECURE_SERVERS6="$bu_SECURE_SERVERS" + FAILED_SERVERS6="$bu_FAILED_SERVERS" + [ -n "$FAILED_SERVERS4" ] && bu_FAILED_SERVERS+=" $FAILED_SERVERS4" + [ -n "$SECURE_SERVERS4" ] && bu_SECURE_SERVERS+=" $SECURE_SERVERS4" + HINTS6_WORKS=y + fi + if [ "$HINTS4_WORKS" = y ] && [ -z "$HINTS6_WORKS" ]; then + rlRun "NAMED_OPTIONS='-4'" + elif [ "$HINTS6_WORKS" = y ] && [ -z "$HINTS4_WORKS" ]; then + rlRun "NAMED_OPTIONS='-6'" + else + rlRun "NAMED_OPTIONS=''" + fi + else + rlRun "buCheckSecureNameservers \"$HINTS4 $HINTS6\"" && HINTS4_WORKS=y && HINTS4_WORKS=y + fi + if [ -n "$NAMED_OPTIONS" ]; then + rlFileBackup /etc/sysconfig/named + echo "OPTIONS+=\"$NAMED_OPTIONS\"" >> /etc/sysconfig/named + rlRun "grep OPTIONS /etc/sysconfig/named" + fi + SKIP_TEST='' + if [ -z "$HINTS6_WORKS" ] && [ -z "$HINTS4_WORKS" ] + then + SKIP_TEST=y + rlLogWarning "No root-servers reachable, skipping the test." + fi + WORKING_NUM=0 + for NSIP in ${bu_SECURE_SERVERS}; do + WORKING_NUM=$((WORKING_NUM+1)) + done + rlAssertGreater "Check we have at least some working root servers" "$WORKING_NUM" 5 + rlLog "Working root-servers: $bu_SECURE_SERVERS" + [ -n "$bu_FAILED_SERVERS" ] && rlLogWarning "Failed root-servers: $bu_FAILED_SERVERS" + + HAS_MANAGED_KEYS='' + rndc -h 2>&1 | grep -q 'managed-keys status' && HAS_MANAGED_KEYS='y' + rlPhaseEnd + +if [ "$SKIP_TEST" != y ]; then + +for I in {1..3}; do + rlPhaseStartTest "Basic test #$I" + rlRun "rlServiceStart named" + # give it chance to warm up + sleep ${DELAY_START} + buDigSuccessSecure @localhost . DNSKEY + buDigSuccessSecure @localhost + + KEYID=$(buDigKskId @localhost .) + rlAssertNotEquals "Check keyId is not empty." "$KEYID" "" + rlAssertGreater "Check keyId is non-zero" "$KEYID" 0 + rlRun "rndc secroots" + rlRun "grep \"^./RSASHA256/$KEYID\" /var/named/data/named.secroots" 0 "Check trust anchor is trusted" + [ "$HAS_MANAGED_KEYS" = y ] && rlRun "rndc managed-keys status" + rlPhaseEnd + sleep ${DELAY_TEST} +done + +DIG_SEC=buDig +DIG_INSEC=buDig + +for I in $(seq $HOSTS_TRIES); do + rlPhaseStartTest "Host tests #$I" + if [ "$I" = "$HOSTS_PASSING" ]; then + DIG_SEC=buDigSuccessSecure + DIG_INSEC=buDigSuccessInsecure + fi + for H in example.{org,com,net} fedoraproject.org isc.org + do + $DIG_SEC @localhost $H A + $DIG_SEC @localhost $H AAAA + done + for H in {org,com,net} + do + $DIG_SEC @localhost $H NS + $DIG_SEC @localhost $H DS + done + for H in {a,d,f}.root-servers.net ipv4only.arpa + do + $DIG_INSEC @localhost $H A + $DIG_INSEC @localhost $H AAAA + done + rlPhaseEnd + sleep ${DELAY_TEST} +done +fi + + rlPhaseStartCleanup + [ "$DEBUG" = y ] && PS1="test-debug $PS1" $SHELL -i + rlRun "popd" + rlRun "rm -r $tmp" 0 "Remove tmp directory" + rlFileRestore + rlRun "rlServiceRestore named" + rlPhaseEnd +rlJournalEnd