Showing posts with label dhcp server. Show all posts
Showing posts with label dhcp server. Show all posts

Monday, March 10, 2025

A Minimal Container Image: ISC DHCP Server - From Scratch

ISC DHCP Server - From Scratch

Customizing a Docker software container is remarkably easy, but that ease imposes a penalty: Image size and layer dependency. This post describes how to create a container image with buildah, a Docker alternative that gives much more control than a simple Dockerfile can offer. The image contains only the service binary and the required libraries, dramatically reducing the container image size and reducing it to a single layer. This image is suitable for use as a systemd service on a container optimized OS like Fedora CoreOS.

The ISC DHCP server, running in the regular mode without backing servers like ldap, is a single process running from a single binary executable. A base image like fedora-minimal provides only package management tools, and those tools remain as a layer of the final image. Installing the dhcp-server package draws in an additional 10 packages and those too remain in the new image layer but are unused by the DHCP service. The dhcp-fedora image described in ISC DHCP - Fedora Base the base image is ~ 146MB and the service layer is an additional 27MB. The base image contains over 30,000 files that are entirely unused during operation.

If the service runs as a single process from a single binary, how small can a functional container image be?

Modern Executable Files - Dynamic Linking and Shared Objects

Modern operating systems depend heavily on dynamic linking and shared objects. A shared object is a file of compiled code that provides a set of functions that are commonly used by many binaries. This reduces the size of those binaries because all those functions do not need to be duplicated in every statically linked binary. Every dynamically linked binary depends on a set of shared objects to resolve all of those dangling function bindings.

To build a working container image from scratch, it must at least contain the service binary and any shared libraries that it depends on. Those files must identified and retrieved. They must be arranged in the container image so that they can be located and linked when the server binary is invoked.

This two-step procedure is divided into two scripts, one to create a model of the file tree that will be placed into the image and a second to produce the image itself.

  • create-model-tree.sh
    Given the name of a binary file that is provided by a package in the Fedora YUM repositories, create a model tree of the binary and libraries is requires.

  • minimal-dhcpd.sh
    Given a model file tree for a DHCP container, populate the the container image filesystem and create the image.

Creating a Model File Tree

A container is (in the simplest form) a single process running from a single binary. In this example that’s the DHCP server running from /usr/sbin/dhcpd. When the container image is instantiated, the process is started with a command line that invokes that binary.

The container image must include the binary file and any files and resources that the binary operation depends on. Rather than installing packages on top of a base image, the files can be extracted directly from those packages and copied into a model file tree. This is directory tree that mirrors the final content of the the image file tree (or the file layout of a real OS containing only those files).

Note
This process is completely defined in create-model-tree.sh This document only highlights the specific steps.

This example uses RPM packages from the Fedora distribution, but the method applies to any Linux distro package system.

Examine the Binary

Since the runtime binary file is known the first step is to download the service package file and unpack that package for examination.

The script creates a workspace with subdirectories to contain all of the downloaded package files and to contain the unpacked file trees. Each package is unpacked into its own tree to keep the contents distinct from all of the others.

Identify the Service Package

The dnf command is used to search remote package repositories and the local system for packages and the files they provide.

dnf provides --quiet dhcpd
dhcp-server-12:4.4.3-14.P1.fc41.x86_64 : Provides the ISC DHCP server
Repo         : fedora
Matched From :
Filename     : /usr/sbin/dhcpd

This command lists the full package name of he current package for the current system and the full path to the file on the last line.

Pull the Service Package

Once the package has been identified, it can be retrieved and stored:

dnf --quiet download --arch ${arch} --destdir ${pkg_dir} ${full_name}

The parameters are provided by the script.

  • arch: The machine architecture of the new container

  • pkg_dir: Where to place the downloaded file

  • full_name: The package name determined in the previous step

Unpack the Service Package

RPM files are a specialized compressed file archive. An RPM must first be converted to a cpio archive before unpacking into the local filesystem.

rpm2cpio ${package_path} | cpio -idmu --quiet --directory ${unpack_dir}
  • package_path: The path to the service package including the file name

  • unpack_dir: The target for unpacking the package tree
    This directory must have been created before unpacking. The script creates a separate root directory for each package so that the contents of one does not pollute or conflict with any others.

Identify Shared Libraries

Most binaries on Linux are dynamically linked. To run a dynamically linked binary the required libraries must be placed in the location where the dynamic linker expects them to be.

