DHCP с безопасными динамическими обновлениями записей DNS Active Directory

  1. Добавляем в /etc/portage/package.use/custom такие строчки
    `#net-proxy/squid kerberos
net-proxy/squid kerberos (если требуется)
dev-libs/cyrus-sasl kerberos (если требуется)
net-misc/dhcp ldap (если требуется)
net-dns/bind-tools gssapi
  1. Редактируем /etc/krb5.conf
[logging]
 default = FILE:/var/log/krb5libs.log
 kdc = FILE:/var/log/krb5kdc.log
 admin_server = FILE:/var/log/kadmind.log

[libdefaults]
 dns_lookup_kdc = no
 dns_lookup_realm = false
 ticket_lifetime = 24h
 renew_lifetime = 7d
 forwardable = true
 rdns = false
 #Для Windows 2003 добавить:
 #default_tgs_enctypes = rc4-hmac des-cbc-crc des-cbc-md5
 #default_tkt_enctypes = rc4-hmac des-cbc-crc des-cbc-md5
 #permitted_enctypes = rc4-hmac des-cbc-crc des-cbc-md5

 default_realm = DIB5.LOCAL
 #default_ccache_name = KEYRING:persistent:%{uid}
 default_keytab_name = /etc/squid/proxy.keytab

[realms]

 DIB5.LOCAL = {
  kdc = dc1.dib5.local
  #kdc = srvasdc2.domen.local
  #admin_server = dc1.dib5.local
 }
  1. Редактиуем /etc/dhcp/dhcpd.conf
# DHCP Server Configuration file.
#   see /usr/share/doc/dhcp*/dhcpd.conf.sample
#   see 'man 5 dhcpd.conf'
#ddns-domainname "dib5.local";          # Имя нашего домена

local-address 192.168.1.200;                            # Адрес сервера

#option tftp-server-name "calculate.dib5.local";
#next-server 192.168.200.71;
#option tftp-server-name "wds-win.dib5.local";
#next-server 192.168.200.77;
#option bootfile-name "boot\\x86\\wdsnbp.com";
#option bootfile-name "boot\\x64\\wdsnbp.com";

#filename "lpxelinux.0";
#filename "pxelinux.0";
#option root-path

ddns-update-style interim;
ddns-updates on;
#ddns-update-style standard;
update-static-leases on;
ignore client-updates;
update-conflict-detection off;
do-forward-updates on;
update-optimization off;
allow unknown-clients;
#use-host-decl-names on;
ddns-domainname "dib5.local.";
ddns-rev-domainname "in-addr.arpa.";
#authoritative;

# Логгинг информации, если запрос пришел от DHCP-relay
#if exists agent.circuit-id {
#       log ( info, concat( " Accepted DHCP RELAY request for ",
#               binary-to-ascii (10, 8, ".", leased-address),
#               " Network segment: ",
#               option agent.circuit-id,
#               " DHCP Agent: ",
#               option agent.remote-id));
#}

# Настройка времени аренды
default-lease-time 60;
max-lease-time 60;

# default netmask /24
#option subnet-mask 255.255.255.0;

include "/etc/dhcp/conf/192.168.1.subnet";
  1. создаем и редактируем /etc/dhcp/conf/192.168.1.subnet
# Vlan200
subnet 192.168.1.0 netmask 255.255.255.0 {
    range 192.168.1.217 192.168.1.245;
    option domain-name-servers 192.168.1.111;
    option netbios-name-servers 192.168.1.111;
    option netbios-node-type 8;
    option subnet-mask 255.255.255.0;
    option broadcast-address 192.168.1.255;
    option domain-name "dib5.local.";
    option routers 192.168.1.1;
    option ntp-servers 192.168.1.111;

on commit {
set noname = concat("dhcp-", binary-to-ascii(10, 8, "-", leased-address));
set ClientName = pick-first-value(option host-name, config-option-host-name, client-name, noname);
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
#set ClientDHCID = binary-to-ascii(16, 8, ":", hardware);
set ClientDHCID = concat (
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
);

log(concat("Commit: IP: ", ClientIP, " DHCID: ", ClientDHCID, " Name: ", ClientName));
#execute("/etc/dhcp/dns-krbnsupdate.sh", "add", ClientIP, "-h", ClientName, "-m", ClientMac);
execute("/etc/dhcp/dns-krbnsupdate.sh", "add", ClientIP, "-h", ClientName, "-m", ClientDHCID);
}

on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
#set ClientDHCID = binary-to-ascii(16, 8, ":", hardware);
set ClientDHCID = concat (
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
);
set ClientName = pick-first-value(option host-name, config-option-host-name, client-name, noname);
log(concat("Release: IP: ", ClientIP));
#execute("/etc/dhcp/dns-krbnsupdate.sh", "delete", ClientIP, "-m", ClientMac);
execute("/etc/dhcp/dns-krbnsupdate.sh", "delete", ClientIP);
}

on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientName = pick-first-value(option host-name, config-option-host-name, client-name, noname);
# cannot get a ClientMac here, apparently this only works when actually receiving a packet
log(concat("Expired: IP: ", ClientIP));
# cannot get a ClientName here, for some reason that always fails
execute("/etc/dhcp/dns-krbnsupdate.sh", "delete", ClientIP);
}

}

  1. создаем, редактируем и даем нужные права на скрипт /etc/dhcp/dns-krbnsupdate.sh
#!/bin/bash

# This script is for secure DDNS updates using GSS/TSIG
# Version: 0.1

## CONFIGURATION ##
# Kerberos realm
REALM="DIB5.LOCAL"
# Kerberos principal
PRINCIPAL="dnsupdater@${REALM}"
# Kerberos keytab
KEYTAB="/etc/dhcp/dnsupdater.keytab"
# Kerberos credentials cache
KRB5CC="/tmp/krb5cc_0"
# Use MIT kerberos args instead of heimdal.
KRB5MIT="YES"

# Domain appended to hostname
DOMAIN="dib5.local"
# Space separated list of DNS servers for sending updates to
NSRVS="192.168.1.111"
# Default DNS resource records TTL
RRTTL="60"
# Do not use TXT RRs (rfc4701)
NOTXTRRS="YES"
# Default to IPv4
PROTO="IPv4"

# Additional nsupdate flags (-g already applied), e.g. "-d" for debug
#NSUPDFLAGS="-d"
# Run in the foreground (for manual run only!!!), it's better to use "-d" as script's first argument
#DEBUG="YES"

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

_usage() {
  if [ "${NOTXTRRS}" == "YES" ]
  then
    echo "Usage:"
    echo "`basename ${0}` add ip-address -h hostname [-m dhcid|mac-address] [-p IP version | --ipv4 | --ipv6] [-t dns-ttl] [-d]"
    echo "`basename ${0}` delete ip-address [-m dhcid|mac-address] [-p IP version | --ipv4 | --ipv6]"
  else
    echo "Usage:"
    echo "`basename ${0}` add ip-address -h hostname -m dhcid|mac-address [-p IP version | --ipv4 | --ipv6] [-t dns-ttl] [-d]"
    echo "`basename ${0}` delete ip-address -m dhcid|mac-address [-p IP version | --ipv4 | --ipv6]"
  fi
}

## VARIABLES ##
ACTION=${1}
IP=${2}
shift && shift
while [ ! -z ${1} ]
do
  case "${1}" in
    "-d")
      DEBUG="YES"
      shift
    ;;
    "-m")
      DHCID=${2}
      shift && shift
    ;;
    "-h")
      NAME=${2%%.*}
      shift && shift
    ;;
    "-t")
      RRTTL="${2}"
      shift && shift
    ;;
    "-p")
      PROTO=${2}
      shift && shift
    ;;
    "--ipv4")
      PROTO="IPv4"
      shift
    ;;
    "--ipv6")
      PROTO="IPv6"
      shift
    ;;
    "--help")
      _usage
      exit 0
    ;;
    *)
      echo "Unknown option: ${1}"
      _usage
      exit 1
  esac
done

_kerberos() {
  export KRB5_KTNAME="${KEYTAB}"
  export KRB5CCNAME="${KRB5CC}"

  if [ "${KRB5MIT}" = "YES" ]; then
    KLISTARG="-s"
  else
    KLISTARG="-t"
  fi

  klist ${KLISTARG} || kinit -k -t "${KEYTAB}" -c "${KRB5CC}" "${PRINCIPAL}" || { echo "DDNS: kinit failed"; exit 1; }
}

_expandv6() {
# Adapted from https://github.com/mutax/IPv6-Address-checks by
# Florian Streibelt <florian@f-streibelt.de>
  local __revipv6=${2}
  local INPUT="${1}"
  local O=""

  while [ "${O}" != "${INPUT}" ]; do
    O="${INPUT}"

    # fill all words with zeroes
    INPUT="$( sed  's|:\([0-9a-f]\{3\}\):|:0\1:|g' <<< "${INPUT}" )"
    INPUT="$( sed  's|:\([0-9a-f]\{3\}\)$|:0\1|g'  <<< "${INPUT}")"
    INPUT="$( sed  's|^\([0-9a-f]\{3\}\):|0\1:|g'  <<< "${INPUT}" )"

    INPUT="$( sed  's|:\([0-9a-f]\{2\}\):|:00\1:|g' <<< "${INPUT}")"
    INPUT="$( sed  's|:\([0-9a-f]\{2\}\)$|:00\1|g'  <<< "${INPUT}")"
    INPUT="$( sed  's|^\([0-9a-f]\{2\}\):|00\1:|g'  <<< "${INPUT}")"

    INPUT="$( sed  's|:\([0-9a-f]\):|:000\1:|g'  <<< "${INPUT}")"
    INPUT="$( sed  's|:\([0-9a-f]\)$|:000\1|g'   <<< "${INPUT}")"
    INPUT="$( sed  's|^\([0-9a-f]\):|000\1:|g'   <<< "${INPUT}")"
  done

  # now expand the ::
  grep -qs "::" <<< "${INPUT}"
  if [ "$?" -eq 0 ]; then
    GRPS="$(sed  's|[0-9a-f]||g' <<< "${INPUT}" | wc -m)"
    ((GRPS--)) # carriage return
    ((MISSING=8-GRPS))
    for ((i=0;i<$MISSING;i++)); do
      ZEROES="$ZEROES:0000"
    done

    # be careful where to place the :
    INPUT="$( sed  's|\(.\)::\(.\)|\1'$ZEROES':\2|g'   <<< "${INPUT}")"
    INPUT="$( sed  's|\(.\)::$|\1'$ZEROES':0000|g'   <<< "${INPUT}")"
    INPUT="$( sed  's|^::\(.\)|'$ZEROES':0000:\1|g;s|^:||g'   <<< "${INPUT}")"
  fi

  # an expanded address has 39 chars + CR
  if [ $(echo ${INPUT} | wc -m) != 40 ]; then
    echo "Invalid IPv6 Address"
    exit 1
  fi

  LEN=${#INPUT}
  for ((i=LEN-1;i>=0;i--))
  do
    [ ${INPUT:i:1} != ":" ] && TMPSTR=${TMPSTR}"."${INPUT:i:1}
  done
  INPUT=${TMPSTR:1}".ip6.arpa"

  # echo the fully expanded version of the address
  eval ${__revipv6}="'${INPUT}'"
}

_main() {
  umask 77

  if [ -z "${IP}" ]
  then
    _usage
    exit 1
  fi

  if [ "${NOTXTRRS}" != "YES" ] && [ -z "${DHCID}" ]
  then
    _usage
    exit 1
  fi

  ## NSUPDATE ##
  case "${ACTION}" in
    add)
      if [ -z "${NAME}" ]
      then
        _usage
        exit 1
      fi

      RRPTR="${NAME}.${DOMAIN}"
      if [ "${NOTXTRRS}" != "YES" ]; then
        NOTXTRRS=""
        RRAOLD=`host ${RRPTR} | awk '/has address/ {print $4}'`
        if [ -n "${RRAOLD}" ]; then
          RRTXTOLD=`host -t txt "${RRPTR}" | sed -n '/descriptive text/s/^.*[[:space:]]descriptive text[[:space:]]*"\(.*\)"$/\1/p'`
          [ -z "${RRTXTOLD}" ] && echo "DDNS: adding records for $ip (${RRPTR}) FAILED: has A record but no DHCID, not mine" && exit 1

          RRTXT=`echo "${DHCID}${RRPTR}" | sha256sum`
          RRTXT="000101${RRTXT%% *}"
          [ "${RRTXT}" != "${RRTXTOLD}" ] && echo "DDNS: adding records for $ip (${RRPTR}) FAILED: has A record but DHCID is wrong" && exit 1
        else
          RRTXT=`echo "${DHCID}${RRPTR}" | sha256sum`
          RRTXT="000101${RRTXT%% *}"
        fi
      else
        NOTXTRRS=";"
      fi

      echo "${PROTO}!"

      case "${PROTO}" in
        "IPv4")
           RRPTRNAME=`echo ${IP} | awk -F '.' '{print $4"."$3"."$2"."$1".in-addr.arpa"}'`
           RECNAME="A"
        ;;
        "IPv6")
          _expandv6 ${IP} RRPTRNAME
          RECNAME="AAAA"
        ;;
        *)
          echo "Unknown IP version: ${PROTO}"
          exit 1
      esac

      _kerberos

      for NSRV in ${NSRVS}; do
        nsupdate -g ${NSUPDFLAGS} << UPDATE
server ${NSRV}
realm ${REALM}
update delete ${RRPTR}. ${RRTTL} ${RECNAME}
${NOTXTRRS}update delete ${RRPTR}. ${RRTTL} TXT
${NOTXTRRS}update add ${RRPTR}. ${RRTTL} TXT ${RRTXT}
update add ${RRPTR}. ${RRTTL} ${RECNAME} ${IP}
send
update delete ${RRPTRNAME}. ${RRTTL} PTR
update add ${RRPTRNAME}. ${RRTTL} PTR ${NAME}.${DOMAIN}.
send
UPDATE

        RESULT=${?}
        [ "${RESULT}" -eq "0" ] && echo "DDNS: adding records for ${IP} (${RRPTR}) succeeded" && exit 0
      done

      [ "${RESULT}" != "0" ] && echo "DDNS: adding records for ${IP} (${RRPTR}) FAILED: nsupdate status ${RESULT}" && exit "${RESULT}"
    ;;
    delete)
      RRPTR=`host ${IP} | awk '/domain name pointer/ { sub(/\.$/, "", $5); print $5}'`
      if [ "${NOTXTRRS}" != "YES" ]; then
        NOTXTRRS=""
        if [ -n "${RRPTR}" ]; then
          RRTXTOLD=`host -t txt "${RRPTR}" | sed -n '/descriptive text/s/^.*[[:space:]]descriptive text[[:space:]]*"\(.*\)"$/\1/p'`
          [ -z "${RRTXTOLD}" ] && echo "DDNS: removing records for $ip (${RRPTR}) FAILED: has A record but no DHCID, not mine" && exit 1

          RRTXT=`echo "${DHCID}${RRPTR}" | sha256sum`
          RRTXT="000101${RRTXT%% *}"
          [ "${RRTXT}" != "${RRTXTOLD}" ] && echo "DDNS: removing records for ${IP} (${RRPTR}) FAILED: has A record but DHCID is wrong" && exit 1
        else
          echo "DDNS: removing records for ${IP} FAILED: has no PTR, can not determine A record" && exit 1
        fi
      else
        NOTXTRRS=";"
      fi

      case "${PROTO}" in
        "IPv4")
           RRPTRNAME=`echo ${IP} | awk -F '.' '{print $4"."$3"."$2"."$1".in-addr.arpa"}'`
           RECNAME="A"
        ;;
        "IPv6")
          _expandv6 ${IP} RRPTRNAME
          RECNAME="AAAA"
        ;;
        *)
          echo "Unknown IP version: ${PROTO}"
          exit 1
      esac

      _kerberos

      for NSRV in ${NSRVS}; do
        nsupdate -g ${NSUPDFLAGS} << UPDATE
server ${NSRV}
realm ${REALM}
update delete ${RRPTR}. ${RRTTL} ${RECNAME}
${NOTXTRRS}update delete ${RRPTR}. ${RRTTL} TXT
send
update delete ${RRPTRNAME}. ${RRTTL} PTR
send
UPDATE

        RESULT=${?}
        [ "${RESULT}" -eq "0" ] && echo "DDNS: removing records for ${IP} (${RRPTR}) succeeded" && exit 0
      done

      [ "${RESULT}" != "0" ] && echo "DDNS: removing records for ${IP} (${RRPTR}) FAILED: nsupdate status ${RESULT}" && exit "${RESULT}"
    ;;
    *)
      _usage && exit 1
    ;;
esac
}

if [ "$DEBUG" = "YES" ]; then
  _main
else
  :
  _main | logger -s -t dhcpd &
fi
  1. На КД создаем пользователя например dnsupdater и добавляем в группу DnsUpdateProxy
  2. В останстке DNS добавляем разделе безопасность пользователя dnsupdater или группу DnsUpdateProxy. Можно сделать на все дочернии обьекты чтобы этот пользователь смог обновить действующие записи.
  3. На КД, запускаем cmd от администратора, и выполняем команду
ktpass /princ dnsupdater@DIB5.LOCAL /mapuser dnsupdater@DIB5.LOCAL /crypto ALL /ptype KRB5_NT_PRINCIPAL /pass "12345" /out C:\dnsupdater.keytab
  1. файл копируем на сервер calculate с dhcp и выставляем нужные права.
  2. редактируем файл /etc/dhcp/dns-krbnsupdate.sh, указываем значения ниже
# This script is for secure DDNS updates using GSS/TSIG
# Version: 0.1

## CONFIGURATION ##
# Kerberos realm
REALM="DIB5.LOCAL"
# Kerberos principal
PRINCIPAL="dnsupdater@${REALM}"
# Kerberos keytab
KEYTAB="/etc/dhcp/dnsupdater.keytab"
# Kerberos credentials cache
KRB5CC="/tmp/krb5cc_0"
# Use MIT kerberos args instead of heimdal.
KRB5MIT="YES"

# Domain appended to hostname
DOMAIN="dib5.local"
# Space separated list of DNS servers for sending updates to
NSRVS="192.168.1.111"
# Default DNS resource records TTL
RRTTL="60"
# Do not use TXT RRs (rfc4701)
NOTXTRRS="YES"
# Default to IPv4
PROTO="IPv4"

# Additional nsupdate flags (-g already applied), e.g. "-d" for debug
#NSUPDFLAGS="-d"
# Run in the foreground (for manual run only!!!), it's better to use "-d" as script's first argument
#DEBUG="YES"

  1. Создаем групповую полититку, или меняем политику Default Domain Policy: В раздлеле Конфигурация компьютера -> Административные шаблоны -> Сеть DNS включаем эти значения Динамическое обновление - отключено, Регистрация PTR записи отлючено.
  2. Теперь DHCP сервер сможет обновлять А записи, PTR запись почему то не обновляется, а появляется новая запись с новым айпи.
  3. На windows клиентах выполняем команду ipconfig /realese *, ipconfig /renew * для получения нового ip.
  4. На dhcp сервер если запускать ручками скрипт /etc/dhcp/dns-krbnsupdate.sh delete <указать ip> то запись удаляется. Также если выполнить /etc/dhcp/dns-krbnsupdate.sh add <указать ip> -h hostname [-m dhcid|mac-address]
dns-krbnsupdate.sh add ip-address -h hostname [-m dhcid|mac-address] [-p IP version | --ipv4 | --ipv6] [-t dns-ttl] [-d]
dns-krbnsupdate.sh delete ip-address [-m dhcid|mac-address] [-p IP version | --ipv4 | --ipv6]