Now that we’ve got a functional Active Directory domain, we can leverage it to provide DNS and DHCP services to the lab network segment.

Creating DNS Records for the Lab

By default, Active Directory creates a DNS zone for its own domain name. In order to manage DNS records for other zones, or to manage reverse DNS lookups, we’ll need to make additional zones. To create forward and reverse DNS zones for the lab network manually, I’d normally run the following in PowerShell:

add-dnsserverprimaryzone -name "blab.renf.ro" -replicationscope forest -passthru
add-dnsserverprimaryzome -networkid "192.168.1.0/24" -replicationscope forest -passthru

But for this work, I’ll use another Bolt task instead of running the commands manually.

Creating DNS Zones with Bolt

We’ll replace the above PowerShell commands with a Bolt task named create_dns_zone. The contents of create_dns_zone.ps1 are:

[CmdletBinding(PositionalBinding=$false)]
param (
  [Parameter(Mandatory=$false)]
  [string]
  $domainname,
  [Parameter(Mandatory=$false)]
  [string]
  $network
)
if ($domainname -ne "" -and $network -ne "") {
  write-host "Cannot specify both domainname and network"
  exit 1
}
if ($domainname -eq "" -and $network -eq "") {
  write-host "Cannot omit both domainname and network"
  exit 1
}    
try {
  if ($domainname -ne "") {
    Add-DnsServerPrimaryZone -Name $domainname -ReplicationScope "Forest" -PassThru
  } else {
    Add-DnsServerPrimaryZone -NetworkID $network -ReplicationScope "Forest" -Passthru
  }
} catch {
  exit 2
}

The contents of create_dns_zone.json are:

{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Create DNS zone.",
  "parameters": {
    "domainname": {
      "description": "The DNS domain name.",
      "type": "Optional[String]"
    },
    "network": {
      "description": "The network ID (CIDR format).",
      "type": "Optional[String]"
    }
  }
}

Run the task to create a second forward DNS zone for the lab hosts, and a reverse lookup zone for the lab network segment:

bolt task run --targets dc renfro::create_dns_zone domainname=blab.renf.ro
bolt task run --targets dc renfro::create_dns_zone network=192.168.1.0/24

Creating DNS Records with Bolt

As before, we’ll create a Bolt task for creating DNS records. The most common set of records to create are:

  • forward lookup records from a hostname to an IP (A records)
  • reverse lookup records from an IP to a hostname (PTR records)
  • forward lookup records from a canonical name to a fully-qualified domain name (CNAME records)

A PowerShell script (create_dns_record.ps1) to create each of these three types of records is:

[CmdletBinding(PositionalBinding=$false)]
param (
  [Parameter(Mandatory=$false)]
  [string]
  $name,
  [Parameter(Mandatory=$true)]
  [string]
  $domain,
  [Parameter(Mandatory=$false)]
  [string]
  $cname,
  [Parameter(Mandatory=$false)]
  [string]
  $ip
)
if (($name -ne "" -and $cname -ne "" -and $ip -ne "") -or
    ($name -ne "" -and $cname -ne "") -or
    ($name -ne "" -and $ip -ne "")) {
  # Continue with record creation
  if ($name -ne "" -and $cname -ne "" -and $ip -ne "") {
    # Add A and PTR records, add CNAME record
    Add-DnsServerResourceRecordA -Name $name -IPv4Address $ip -ZoneName $domain -CreatePtr
    Add-DnsServerResourceRecordCName -Name $cname -HostNameAlias ("{0}.{1}" -f $name, $domain) -ZoneName $domain
    exit 0
  }
  if ($name -ne "" -and $ip -ne "") {
    # Add A and PTR records
    Add-DnsServerResourceRecordA -Name $name -IPv4Address $ip -ZoneName $domain -CreatePtr
    exit 0
  }
  if ($name -ne "" -and $cname -ne "") {
    # Add CNAME record
    Add-DnsServerResourceRecordCName -Name $cname -HostNameAlias ("{0}.{1}" -f $name, $domain) -ZoneName $domain
    exit 0
  }
} else {
  write-host "Must specify: name, cname, ip; name, cname; name, ip"
  exit 1
}

and the corresponding JSON metadata is:

{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Create DNS A, PTR, CNAME records.",
  "parameters": {
    "name": {
      "description": "The host name.",
      "type": "Optional[String]"
    },
    "domain": {
      "description": "The domain name.",
      "type": "String"
    },
    "cname": {
      "description": "The host CNAME (alias).",
      "type": "Optional[String]"
    },
    "ip": {
      "description": "The host IP.",
      "type": "Optional[String]"
    }
  }
}

This task can be tested with records for the gold server as:

bolt task run --targets=dc renfro::create_dns_record name=puppet domain=blab.renf.ro ip=192.168.1.2 cname=gold

And verified with the commands:

bolt command run "Get-DnsServerResourceRecord -ZoneName blab.renf.ro" --target dc
bolt command run "Get-DnsServerResourceRecord -ZoneName 1.168.192.in-addr.arpa" --target dc

both of which should show:

  • a CNAME record from hostname ‘gold’ to ‘puppet.blab.renf.ro’
  • an A record from hostname ‘puppet’ to ‘192.168.1.3’
  • a PTR record from hostname ‘2’ to ‘puppet.blab.renf.ro’

Removing DNS Records with Bolt

The PowerShell script to remove DNS records (remove_dns_record.ps1) is a bit longer, since it includes logic to find the correct reverse DNS zone without having to specify it explicitly:

[CmdletBinding(PositionalBinding=$false)]
param (
  [Parameter(Mandatory=$false)]
  [string]
  $name,
  [Parameter(Mandatory=$true)]
  [string]
  $domain,
  [Parameter(Mandatory=$false)]
  [string]
  $cname,
  [Parameter(Mandatory=$false)]
  [string]
  $ip
)
if (($name -ne "" -and $cname -ne "" -and $ip -ne "") -or
    ($name -ne "" -and $cname -ne "") -or
    ($name -ne "" -and $ip -ne "")) {
  # Continue with record removal

  # Lots of this lifted from comments at
  # https://rcmtech.wordpress.com/2014/02/26/get-and-delete-dns-a-and-ptr-records-via-powershell/
  # Requires Server 2012R2 or later
  $DNSARecord = Resolve-DnsName ("{0}.{1}" -f $name, $domain)
  $DNSPtrRecord = Resolve-DnsName $DNSARecord.IPAddress
  $DNSReverseZone = (Get-DnsServerZone | ?{$DNSPtrRecord.Name -match $_.ZoneName -and $_.IsDsIntegrated -eq $true}).ZoneName
  $PtrHostName = $DNSARecord.IPAddress -split "\."
  [array]::Reverse($PtrHostName)
  $DNSReverseZoneSuffix = $DNSReverseZone -replace ".in-addr.arpa",""
  $PtrHostName = $PtrHostName -join "." -replace $DNSReverseZoneSuffix,"" -replace "\.$",""

  if ($name -ne "" -and $cname -ne "" -and $ip -ne "") {
    # Remove CNAME record, remove A and PTR records
    Remove-DnsServerResourceRecord -RRType "CNAME" -Name $cname -ZoneName $domain -Force
    Remove-DnsServerResourceRecord -RRType "A" -Name $name -RecordData $ip -ZoneName $domain -Force
    exit 0
  }
  elseif ($name -ne "" -and $ip -ne "") {
    # Remove A and PTR records
    Remove-DnsServerResourceRecord -RRType "A" -Name $name -RecordData $ip -ZoneName $domain -Force
    exit 0
  }
  elseif ($name -ne "" -and $cname -ne "") {
    # Remove CNAME record
    Remove-DnsServerResourceRecord -RRType "CNAME" -Name $cname -ZoneName $domain -Force
    exit 0
  }
} else {
  write-host "Must specify: name, cname, ip; name, cname; name, ip"
  exit 1
}

The metadata JSON (remove_dns_record.json) looks like the others:

{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Remove DNS A, PTR, CNAME records.",
  "parameters": {
    "name": {
      "description": "The host name.",
      "type": "Optional[String]"
    },
    "domain": {
      "description": "The domain name.",
      "type": "String"
    },
    "cname": {
      "description": "The host CNAME (alias).",
      "type": "Optional[String]"
    },
    "ip": {
      "description": "The host IP.",
      "type": "Optional[String]"
    }
  }
}

Rather than remove any existing DNS records for the domain controller or the gold server, we’ll create a set of new throwaway records, verify them, remove them, and verify their removal.

Creation:

bolt task run --targets=dc renfro::create_dns_record name=foo domain=blab.renf.ro ip=192.168.1.4 cname=bar