The ldd tool examines a dynamically linked binary and reports the libraries it expects to find.

ldd ${unpack_dir}/${binary_file}
	linux-vdso.so.1 (0x0000ffffb7a1c000)
	libkrb5.so.3 => /lib64/libkrb5.so.3 (0x0000ffffb7640000)
	liblber.so.2 => /lib64/liblber.so.2 (0x0000ffffb7610000)
	libldap.so.2 => /lib64/libldap.so.2 (0x0000ffffb7590000)
	libsystemd.so.0 => /lib64/libsystemd.so.0 (0x0000ffffb7480000)
	libc.so.6 => /lib64/libc.so.6 (0x0000ffffb72b0000)
	/lib/ld-linux-aarch64.so.1 (0x0000ffffb79e0000)
	libk5crypto.so.3 => /lib64/libk5crypto.so.3 (0x0000ffffb7270000)
	libcom_err.so.2 => /lib64/libcom_err.so.2 (0x0000ffffb7240000)
	libkrb5support.so.0 => /lib64/libkrb5support.so.0 (0x0000ffffb7210000)
	libkeyutils.so.1 => /lib64/libkeyutils.so.1 (0x0000ffffb71c0000)
	libcrypto.so.3 => /lib64/libcrypto.so.3 (0x0000ffffb6db0000)
	libresolv.so.2 => /lib64/libresolv.so.2 (0x0000ffffb6d80000)
	libevent-2.1.so.7 => /lib64/libevent-2.1.so.7 (0x0000ffffb6cf0000)
	libsasl2.so.3 => /lib64/libsasl2.so.3 (0x0000ffffb6c90000)
	libssl.so.3 => /lib64/libssl.so.3 (0x0000ffffb6ba0000)
	libcap.so.2 => /lib64/libcap.so.2 (0x0000ffffb6b50000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000ffffb6b10000)
	libselinux.so.1 => /lib64/libselinux.so.1 (0x0000ffffb6ab0000)
	libz.so.1 => /lib64/libz.so.1 (0x0000ffffb6a70000)
	libcrypt.so.2 => /lib64/libcrypt.so.2 (0x0000ffffb6a20000)
	libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x0000ffffb6950000)
  • unpack_dir: The root of the directory containing the unpacked file trees

  • binary_file: The absolute path to the binary in the unpacked tree
    In this case: /usr/sbin/dhcpd

Each line of this output represents a required shared library. Most lines in this output contain three elements:

  1. The name of the required library

  2. The absolute path of the file containing the library

  3. The memory location where the library is loaded

Only the absolute path is useful for our purposes.

There are two lines that are different from the others. Both relate to the operation of the dynamic linker.

The linux-vdso.so.1 is a virtual file that is provided by the kernel to to all processes in user space. This line can be ignored.

The other is the dynamic linking library, /lib/ld-linux-aarch64.so.1. It does not present a "file name" because only the path matters. This library implements the dynamic linking operations for the rest.

With a little processing this output results in a list of files with absolute pathnames. These can be used in the same way as the binary file name to identify the containing package.

Resolve the Shared Libraries

The next few steps must be done for each of the shared libraries indicated. Note that some of the packages provide more than one of these libraries, so it is beneficial, for each library, to check if the package has already been downloaded and unpacked before proceeding.

Identify a Shared Library Package

The library packages can be identified using the same dnf provide command as was used for the dhcp-server package, with one exception.

The Linux Filesystem Hierarchy Standard defines two possible locations for libraries. These are /lib and /usr/lib. 64-bit systems add two more, /lib64 and /usr/lib64. Most distributions now symlink the top level directories to those in /usr.

ls -l /lib*
lrwxrwxrwx. 1 root root 7 Jan  1  1970 /lib -> usr/lib
lrwxrwxrwx. 1 root root 9 Jan  1  1970 /lib64 -> usr/lib64

This means that the path given by ldd may not be the path that the package publishes for the file. Fortunately, the dnf provide command can take multiple paths and any that don’t resolve are ignored.

In this example libpath is /lib64/libkrb5.so.3

dnf --quiet provides ${libpath} /usr${libpath} 2>/dev/null | head -4
krb5-libs-1.21.3-3.fc41.aarch64 : The non-admin shared libraries used by Kerberos 5
Repo         : @System
Matched From :
Filename     : /usr/lib64/libkrb5.so.3

