Configure DHCP to update DNS records
Introduction
This HowTo describes how to configure isc DHCP to update Samba dns records in AD.
It has now been tested with the Samba AD internal DNS server and BIND9_DLZ.
This HowTo is based on a Debian OS install, the paths given may be different if you use another OS.
The script has now been modified to use samba-tool
instead of nsupdate
, it also can optionally add the macAddress
attribute to a computers AD object, this attribute will contain the computers MAC address.
Preconditions
- The computer has been provisioned as an AD DC and the samba, smbd and winbindd daemons are running.
- You have created any required reverse zones.
- You are logged into the DC as 'root'
- If using Bind9, Bind9_dlz must be installed and working on the Samba AD DC that you are doing this on. Tested with various 9.x versions.
Names and Addresses used in this howto
- Realm : SAMDOM.EXAMPLE.COM
- Subnet : 192.168.0.0
- Netmask : 255.255.255.0
- Subnet-mask : 255.255.255.0
- Broadcast-address : 192.168.0.255
- Gateway : 192.168.0.1
- Domain-name : samdom.example.com
- Domain-name-servers : 192.168.0.6, 192.168.0.5
- Netbios-name-servers : 192.168.0.5, 192.168.0.6
- Ntp-servers : 192.168.0.5, 192.168.0.6;
- Pool range : 192.168.0.50 192.168.0.229
Install isc DHCP
First install the DHCP server
# apt-get install isc-dhcp-server
Or on FreeBSD:
# pkg install isc-dhcp44-server
Create a user to carry out the updates
You need a user that the script will run as, set a random password because you will never logon as the user.
# samba-tool user create dhcpduser --description="Unprivileged user for TSIG-GSSAPI DNS updates via ISC DHCP server" --random-password
Now set the users password to never expire and add the user to the DnsAdmins group.
# samba-tool user setexpiry dhcpduser --noexpiry # samba-tool group addmembers DnsAdmins dhcpduser
Now export the required keytab. On FreeBSD change /etc/dhcpduser.keytab
to /usr/local/etc/dhcpduser.keytab
# samba-tool domain exportkeytab --principal=dhcpduser@SAMDOM.EXAMPLE.COM /etc/dhcpduser.keytab # chown root:root /etc/dhcpduser.keytab # chmod 400 /etc/dhcpduser.keytab
![]() | In the chown command above root:root is used, you need to check what user & group DHCP runs as on your distro and if different, change root:root to the correct user & group. On FreeBSD this is dhcpd:dhcpd . |
Create the script for the updates
Copy this script to /usr/local/bin/dhcp-dyndns.sh
#!/bin/bash # On FreeBSD change the above line to #!/usr/local/bin/bash # # /usr/local/bin/dhcp-dyndns.sh # # This script is for secure DDNS updates on Samba, # it can also add the 'macAddress' to the Computers object. # # Version: 0.9.5 # # Copyright (C) Rowland Penny 2020-2022 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # You may need to ensure that you have a useful path # If you have 'path' problems, Uncomment the next line and adjust for # your setup e.g. self-compiled Samba #export PATH=/usr/local/samba/bin:/usr/local/samba/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin ########################################################################## # # # You can optionally add the 'macAddress' to the Computers object. # # Add 'dhcpduser' to the 'Domain Admins' group if used # # Change the next line to 'yes' to make this happen # Add_macAddress='no' # # ########################################################################## # On FreeBSD change this to /usr/local/etc/dhcpduser.keytab keytab=/etc/dhcpduser.keytab usage() { cat <<-EOF USAGE: $(basename "$0") add ip-address dhcid|mac-address hostname $(basename "$0") delete ip-address dhcid|mac-address EOF } _KERBEROS() { # get current time as a number test=$(date +%d'-'%m'-'%y' '%H':'%M':'%S) # Note: there have been problems with this # check that 'date' returns something like # Check for valid kerberos ticket #logger "${test} [dyndns] : Running check for valid kerberos ticket" klist -c "${KRB5CCNAME}" -s ret="$?" if [ $ret -ne 0 ] then logger "${test} [dyndns] : Getting new ticket, old one has expired" # On FreeBSD change the -F to --no-forwardable kinit -F -k -t $keytab "${SETPRINCIPAL}" ret="$?" if [ $ret -ne 0 ] then logger "${test} [dyndns] : dhcpd kinit for dynamic DNS failed" exit 1 fi fi } rev_zone_info() { local RevZone="$1" local IP="$2" local rzoneip rzoneip="${RevZone%.in-addr.arpa}" local rzonenum rzonenum=$(echo "$rzoneip" | tr '.' '\n') declare -a words for n in $rzonenum do words+=("$n") done local numwords="${#words[@]}" unset ZoneIP unset RZIP unset IP2add case "$numwords" in 1) # single ip rev zone '192' ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1}') RZIP="${rzoneip}" IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3"."$2}') ;; 2) # double ip rev zone '168.192' ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2}') RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $2"."$1}') IP2add=$(echo "${IP}" | awk -F '.' '{print $4"."$3}') ;; 3) # triple ip rev zone '0.168.192' ZoneIP=$(echo "${IP}" | awk -F '.' '{print $1"."$2"."$3}') RZIP=$(echo "${rzoneip}" | awk -F '.' '{print $3"."$2"."$1}') IP2add=$(echo "${IP}" | awk -F '.' '{print $4}') ;; *) # should never happen exit 1 ;; esac } BINDIR=$(samba -b | grep 'BINDIR' | grep -v 'SBINDIR' | awk '{print $NF}') [[ -z $BINDIR ]] && printf "Cannot find the 'samba' binary, is it installed ?\\nOr is your path set correctly ?\\n" WBINFO="$BINDIR/wbinfo" SAMBATOOL=$(command -v samba-tool) [[ -z $SAMBATOOL ]] && printf "Cannot find the 'samba-tool' binary, is it installed ?\\nOr is your path set correctly ?\\n" MINVER=$($SAMBATOOL -V | grep -o '[0-9]*' | tr '\n' ' ' | awk '{print $2}') if [ "$MINVER" -gt '14' ] then KTYPE="--use-kerberos=required" else KTYPE="-k yes" fi # DHCP Server hostname Server=$(hostname -s) # DNS domain domain=$(hostname -d) if [ -z "${domain}" ] then logger "Cannot obtain domain name, is DNS set up correctly?" logger "Cannot continue... Exiting." exit 1 fi # Samba realm REALM="${domain^^}" # krbcc ticket cache export KRB5CCNAME="/tmp/dhcp-dyndns.cc" # Kerberos principal SETPRINCIPAL="dhcpduser@${REALM}" # Kerberos keytab as above # krbcc ticket cache : /tmp/dhcp-dyndns.cc TESTUSER="$($WBINFO -u | grep 'dhcpduser')" if [ -z "${TESTUSER}" ] then logger "No AD dhcp user exists, need to create it first.. exiting." logger "you can do this by typing the following commands" logger "kinit Administrator@${REALM}" logger "$SAMBATOOL user create dhcpduser --random-password --description='Unprivileged user for DNS updates via ISC DHCP server'" logger "$SAMBATOOL user setexpiry dhcpduser --noexpiry" logger "$SAMBATOOL group addmembers DnsAdmins dhcpduser" exit 1 fi # Check for Kerberos keytab if [ ! -f /etc/dhcpduser.keytab ] then logger "Required keytab $keytab not found, it needs to be created." logger "Use the following commands as root" logger "$SAMBATOOL domain exportkeytab --principal=${SETPRINCIPAL} $keytab" logger "chown XXXX:XXXX $keytab" logger "Replace 'XXXX:XXXX' with the user & group that dhcpd runs as on your distro" logger "chmod 400 $keytab" exit 1 fi # Variables supplied by dhcpd.conf action="$1" ip="$2" DHCID="$3" name="${4%%.*}" # Exit if no ip address if [ -z "${ip}" ] then usage exit 1 fi # Exit if no computer name supplied, unless the action is 'delete' if [ -z "${name}" ] then if [ "${action}" = "delete" ] then name=$(host -t PTR "${ip}" | awk '{print $NF}' | awk -F '.' '{print $1}') else usage exit 1 fi fi # exit if name contains a space case ${name} in *\ * ) logger "Invalid hostname '${name}' ...Exiting" exit ;; esac # if you want computers with a hostname that starts with 'dhcp' in AD # comment the following block of code. if [[ $name == dhcp* ]] then logger "not updating DNS record in AD, invalid name" exit 0 fi ## update ## case "${action}" in add) _KERBEROS count=0 # does host have an existing 'A' record ? A_REC=$($SAMBATOOL dns query "${Server}" "${domain}" "${name}" A "$KTYPE" 2>/dev/null | grep 'A:' | awk '{print $2}') # turn A_REC into an array A_REC=("$A_REC") if [ "${#A_REC[@]}" -eq 0 ] then # no A record to delete result1=0 $SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE" result2="$?" elif [ "${#A_REC[@]}" -gt 1 ] then for i in "${A_REC[@]}" do $SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${i}" "$KTYPE" done # all A records deleted result1=0 $SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE" result2="$?" elif [ "${#A_REC[@]}" -eq 1 ] then # turn array into a variable VAR_A_REC="${A_REC[*]}" if [ "$VAR_A_REC" = "${ip}" ] then # Correct A record exists, do nothing logger "Correct 'A' record exists, not updating." result1=0 result2=0 count=$((count+1)) elif [ "$VAR_A_REC" != "${ip}" ] then # Wrong A record exists logger "'A' record changed, updating record." $SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${VAR_A_REC}" "$KTYPE" result1="$?" $SAMBATOOL dns add "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE" result2="$?" fi fi # get existing reverse zones (if any) ReverseZones=$($SAMBATOOL dns zonelist "${Server}" "$KTYPE" --reverse | grep 'pszZoneName' | awk '{print $NF}') if [ -z "$ReverseZones" ]; then logger "No reverse zone found, not updating" result3='0' result4='0' count=$((count+1)) else for revzone in $ReverseZones do rev_zone_info "$revzone" "${ip}" if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ] then # does host have an existing 'PTR' record ? PTR_REC=$($SAMBATOOL dns query "${Server}" "${revzone}" "${IP2add}" PTR "$KTYPE" 2>/dev/null | grep 'PTR:' | awk '{print $2}' | awk -F '.' '{print $1}') if [[ -z $PTR_REC ]] then # no PTR record to delete result3=0 $SAMBATOOL dns add "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE" result4="$?" break elif [ "$PTR_REC" = "${name}" ] then # Correct PTR record exists, do nothing logger "Correct 'PTR' record exists, not updating." result3=0 result4=0 count=$((count+1)) break elif [ "$PTR_REC" != "${name}" ] then # Wrong PTR record exists # points to wrong host logger "'PTR' record changed, updating record." $SAMBATOOL dns delete "${Server}" "${revzone}" "${IP2add}" PTR "${PTR_REC}"."${domain}" "$KTYPE" result3="$?" $SAMBATOOL dns add "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE" result4="$?" break fi else continue fi done fi ;; delete) _KERBEROS count=0 $SAMBATOOL dns delete "${Server}" "${domain}" "${name}" A "${ip}" "$KTYPE" result1="$?" # get existing reverse zones (if any) ReverseZones=$($SAMBATOOL dns zonelist "${Server}" --reverse "$KTYPE" | grep 'pszZoneName' | awk '{print $NF}') if [ -z "$ReverseZones" ] then logger "No reverse zone found, not updating" result2='0' count=$((count+1)) else for revzone in $ReverseZones do rev_zone_info "$revzone" "${ip}" if [[ ${ip} = $ZoneIP* ]] && [ "$ZoneIP" = "$RZIP" ] then host -t PTR "${ip}" > /dev/null 2>&1 ret="$?" if [ $ret -eq 0 ] then $SAMBATOOL dns delete "${Server}" "${revzone}" "${IP2add}" PTR "${name}"."${domain}" "$KTYPE" result2="$?" else result2='0' count=$((count+1)) fi break else continue fi done fi result3='0' result4='0' ;; *) logger "Invalid action specified" exit 103 ;; esac result="${result1}:${result2}:${result3}:${result4}" if [ "$count" -eq 0 ] then if [ "${result}" != "0:0:0:0" ] then logger "DHCP-DNS $action failed: ${result}" exit 1 else logger "DHCP-DNS $action succeeded" fi fi if [ "$Add_macAddress" != 'no' ] then if [ -n "$DHCID" ] then Computer_Object=$(ldbsearch "$KTYPE" -H ldap://"$Server" "(&(objectclass=computer)(objectclass=ieee802Device)(cn=$name))" | grep -v '#' | grep -v 'ref:') if [ -z "$Computer_Object" ] then # Computer object not found with the 'ieee802Device' objectclass, does the computer actually exist, it should. Computer_Object=$(ldbsearch "$KTYPE" -H ldap://"$Server" "(&(objectclass=computer)(cn=$name))" | grep -v '#' | grep -v 'ref:') if [ -z "$Computer_Object" ] then logger "Computer '$name' not found. Exiting." exit 68 else DN=$(echo "$Computer_Object" | grep 'dn:') objldif="$DN changetype: modify add: objectclass objectclass: ieee802Device" attrldif="$DN changetype: modify add: macAddress macAddress: $DHCID" # add the ldif echo "$objldif" | ldbmodify "$KTYPE" -H ldap://"$Server" ret="$?" if [ $ret -ne 0 ] then logger "Error modifying Computer objectclass $name in AD." exit "${ret}" fi sleep 2 echo "$attrldif" | ldbmodify "$KTYPE" -H ldap://"$Server" ret="$?" if [ "$ret" -ne 0 ]; then logger "Error modifying Computer attribute $name in AD." exit "${ret}" fi unset objldif unset attrldif logger "Successfully modified Computer $name in AD" fi else DN=$(echo "$Computer_Object" | grep 'dn:') attrldif="$DN changetype: modify replace: macAddress macAddress: $DHCID" echo "$attrldif" | ldbmodify "$KTYPE" -H ldap://"$Server" ret="$?" if [ "$ret" -ne 0 ] then logger "Error modifying Computer attribute $name in AD." exit "${ret}" fi unset attrldif logger "Successfully modified Computer $name in AD" fi fi fi exit 0
If you wish to store the computers MAC address in AD, find this line:
Add_macAddress='no'
It is near the top of the script. Change 'no' to 'yes'. Note you will need to grant DomainAdmin privileges to the DNS update user.
Set the permissions on the script.
# chmod 755 /usr/local/bin/dhcp-dyndns.sh
Modify the dhcp conf file
First backup the original conf file.
# cp /etc/dhcp/dhcpd.conf /etc/dhcp/dhcpd.conf.orig
Now edit /etc/dhcp/dhcpd.conf and make it look similar to the this.
authoritative; ddns-update-style none; subnet 192.168.0.0 netmask 255.255.255.0 { option subnet-mask 255.255.255.0; option broadcast-address 192.168.0.255; option time-offset 0; option routers 192.168.0.1; option domain-name "samdom.example.com"; option domain-name-servers 192.168.0.6, 192.168.0.5; option ntp-servers 192.168.0.5, 192.168.0.6; pool { max-lease-time 1800; # 30 minutes range 192.168.0.50 192.168.0.229; } } on commit { set noname = concat("dhcp-", binary-to-ascii(10, 8, "-", leased-address)); set ClientIP = binary-to-ascii(10, 8, ".", leased-address); 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("Commit: IP: ", ClientIP, " DHCID: ", ClientDHCID, " Name: ", ClientName)); execute("/usr/local/bin/dhcp-dyndns.sh", "add", ClientIP, ClientDHCID, ClientName); } on release { set ClientIP = binary-to-ascii(10, 8, ".", leased-address); 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("Release: IP: ", ClientIP)); execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, ClientDHCID); } on expiry { set ClientIP = binary-to-ascii(10, 8, ".", leased-address); # 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 # however the dhcp update script will obtain the short hostname. execute("/usr/local/bin/dhcp-dyndns.sh", "delete", ClientIP, "", "0"); }
Start the dhcp server and see what happens.
You must stop your windows clients from trying to update their own records, as this will fail and fill your logs with errors.
Add failover
Add the following to the /etc/dhcp/dhcpd.conf file on the primary:
failover peer "dhcp-failover" { primary; address dc1.samdom.example.com; peer address dc2.samdom.example.com; max-response-delay 60; max-unacked-updates 10; mclt 3600; split 255; load balance max seconds 3; }
..and secondary:
failover peer "dhcp-failover" { secondary; address dc2.samdom.example.com; peer address dc1.samdom.example.com; max-response-delay 60; max-unacked-updates 10; load balance max seconds 3; }
Make sure that you add the failover section, above the 'subnet' section.
Add references for the subnet/pool which will do failover.
subnet 192.168.0.0 netmask 255.255.255.0 { option subnet-mask 255.255.255.0; option broadcast-address 192.168.0.255; option time-offset 0; option routers 192.168.0.1; option domain-name "samdom.example.com"; option domain-name-servers 192.168.0.5, 192.168.0.6; option ntp-servers 192.168.0.5, 192.168.0.6; pool { failover peer "dhcp-failover"; max-lease-time 1800; # 30 minutes range 192.168.0.50 192.168.0.229; } }
Configure OMAPI and Define a Secret Key
BIND 9.12 and earlier
Generate a random OMAPI key on the primary, using the dnssec-keygen utility distributed with BIND.
dnssec‐keygen ‐a HMAC‐MD5 ‐b 512 ‐n USER DHCP_OMAPI
Now extract the actual key:
cat Kdhcp_omapi.+*.private |grep ^Key|cut -d ' ' -f2-
Add the following to dhcpd.conf on both primary and secondary.
omapi-port 7911; omapi-key omapi_key; key omapi_key { algorithm hmac-md5; secret "PUT_YOUR_KEY_HERE"; }
Replace PUT_YOUR_KEY_HERE with the key you extracted from the private key created by the dnssec command.
Continue with #All BIND versions
BIND 9.13 and later
Generate a random OMAPI key on either primary or secondary, using the tsig-keygen utility distributed with BIND.
tsig-keygen -a hmac-md5 omapi_key
The command will output text to your screen, similar to this:
key "omapi_key" { algorithm hmac-md5; secret "some_secret_text"; };
Add the following lines to dhcpd.conf on both primary and secondary, followed by the text from the previous command:
omapi-port 7911; omapi-key omapi_key;
key "omapi_key" { algorithm hmac-md5; secret "some_secret_text"; };
Continue with #All BIND versions
All BIND versions
Restart both servers to apply the configuration changes.
You should find lines similar to these, in the system logs on both machines:
Feb 28 17:34:39 dc1 dhcpd: failover peer dhcp-failover: peer moves from recover-done to normal Feb 28 17:34:39 dc1 dhcpd: failover peer dhcp-failover: Both servers normal
If OMAPI is working properly you can test failover by stopping the primary server.
If you are using a firewall, you will need to open TCP ports 647 and 7911
Once you are sure everything is working as expected, restart both servers to ensure everything is running correctly.
The 'split' value '255' on the 'primary', makes it responsible for the clients. With the value set to '255', the primary will answer all dhcp requests unless it is down (for whatever reason), use '0' to make the secondary responsible.
For more information, read the dhcpd.conf manpage man dhcpd.conf
.
Apparmor
To get DHCP updates working with Apparmor, you need to alter /etc/apparmor.d/usr.sbin.dhcpd to match this.
/etc/dhcp/ r, /etc/dhcp/** r, /etc/dhcpd{,6}.conf r, /etc/dhcpd{,6}_ldap.conf r, /usr/local/bin/dhcp-dyndns.sh ix, /bin/grep rix, /usr/sbin/samba rix, /usr/bin/gawk rix, /bin/hostname rix, /usr/bin/wbinfo rix, /usr/bin/heimtools rix, /usr/bin/logger rix, /usr/bin/kinit.heimdal rix, /bin/date rix, /dev/tty wr, /dev/urandom w, /proc/** r, /usr/bin/kinit w, /run/samba/winbindd/pipe wr,
The first 4 lines are the default, you will need to add everything else. With these settings the dhcp-server will start and work.
The above settings have been tested on Ubuntu 18.04 and were supplied by Stefan Kania.
Any questions or problems, ask on the Samba mailing list.
![]() | Do not log a bug report on Samba bugzilla for any problems you have with this set up, ask on the samba mailing list. |