Verification:

bolt command run "Get-DnsServerResourceRecord -ZoneName blab.renf.ro" --target dc
bolt command run "Get-DnsServerResourceRecord -ZoneName 1.168.192.in-addr.arpa" --target dc

Removal:

bolt task run --targets=192.168.1.3 renfro::remove_dns_record name=foo domain=blab.renf.ro ip=192.168.1.4 cname=bar

Verification:

bolt command run "Get-DnsServerResourceRecord -ZoneName blab.renf.ro" --target dc
bolt command run "Get-DnsServerResourceRecord -ZoneName 1.168.192.in-addr.arpa" --target dc

Using Active Directory DNS on the Gold Server

Now that the domain controller’s DNS is managed from the gold server, we can point the gold server’s /etc/resolv.conf to it instead of pfSense. By default, CentOS 7 uses NetworkManager to manage /etc/resolv.conf, so use either nmcli or nmtui to change the DNS settings for the default Ethernet interface.

When complete, you should be able to ping both dc1.ad.blab.renf.ro and gold.blab.renf.ro.

Setting up Name Resolution from Outside the Lab Network

To resolve the lab DNS entries outside the lab network, you’ll need:

  • a firewall rule in pfSense allowing DNS queries to the domain controller on both TCP and UDP port 53, and
  • have the home DNS servers forward lab domain queries to the domain controller.

On the home DNS servers running BIND9 on Debian, add:

zone "blab.renf.ro" {
    type forward;
    forwarders { 192.168.1.3; };
    };
zone "1.168.192.in-addr.arpa" {
    type forward;
    forwarders { 192.168.1.3; };
    };

to /etc/bind/named.conf.local, add empty-zones-enable no; to the options structure in /etc/bind/named.conf.options, and restart the BIND service.

Test outside name resolution with:

nslookup gold.blab.renf.ro
nslookup 192.168.1.2

Lab Domain DHCP Setup

The manual PowerShell commands to install and configure DHCP equivalent to pfSense would be:

install-windowsfeature dhcp

and:

add-dhcpserverindc -dnsname dc1.blab.renf.ro -ipaddress 192.168.1.3
set-dhcpserverv4dnssetting -dynamicupdates always -deletednsrronleaseexpiry $true
add-dhcpserverv4scope -name 'blab.renf.ro' -startrange 192.168.1.1 -endrange 192.168.1.254 -subnetmask 255.255.255.0 -state active
add-dhcpserverv4exclusionrange -scopeid 192.168.1.0 -startrange 192.168.1.1 -endrange 192.168.1.99
add-dhcpserverv4exclusionrange -scopeid 192.168.1.0 -startrange 192.168.1.200 -endrange 192.168.1.254
set-dhcpserverv4optionvalue -optionid 3 -value 192.168.1.1 -scopeid 192.168.1.0
set-dhcpserverv4optionvalue -dnsdomain blab.renf.ro -scopeid 192.168.1.0 -dnsserver 192.168.1.3
restart-service dhcpserver

Creating a DHCP Scope with Bolt

As before, we’ll replace those commands with a Bolt task named create_dhcp_server. The contents of create_dhcp_server.ps1 are:

[CmdletBinding(PositionalBinding=$false)]
param (
  [Parameter(Mandatory=$true)]
  [string]
  $dhcpserver,
  [Parameter(Mandatory=$true)]
  [string]
  $dhcpserverip,
  [Parameter(Mandatory=$true)]
  [string]
  $scopeid,
  [Parameter(Mandatory=$true)]
  [string]
  $scopename,
  [Parameter(Mandatory=$true)]
  [string]
  $netmask,
  [Parameter(Mandatory=$true)]
  [string]
  $scopestartrange,
  [Parameter(Mandatory=$true)]
  [string]
  $scopeendrange,
  [Parameter(Mandatory=$true)]
  [string]
  $exclusionrange,
  [Parameter(Mandatory=$true)]
  [string]
  $router,
  [Parameter(Mandatory=$true)]
  [string]
  $dnsserverip
)
try {
  install-windowsfeature dhcp
} catch {
  Write-Output "Cannot install dhcp windows feature"
  exit 1
}
try {
  add-dhcpserverindc -dnsname $dhcpserver -ipaddress $dhcpserverip
  set-dhcpserverv4dnssetting -dynamicupdates always `
    -deletednsrronleaseexpiry $true
  add-dhcpserverv4scope -name $scopename -startrange $scopestartrange `
    -endrange $scopeendrange -subnetmask $netmask -state active
  foreach ($range in ($exclusionrange -split ",")) {
    $start = ($range -split "-")[0]
    $end = ($range -split "-")[1]
    add-dhcpserverv4exclusionrange -scopeid $scopeid `
      -startrange $start -endrange $end
  }
  set-dhcpserverv4optionvalue -optionid 3 -value $router -scopeid $scopeid
  set-dhcpserverv4optionvalue -dnsdomain blab.renf.ro -scopeid $scopeid `
    -dnsserver $dnsserverip
  restart-service dhcpserver
} catch {
  exit 2
}

and the contents of the create_dhcp_scope.json JSON metadata are:

{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Create DHCP scope and server.",
  "parameters": {
    "dhcpserver": {
      "description": "The DHCP server name.",
      "type": "String"
    },
    "dhcpserverip": {
      "description": "The DHCP server IP address.",
      "type": "String"
    },
    "scopeid": {
      "description": "The DHCP scope ID (network segment address).",
      "type": "String"
    },
    "scopename": {
      "description": "The DHCP scope name (network segment name).",
      "type": "String"
    },
    "netmask": {
      "description": "The DHCP scope netmask (dotted form).",
      "type": "String"
    },
    "scopestartrange": {
      "description": "The DHCP scope starting IP address.",
      "type": "String"
    },
    "scopeendrange": {
      "description": "The DHCP scope ending IP address.",
      "type": "String"
    },
    "exclusionrange": {
      "description": "The DHCP scope exclusion ranges (A-B,C-D,...).",
      "type": "String"
    },
    "router": {
      "description": "The DHCP scope default router IP.",
      "type": "String"
    },
    "dnsserverip": {
      "description": "The DHCP scope DNS server IP.",
      "type": "String"
    }
  }
}

Create the DHCP scope with Bolt by running:

bolt task run --targets dc renfro::create_dhcp_scope dhcpserver=dc1.ad.blab.renf.ro dhcpserverip=192.168.1.3 scopeid=192.168.1.0 scopename=blab.renf.ro netmask=255.255.255.0 scopestartrange=192.168.1.1 scopeendrange=192.168.1.254 exclusionrange=192.168.1.1-192.168.1.99,192.168.1.200-192.168.1.254 router=192.168.1.1 dnsserverip=192.168.1.3

Verify the new DHCP scope settings are correct with:

bolt command run "get-dhcpserverv4scope" --target dc
bolt command run "get-dhcpserverv4scope -scopeid 192.168.1.0" --target dc
bolt command run "get-dhcpserverv4exclusionrange -scopeid 192.168.1.0" --target dc
bolt command run "get-dhcpserverv4optionvalue -scopeid 192.168.1.0" --target dc

Disable the DHCP service on pfSense, then disconnect and reconnect the Xubuntu system from the network segment to verify it gets an IP from the domain controller.

Managing DHCP Reservations with Bolt

In the long run, we may want to have future hosts get their IP addresses over DHCP, but we’ll probably want to reserve those IPs to enable easier firewall rule management.

In manual PowerShell, we’d normally run:

Add-DhcpServerv4Reservation -ScopeId 192.168.1.0 -IPAddress 192.168.1.4 -ClientId "00-22-44-66-88-AA" -Description "foo.blab.renf.ro"

and

Remove-DhcpServerv4Reservation -IPAddress 192.168.1.4

Since there’s only one command for each operation and no duplicated variables, there’s little need to make a Bolt task just for DHCP reservations. But when we start provisioning new hosts automatically, we’d like the host to:

  • boot over the network (PXE)
  • determine its own hostname via reverse DNS lookup

Combining these steps would require a process with parameters of a hostname, an IP address, and a MAC address. The hostname and IP would be used to create forward and reverse DNS records, and the MAC address and IP would be used for a DHCP reservation. We’ve already got a Bolt task to manage DNS, so let’s write up a simple Bolt task to manage DHCP.

Creating DHCP Reservations

The PowerShell script create_dhcp_reservation.ps1 contains:

[CmdletBinding(PositionalBinding=$false)]
param (
  [Parameter(Mandatory=$true)]
  [string]
  $scope,
  [Parameter(Mandatory=$true)]
  [string]
  $ip,
  [Parameter(Mandatory=$true)]
  [string]
  $mac,
  [Parameter(Mandatory=$true)]
  [string]
  $hostname
)
$mac = $mac -replace ":","-"
Add-DhcpServerv4Reservation -ScopeId $scope -IPAddress $ip -ClientId $mac `
  -Description $hostname

The metadata JSON create_dhcp_reservation.json contains:

{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Create DHCP reservation.",
  "parameters": {
    "scope": {
      "description": "The DHCP scope (network address).",
      "type": "String"
    },
    "ip": {
      "description": "The DHCP client IP address.",
      "type": "String"
    },
    "mac": {
      "description": "The DHCP client MAC address.",
      "type": "String"
    },
    "hostname": {
      "description": "The DHCP client hostname.",
      "type": "String"
    }
  }
}

Run the task with:

bolt task run renfro::create_dhcp_reservation scope=192.168.1.0 ip=192.168.1.4 mac=00:50:56:22:44:66 hostname=xubuntu --targets dc

and disconnect and reconnect the Xubuntu system’s network interface to verify it gets a new IP of 192.168.1.4.

Removing DHCP Reservations

The PowerShell script remove_dhcp_reservation.ps1 contains:

[CmdletBinding(PositionalBinding=$false)]
param (
  [Parameter(Mandatory=$true)]
  [string]
  $ip
)
$mac = $mac -replace ":","-"
Remove-DhcpServerv4Reservation -IPAddress $ip

The metadata JSON remove_dhcp_reservation.json contains:

{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Remove DHCP reservation.",
  "parameters": {
    "ip": {
      "description": "The DHCP client IP address.",
      "type": "String"
    }
  }
}

Run the task with:

bolt task run renfro::remove_dhcp_reservation ip=192.168.1.4 --targets dc

and disconnect and reconnect the Xubuntu system’s network interface to verify it gets a new IP between 192.168.1.100 and 192.168.1.199.

Combining DHCP and DNS Settings to Provision New Hosts and Retire Old Hosts with a Bolt Plan

We used a Bolt plan during the creation of the Active Directory domain because it let us prompt for a safe-mode administator password. We’ll use one here for its original purpose: orchestrating tasks into a higher-level workflow. Since new server hosts will need a DHCP reservation and a DNS entry, we’ll create a plan to call our earlier tasks that manage DHCP and DNS entries. When a server host is retired, we’ll want to release its DHCP reservation and clear its DNS entries. The same principle applies, and we’ll make a second plan for clearing those settings.

Im the Boltdir/site-modules/renfro/plans directory, we make two plan files using the Puppet language. The first is provision_dhcp_dns.pp, which contains:

plan renfro::provision_dhcp_dns(
  TargetSpec $dc,
  String $hostname,
  String $domain,
  String $ip,
  String $mac,
  String $scope
) {
  run_task('renfro::create_dhcp_reservation', $dc, scope => $scope, ip => $ip,
           mac => $mac, hostname => $hostname)
  run_task('renfro::create_dns_record', $dc, name => $hostname, ip => $ip,
           domain => $domain)
}

The second is clear_dhcp_dns.pp, which contains:

plan renfro::clear_dhcp_dns(
  TargetSpec $dc,
  String $hostname,
  String $domain,
  String $ip
) {
  run_task('renfro::remove_dhcp_reservation', $dc, ip => $ip)
  run_task('renfro::remove_dns_record', $dc, name => $hostname, ip => $ip,
           domain => $domain)
}

They can be examined at the command line using bolt plan show. Execute the provision_dhcp_dns plan for the Xubuntu host by running:

bolt plan run renfro::provision_dhcp_dns dc=dc hostname=xubuntu domain=blab.renf.ro ip=192.168.1.4

and verify that the forward and reverse DNS records for xubuntu.blab.renf.ro are correct. Disconnect and reconnect the Xubuntu system’s network interface to verify it gets an IP address of 192.168.1.4.

Execute the clear_dhcp_dns plan for the Xubuntu host by running:

bolt plan run renfro::clear_dhcp_dns dc=dc hostname=xubuntu domain=blab.renf.ro ip=192.168.1.4

and verify that the forward and reverse DNS records for xubuntu.blab.renf.ro are deleted. Disconnect and reconnect the Xubuntu system’s network interface to verify it gets a new IP between 192.168.1.100 and 192.168.1.199.