The full package name is the first word on the first line. This can be tokenized down to 4 components:

  • krb5-libs-1: The package name

  • 1.21.3-3 : The major, minor, release and build numbers

  • fc41: Fedora version 41

  • aarch64: The machine architecture

Only the first element is needed to locate the package.

Note
This package name is an example of one variation that must be accounted for. Some package names end with a hyphenated number -1 or some other integer. I’m not sure what the value represents but it will interfere with package lookup. If the download with the full name fails to find a package, try it with the name minus that trailing string.

Retrieve a Shared Library Package

Downloading the library packages works in the same way as the dhcp-server package did. For this example the enviroment variables are:

  • package_name: krb5-libs

  • package_dir: The workspace for downloaded packages

dnf download ${package_name} --destdir ${package_dir}
Updating and loading repositories:
Repositories loaded.
Downloading Packages:
  krb5-libs-0:1.21.3-4.fc41.aarch64                           100% | 772.7 KiB/s | 763.4 KiB |  00m01s

The output indicates the actual package version retrieved. This command also accepts the --quiet option for scripting and parsing. If the package is already present it will indicate that and exit.

Unpack a Shared Library Package

Unpacking the library packages is done in the same way as it was for the dhcp-server package. Each package should be unpacked into a dedicated root directory to prevent the packages from overlaying each other.

rpm2cpio ${package_path} | cpio -idmu --quiet --directory ${unpack_dir}
  • package_path: The path to the service package including the file name

  • unpack_dir: The target for unpacking the package tree
    This directory must have been created before unpacking. The script creates a separate root directory for each package so that the contents of one does not pollute or conflict with any others.

Populate the Model Tree

At this point all of the required packages are unpacked and all of the required files have been located by the package name and an absolute path from the root of the unpack tree. The model tree must be prepared for the the binary and library files.

mkdir ${model_root}
ln -s usr/lib ${model_root}/lib
ln -s usr/lib64 ${model_root}/lib64
mkdir -p ${model_root}/usr/lib
mkdir -p ${model_root}/usr/lib64
mkdir -p ${model_root}/usr/sbin

Most of the shared library files that ldd reported are actually symbolic links to a matching file with an additional version number.

For example, the libkrb5.so.3 library is a symlink to libkrb5.so.3.3.

(cd ${workdir} ; ls -l usr/lib64/libkrb5.so.*)
lrwxrwxrwx. 1 core core     14 Feb 11 00:00 usr/lib64/libkrb5.so.3 -> libkrb5.so.3.3
-rwxr-xr-x. 1 core core 873304 Feb 11 00:00 usr/lib64/libkrb5.so.3.3

It may be possible to copy the library to the short name but for rigor the script copies the file to the correct name and reproduces the symlink as it is created by the package.

The final result looks like this:

(cd ${model_root} ; ls -lgGR *)
lrwxrwxrwx. 1  7 Mar  4 15:23 lib -> usr/lib
lrwxrwxrwx. 1  9 Mar  4 15:23 lib64 -> usr/lib64

usr:
total 4
drwxr-xr-x. 2   35 Mar  4 15:23 lib
drwxr-xr-x. 2 4096 Mar  4 15:23 lib64

usr/lib:
total 816
-rwxr-xr-x. 1 832552 Mar  4 15:23 ld-linux-aarch64.so.1

usr/lib64:
total 12584
-rwxr-xr-x. 1 2301232 Mar  4 15:23 libc.so.6
lrwxrwxrwx. 1      14 Mar  4 15:23 libcap.so.2 -> libcap.so.2.70
-rwxr-xr-x. 1  200816 Mar  4 15:23 libcap.so.2.70
lrwxrwxrwx. 1      17 Mar  4 15:23 libcom_err.so.2 -> libcom_err.so.2.1
-rwxr-xr-x. 1   69296 Mar  4 15:23 libcom_err.so.2.1

... <lines elided>

lrwxrwxrwx. 1      21 Mar  4 15:23 libz.so.1 -> libz.so.1.3.1.zlib-ng
-rwxr-xr-x. 1  136752 Mar  4 15:23 libz.so.1.3.1.zlib-ng

usr/sbin:
total 2492
-rwxr-xr-x. 1 2548720 Mar  4 15:23 dhcpd

The model tree now contains the dhcpd binary and all of the required library files.

Building a Minimal Container Image

The idea of building a minimal container image is to decrease the amount of data that must be downloaded initially and downloaded again when the container image is updated and rebuilt (and the base image is updated underneath it). The ratio of size of the runtime required bits to the installation overhead is surprsingly large.

The other reason to minimize an image is that it decreases the attack surface of a container process by removing any files that aren’t critical to operation. Containers are not a security mechanism. If a cracker manages to exploit the running process and gain access to the container filesystem, the fewer resources the container gives them the better.

Note
This section only shows the highlights of this procedure. The procedure is fully described in the minimal-dhcpd.sh script.
Initialize a new container build
container_id=$(buildah from scratch)

The command above starts a container build procedure. It initializes a file space and metadata that will be manipulated in the steps that follow.

When building a container image using a distro base image, you get the access to the package management system and the distro repositories. When building from scratch you have to provide all of the image files and place them in a file tree that matches the expected structure for the application to run. Since the scratch image doesn’t have tools like mkdir, it’s not possible to use buildah run commands to manipulate the container file system.

The solution is to loopback mount the image filesystem onto the operating system and then use the OS tools to create the file tree. This is where buildah stands out.

buildah unshare for Rootless Containers

As Dan Walsh explains in a blog post on buildah unshare, the common build commands, run and copy, create a new namespace where the user appears to be UID 0 (root) and mount the image filesystem so that they can operate on the files in the image and then destroy that namespace before returning.

The common buildah commands do one thing at a time. Without a base image containing a shell, the run command isn’t useful. The copy command can import single files or the contents of a single directory into a single target directory, but it doesn’t offer recursive copies and the destination must already exist inside the container image.

The buildah unshare command creates a new namespace in the same way as the other commands, but it runs a shell inside that namespace that makes it possible for the caller to access the container filesystem without root access to the host system. For the purpose here this allows the user to loopback mount the container filesystem and copy the model file tree into it.

An example of buildah unshare
user@hostname:~/dhcpd-container$ buildah unshare
root@hosthame:~/dhcpd-container# id
uid=0(root) gid=0(root) groups=0(root)...
root@hostname:~/dhcpd-container# lsns
        NS TYPE   NPROCS   PID USER COMMAND
4026531834 time        3  4862 root buildah-in-a-user-namespace unshare
4026531835 cgroup      3  4862 root buildah-in-a-user-namespace unshare
4026531836 pid         3  4862 root buildah-in-a-user-namespace unshare
4026531838 uts         3  4862 root buildah-in-a-user-namespace unshare
4026531839 ipc         3  4862 root buildah-in-a-user-namespace unshare
4026531840 net         3  4862 root buildah-in-a-user-namespace unshare
4026532291 user        3  4862 root buildah-in-a-user-namespace unshare
4026532293 mnt         3  4862 root buildah-in-a-user-namespace unshare
root@hostname:~/dhcpd-container# env | grep BUILDAH
BUILDAH_ISOLATION=rootless
root@hostname:~/dhcpd-container# exit
user@hostname:~/dhcpd-container$

The fragment above shows what buildah unshare is doing.

All of the buildah commands can be run within the unshare namespace, but the only ones that require it for this procedure are the mount and unmount commands. The image build script can be run either way and will unshare for the copy steps if needed.

To make the container filesystem available, the unshare command takes the container id in Building a Minimal Container Image above.

Create a mock-root namespace for container filesystem access
buildah unshare ${container_id}

Rather than requiring the user to call buildah unshare before invoking the script, it checks to see if it’s already running in an unshare environment. If not, it calls itself again with unshare. Then it calls the copy_model_tree() function to mount the container filesystem and copy the model tree into it.

Re-call the script with unshare if needed.
# ...
if [ -z "${BUILDAH_ISOLATION}" ] ; then
    # Run the file copy in an unshare environement
    buildah unshare bash $0 -c ${container} -s ${SOURCE_ROOT}
else
    # Aldready in an unshare environment
    copy_model_tree ${SOURCE_ROOT} ${container}
fi
# ...

Copy the Model Tree

The critical step in creating a container is populating the filesystem for the image. For an image using a distro base, this is done with the distro package manager. Single files are added using the copy command.

For a minimal image, the file tree must be created and the files placed without access to tools inside the container base. The solution is to mount the container image filesystem onto the build host and copy the files in directly using the host tools.

The bash function below assumes that the process is already in an unshare environment. It mounts the container filesystem, copies the contents of a file tree into the image file tree recursively. It creates two directories required for the application configuration and data volumes. Finally it unmounts the container image and returns.

copy_model_tree function
function copy_model_tree() {
    local source_root=$1
    local container_id=$2

    # Access the container file space
    local mountpoint=$(buildah mount $container_id)

    # Create the model directory tree
    (cd ${source_root} ; find * -type d) | xargs -I{} mkdir -p ${mountpoint}/{}
    # Copy the model tree to the image filesystem.
    cp -r ${source_root}/* ${mountpoint}

    # Create volume mount points
    mkdir -p ${mountpoint}/etc/dhcp
    mkdir -p ${mountpoint}/var/lib/dhcpd

    # Release the container file space
    buildah unmount ${container_id}
}

The separate mkdir line insures that symlinks to directories in the model tree aren’t created in place of real directories.

Define Container Operation

The final container definition steps are identical to those for a distro based image.

Define container operation and metadata
# add a volume to include the configuration file
# Leave the files in the default locations
buildah config --volume /etc/dhcp/dhcpd.conf $container
buildah config --volume /var/lib/dhcpd $container

# open ports for listening
buildah config --port 68/udp --port 69/udp ${container}

# Define the startup command
buildah config --cmd "/usr/sbin/dhcpd -d --no-pid" $container

buildah config --author "${AUTHOR}" $container
buildah config --created-by "${BUILDER}" $container
buildah config --annotation description="ISC DHCPD 4.4.3" $container
buildah config --annotation license="MPL-2.0" $container

# Save the container to an image
buildah commit --squash $container dhcpd

This fragment defines the configuration volumes, opens the required ports and sets the image metadata before committing and naming the image within the local container namespace.

Review the new container image
podman image inspect localhost/dhcpd |
  jq '.[0] | {"Id": .Id, "Size": .Size, "Config": .Config }'
{
  "Id": "aacc40467b44590ece02a7c68c4e00ac6fcafaa08d7914452618f622cd65a445",
  "Size": 16260301,
  "Config": {
    "ExposedPorts": {
      "68/udp": {},
      "69/udp": {}
    },
    "Cmd": [
      "/usr/sbin/dhcpd",
      "-d",
      "--no-pid"
    ],
    "Volumes": {
      "/etc/dhcp/dhcpd.conf": {},
      "/var/lib/dhcpd": {}
    },
    "WorkingDir": "/",
    "Labels": {
      "io.buildah.version": "1.39.0"
    }
  }
}

You can always examine a container image this way to determine the run-time parameters. The full report is significantly bigger and more detailed.

Summary

As noted, this container image runs in exactly the same way as the Fedora based image. The real payoff is in the the size savings.

Compare scratch and distro based image size
podman images | grep dhcp
localhost/dhcpd                            latest      aacc40467b44  25 hours ago  16.3 MB
localhost/dhcpd-fedora                     latest      4581f80d82a6  2 days ago    172 MB

Monday, May 19, 2014

Robust and Flexable DHCP and provisioning: An LDAP backed DHCP service.

In the last post I created an empty LDAP database ready to accept content. In this one I mean to add a DHCP service configuration for a single subnet and a test host entry.

This section is a long argument describing the advantages of using a backing database for DHCP. You can skip it if you're already convinced.

Why use a database?


There are significant reasons to use a proper database (yes, LDAP is a database) for DHCP management.

  • Update without restart
  • Avoid ad hoc file parsing or generation
  • Reduce configuration sites

The use of a flat file for configuration and data, the use of an inaccessible in-memory database and the network limitations of the DHCP protocol all pose problems for all but the smallest networks.  Backing the DHCP services with a database can address all three.

Testing: Emit and Collect Test DHCP Queries - dhtest


It turns out that there aren't many tools for testing DHCP responses. I found several but they were only in source code. The one I decided to use is called dhtest and it's available from Github:
https://github.com/saravana815/dhtest

It builds cleanly on Fedora 19 and 20.
git clone https://github.com/saravana815/dhtest
Cloning into 'dhtest'...
remote: Reusing existing pack: 53, done.
remote: Total 53 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (53/53), done.
cd dhtest
make
gcc    -c -o dhtest.o dhtest.c
gcc    -c -o functions.o functions.c
gcc dhtest.o functions.o -o dhtest

When it runs successfully this is what it looks like

sudo ./dhtest --mac 0a:00:00:00:00:01 \
  --interface p2p1 --server 10.0.2.15 --verbose
DHCP discover sent  - Client MAC : 0a:00:00:00:00:01
DHCP offer received  - Offered IP : 10.0.2.16

DHCP offer details
----------------------------------------------------------
DHCP offered IP from server - 10.0.2.16
Next server IP(Probably TFTP server) - 10.0.2.4
Subnet mask - 255.255.255.0
Router/gateway - 10.0.2.2
DNS server - 10.0.2.3
Lease time - 1 Days 0 Hours 0 Minutes
DHCP server  - 10.0.2.2
----------------------------------------------------------

DHCP request sent  - Client MAC : 0a:00:00:00:00:01
DHCP ack received  - Acquired IP: 10.0.2.16

DHCP ack details
----------------------------------------------------------
DHCP offered IP from server - 10.0.2.16
Next server IP(Probably TFTP server) - 10.0.2.4
Subnet mask - 255.255.255.0
Router/gateway - 10.0.2.2
DNS server - 10.0.2.3
Lease time - 1 Days 0 Hours 0 Minutes
DHCP server  - 10.0.2.2
----------------------------------------------------------

Procedure


Finally I get to the actual process of creating the DHCP service.  First the ingredients and a summary of the process. Then the details.

Ingredients


Before starting there are a set of parameters that should be defined.  The DHCP server will need to gain access to the LDAP service and the DHCP server configuration in the LDAP database must reflect the network on which the DHCP server resides.  I also add one dummy test host that I can use for validation.

LDAP Server
LDAP Server Hostnameldap.example.com
Database DNdc=example,dc=com
Admin Username (DN)dc=Manager,dc=example,dc=com
Admin Passwordchangeme

Subnet  Specification
Base Address10.0.2.0
Netmask/24
Gateway10.0.2.2
DNS Servers10.0.2.3
NTP Servers10.0.2.3

Host Entry
MAC Address0a:00:00:00:00:01
IP Address10.0.2.16

Recipe


Running DHCP with LDAP (conceptually) requires two different servers. You can run them both on the same host if you want. Adjust your IP addresses and hostnames to your environment.
  1. On the LDAP server
    1. Prepare the LDAP database for DHCP configuration
      1. Convert the DHCP schema file to LDIF
      2. Import the DHCP schema (as LDIF) into the cn=config database
    2. Convert the DHCP config to LDIF and load it into the database
      1. dhcpServer
      2. dhcpService
      3. dhcpSubnet
      4. dhcpHost
  2. On the DHCP server
    1. Prepare logging
    2. Verify LDAP connectivity
    3. Configure DHCP service
    4. Start DHCP service
    5. Test DHCP service

LDAP Server Host

Convert DHCP Schema to LDIF

The DHCP schema for LDAP isn't part of the standard OpenLDAP server packages. On Fedora it's part of the DHCP package. On Debian it's part of a special package which includes the DHCP server with LDAP integration: isc-dhcp-ldap. Because the LDAP schema file is provided as part of the DHCP server packaging, it must be transferred to the LDAP server to be loaded into the database schema set.

Even then the schema is provided in the older LDAP schema format. I need it in LDIF format so that I can load it like the others. Fortunately it's possible to load the older schema into memory and then write them out as LDIF using slapcat. The trick is to convince it to use a special alternate configuration file which just imports the old form schema and then dump the config as LDIF. There are a couple of tweaks to make on the resulting LDIF. The schema object is created with an array index of zero (0). That has to be removed. Slapcat also adds a CRC, and some reference and time stamp information that won't apply to the schema definition when it is loaded into a new database.

The section of code below will produce a file named dhcp.ldif. It takes the dhcp.schema file as input. It uses a temporary file for the LDAP configuration which only loads the DHCP schema and a temporary directory to contain the resulting LDIF config tree which slapcat produces as a matter of course.

#!/bin/sh
# Create the required tmp file/directory
mkdir slapd.d
echo 'include /etc/openldap/schema/dhcp.schema' > slapd.conf
# load the schema and then dump it in LDIF format
slapcat -f slapd.conf -F slapd.d -n0 -l dhcp.ldif \
  -H ldap:///cn={0}dhcp,cn=schema,cn=config
# remove the CRC, array index and timestamp/UUID entries
sed -i -e '/CRC32/d ; s/{0}dhcp/dhcp/ ; /structuralObjectClass/,$d' \
  dhcp.ldif
# remove the tmp file/directory
rm -rf slapd.d
rm slapd.conf
sudo cp dhcp.ldif /etc/openldap/schema/dhcp.ldif

(remember, this runs on the LDAP server host)

Import DHCP schema into configuration database


Once I have a the DHCP schema in LDIF format I can load it the same way I loaded the stock schema. This will be the last command which must run as root on the LDAP server and uses local authentication.

sudo ldapadd -Q -Y EXTERNAL -H ldapi:/// /etc/openldap/schema/dhcp.ldif

From this point on I'll be adding things not to the config database but to the hdb database using the RootDN and RootPW account.

Load the DHCP configuration into the LDAP server


The DHCP service configuration (as expressed in LDIF) requires three objects to describe a minimal working DHCP service:

  1. dhcpServer - The host on which the DHCP service will run
  2. dhcpService - The global settings which control the behavior of the DHCP service
  3. dhcpSubnet - A description of a subnet to which the DHCP server is connected
Making changes to any of these objects will require a restart of the affected DHCP daemon processes.

DHCP Server


The LDAP dhcpServer object is the hook to which the dhcpd process will attach when it starts up. This object contains the DN of the top of the DHCP service configuration.

LDAP object classes are additive. That is, a single entry in the database will commonly have more than one objectClass attribute. The objectClass attributes declare the set of attributes which the object can have and
there is no limit (other than conflict) to the combinations.

I believe that the dhcpServer objectClass can be combined with the NIS host class so that information about particular hosts can be unified under a single object.

#
# Define the DHCP host entry which will be used by the DHCP service on startup
# This is the configuration entry hook
#
dn: cn=dhcp-host,dc=example,dc=com
cn: dhcp-host
objectClass: top
objectClass: dhcpServer
dhcpServiceDN: cn=dhcp-service,dc=example,dc=com


DHCP Service


The dhcpService object is the root of the DHCP daemon configuration information. All of the objects which define a DHCP service configuration will be children of this object. That is, the DN of the dhcpService object will be the suffix for the rest of the objects that define the configuration.

There are two types of attribute which all objects in the DHCP configuration can have. These are the dhcpStatement and dhcpOption attributes. These correspond to normal statement lines and option lines in the traditional dhcpd.conf file.

The dhcpService attributes define the deamon behavior and any global options which would apply to all query responses.

# The root object of the DHCP service
# All elements of the DHCP configuration will use this DN for a suffix.
# 
dn: cn=dhcp-service,dc=example,dc=com
cn: dhcp-service
objectClass: top
objectClass: dhcpService
objectClass: dhcpOptions
dhcpPrimaryDN: cn=dhcp-host, dc=example,dc=com
dhcpStatements: authoritative
dhcpStatements: ddns-update-style none
dhcpStatements: max-lease-time 43200
dhcpStatements: default-lease-time 3600
dhcpStatements: allow booting
dhcpStatements: allow bootp
dhcpOption: domain-name "example.com"
dhcpOption: domain-name-servers 10.0.2.3


DHCP Subnet


The DHCP service needs a subnet definition so that it knows what interface(s) to bind to. A DHCP server listens for discovery requests. There's no point in listening if there are no networks to listen on, so the daemon will exit.

# DHCP Subnet object
# 
dn: cn=10.0.2.0, cn=dhcp-service,dc=example,dc=com
cn: 10.0.2.0
objectClass: top
objectClass: dhcpSubnet
dhcpNetMask: 24
dhcpOption: routers 10.0.2.2


Test DHCP Lease Reservation



# A Test Host Lease Reservation
# The definition of a host: name, MAC, IP address
# Additional options can control PXE boot and OS installation
#
dn: cn=testhost, cn=dhcp-service,dc=example,dc=com
cn: testhost
objectClass: top
objectClass: dhcpHost
objectClass: dhcpOptions
dhcpHWAddress: ethernet 0a:00:00:00:00:01
dhcpStatements: fixed-address 10.0.2.16
dhcpOption: host-name "testhost"


DHCP Server Host

These operations configure the DHCP server host and the dhcp daemon.

Prepare Logging (Optional)


I like to be able to view the logs for critical services separately from the rest of the system logs. This can make it easier. For this I'll add a config file for rsyslog which filters the dhcpd log entries to a file of their own. This doesn''t change the behavior at all, it just makes viewing the logs simpler.
First, create an empty log file (rsyslog doesn't like to create files that don't exist)
sudo touch /var/log/dhcpd.log

Then create the rsyslog config entry in /etc/rsyslog.d

cat <<EOF >/etc/rsyslog.d/dhcpd.conf
if $programname == "dhcpd" then /var/log/dhcpd.log
EOF

Finally, restart the rsyslog daemon

sudo systemctl restart rsyslog

Verify LDAP access


Before trying to connect the DHCP server to the LDAP service, I need to verify that the DHCP host can make the required connection and retrieve the dhcpServer entry which is the anchor for the configuration data.

ldapsearch -H ldap://ldap.example.com \
    -x -w changeme \
    -D cn=Manager,dc=example,dc=com \
    -b dc=example,dc=com \
    objectClass=dhcpServer

Set the DHCP server configuration - use LDAP server

When the dhcpd is configured for an LDAP database, the configuration file is a lot smaller than is typical.  It merely identifies where to find the configuration.  It can also indicate whether the daemon should read the configuration once and load it into memory, or resolve each query with a check of the database. Finally, it can write a copy of the configuration in the traditional format for verification.

# DHCP Host Location
ldap-server "ldap.example.com" ;
ldap-port 389 ;

# A user with read/write access to the database
ldap-username "cn=Manager,dc=example,dc=com" ;
ldap-password "changeme" ;

# Identify the root object of the config
ldap-base-dn "dc=example,dc=com" ;
ldap-dhcp-server-cn "dhcp-host" ;

# All queries check the database
ldap-method dynamic ;

# Write the DHCP config for validation
#   An empty file must exist before starting the daemon
#   And it must be writable by the dhcpd user
#ldap-debug-file "/var/log/dhcp-ldap-startup.log" ;


Start the DHCP server


sudo systemctl start dhcpd

Verify that the daemon has started and is serving queries for the subnet

May 16 20:06:53 fedora-20-x64 dhcpd: Internet Systems Consortium DHCP Server 4.2
.6
May 16 20:06:53 fedora-20-x64 dhcpd: Copyright 2004-2014 Internet Systems Consor
tium.
May 16 20:06:53 fedora-20-x64 dhcpd: All rights reserved.
May 16 20:06:53 fedora-20-x64 dhcpd: For info, please visit https://www.isc.org/
software/dhcp/
May 16 20:06:53 fedora-20-x64 dhcpd: Wrote 0 leases to leases file.
May 16 20:06:53 fedora-20-x64 dhcpd: Listening on LPF/p2p1/08:00:27:35:3b:b0/10.
0.2.0/24
May 16 20:06:53 fedora-20-x64 dhcpd: Sending on   LPF/p2p1/08:00:27:35:3b:b0/10.
0.2.0/24
May 16 20:06:53 fedora-20-x64 dhcpd: Sending on   Socket/fallback/fallback-net

Verify Operation


sudo dhtest --verbose --mac 0a:00:00:00:00:01 --interface eth0 --server 10.0.2.15
...
May 16 20:11:49 fedora-20-x64 dhcpd: DHCPDISCOVER from 0a:00:00:00:00:01 via eth-
May 16 20:11:49 fedora-20-x64 dhcpd: DHCPOFFER on 10.0.2.16 to 0a:00:00:00:00:01
 via eth0
May 16 20:11:49 fedora-20-x64 dhcpd: DHCPREQUEST for 10.0.2.16 (10.0.2.2) from 0
a:00:00:00:00:01 via eth0
May 16 20:11:49 fedora-20-x64 dhcpd: DHCPACK on 10.0.2.16 to 0a:00:00:00:00:01 v
ia eth0

Additional Work


This is a very simple example. There is considerable work that is still needed for a production system.
  1. Security - LDAP over SSL
  2. Security - Add LDAP users for access control
  3. Security - SASL or Kerberos authentication
  4. Security - Database access controls (user ACLs)
  5. HA - LDAP database replication

References

  • DHCP LDAP Patch
    https://github.com/dcantrell/ldap-for-dhcp/wiki
  • An Early example:
    https://skalyanasundaram.wordpress.com/dhcp/dhcp-with-ldap-support/
  • dhtest - DHCP emitter/responder
    https://github.com/saravana815/dhtest