B9
Networking AWS

BIND 9.20.29 on AWS User Guide

| Product: BIND 9.20.29 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying BIND 9.20.29 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Pi-hole, AdGuard Home & DNS Server AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other DNS and time servers in the listing are described on the listing's guide hub.

BIND 9, from Internet Systems Consortium (ISC), is the most widely deployed DNS server software. It serves authoritative zones (the records for your own domains) and runs as a recursive, caching resolver that looks up any other name for your clients. Zones are plain text files, and the daemon (named) is managed with rndc.

The image installs BIND 9.20.29, the current-stable 9.20 branch, from ISC's official package repository (ppa:isc/bind), not the 9.18 package in the Ubuntu archive, which reached the end of upstream support in 2026. The repository is pinned to ISC's signing key, and unattended upgrades are allowed to apply ISC's 9.20 updates, so your instance keeps receiving BIND security fixes.

What is on the image:

  • BIND 9.20.29 (named.service), running as the unprivileged bind user
  • Not an open resolver: one access list allows loopback and the private address ranges, for queries and recursion alike, and every other source gets REFUSED
  • Recursion with DNSSEC validation for those trusted ranges
  • One example authoritative zone, example.internal, so a query works out of the box, plus a template for your own zones
  • A per-instance rndc control key, generated on first boot, with the control channel on 127.0.0.1:953 only
  • Zone data on its own 20 GiB gp3 volume at /var/lib/bind
  • Hardening defaults: no zone transfers, no dynamic updates, the version string hidden, and response rate limiting
  • A self-test, /usr/local/sbin/bind9-selftest.sh

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3 · Data volume: 20 GiB gp3 at /var/lib/bind

Security posture

  • Not an open resolver. named listens on the instance's addresses so clients in your VPC can reach it. The access list cloudimg-trusted (127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fd00::/8, fe80::/10) decides who may query it, who may use recursion and who may read the cache. Any other source is refused, for your zones and for recursive lookups. Open resolvers are abused for DNS amplification attacks, so this server answers private networks only.
  • The listing security group opens SSH only. DNS (53, UDP and TCP) stays closed until you open it to your own VPC or VPN ranges. Clients use the instance's private IP.
  • Per-instance control key. The rndc key is generated on each instance's first boot. No key ships in the image, so no two instances share one, and the control channel listens on 127.0.0.1:953 only.
  • No credential to steal. BIND has no login. The only management interface is rndc on the loopback address, using a key only root can read.
  • No metadata access for the DNS server. named.service and the first-boot unit are blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Amazon Time Sync (169.254.169.123) and the Amazon VPC resolver (169.254.169.253) still work.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • Fails closed. named will not start until first boot has created its control key. If first boot cannot finish, it stops named rather than leaving it half configured, and tries again on the next boot.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. A VPC subnet for the DNS server, and the CIDR ranges of the clients that will use it

Recommended instance type: m5.large. A small DNS server runs comfortably on t3.small or larger.

Connecting to your instance

Version Operating system SSH user
BIND 9.20.29 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened Pi-hole, AdGuard Home & DNS Server AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose BIND 9.20.29 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group allows SSH (22) only. Restrict it to your own IP address. You will open DNS to your clients in Step 4.
  6. Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. It creates a security group that allows SSH from your management address and DNS from your VPC, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR ranges with your own:

SG=$(aws ec2 create-security-group --group-name bind-dns \
  --description "BIND DNS server" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol udp --port 53 --cidr 10.0.0.0/16
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 53 --cidr 10.0.0.0/16
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=bind-dns}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required. The AMI's block device mapping includes the 20 GiB data volume for /var/lib/bind.

Step 2: Connect and read the instance details

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It creates the instance's rndc control key, starts named and writes /root/bind-info.txt with the private IP, the trusted access list and how to manage the server. The note holds no secret: the key itself stays in /etc/bind/rndc.key, readable only by root and the bind group.

sudo cat /root/bind-info.txt
# BIND 9.20.29 on Ubuntu 24.04 LTS (cloudimg AWS Marketplace image) - THIS instance.
# BIND has no login. The rndc control key in /etc/bind/rndc.key was generated on this instance's
# first boot and exists nowhere else.

bind.private_ip=172.31.95.105
bind.dns=172.31.95.105:53 (udp+tcp)
bind.trusted_acl=127.0.0.0/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10
rndc.key.file=/etc/bind/rndc.key
rndc.algorithm=hmac-sha256
rndc.control.address=127.0.0.1:953 (loopback only)
#
# NOT AN OPEN RESOLVER: queries (recursive and authoritative) are answered ONLY for the trusted ACL
# above ('acl "cloudimg-trusted"' in /etc/bind/named.conf.options); every other source gets REFUSED. Recursion
# validates DNSSEC. Zone transfers and dynamic updates are off; version.bind is hidden.
#
# NETWORK ACCESS: the listing security group opens SSH only. To serve clients, add inbound rules
# for 53 (udp+tcp) ONLY from your own VPC / VPN CIDRs - never 0.0.0.0/0 - and point clients at the
# PRIVATE IP above.
#
# Manage named locally:  sudo rndc status ; sudo rndc reload
# Example zone:          dig @172.31.95.105 www.example.internal     # -> 10.0.0.20
# Add your own zones:    a db.<zone> file in /var/lib/bind + /etc/bind/named.conf.local,
#                        then: sudo named-checkconf && sudo rndc reload
# Self-test:             sudo /usr/local/sbin/bind9-selftest.sh
# User guide: https://www.cloudimg.co.uk/guides/bind-9-20-29-on-ubuntu-24-04-aws/

Step 3: Confirm the server is running

named.service and its first-boot unit are active, rndc reaches the daemon with this instance's key, and the self-test checks the version, the control key, authoritative answers over UDP and TCP, the hardening options, the access list (with a live query from a public test address, as in Step 7) and the exact set of listening ports.

systemctl is-active named.service bind9-firstboot.service
named -v
sudo rndc status | grep -E '^(version|number of zones|recursive clients)|server is up'
sudo /usr/local/sbin/bind9-selftest.sh
active
active
BIND 9.20.29-1~deb13u1+ubuntu24.04.1+deb.sury.org+1-Ubuntu (Stable Release) <id:>
version: BIND 9.20.29-1~deb13u1+ubuntu24.04.1+deb.sury.org+1-Ubuntu (Stable Release) <id:> (version.bind/txt/ch disabled)
number of zones: 102 (100 automatic)
recursive clients: 0/900/1000
server is up and running
[bind9-selftest] not an open resolver: 192.0.2.53 REFUSED (zone + recursion, udp + tcp); 127.0.0.1 and 172.31.95.105 answered
[bind9-selftest] listening set OK (tcp: 22 53 ; udp: 53 68 ); rndc 127.0.0.1:953 only; no web server
BIND_SELFTEST_OK

The 100 automatic zones are BIND's built-in empty zones for private and reserved address ranges; they stop reverse lookups for those ranges leaking to the internet.

named.service and its first-boot unit active, named -v reporting BIND 9.20.29, rndc status showing the server up, and the self-test confirming the not-an-open-resolver probe and the exact listening set

Step 4: Open DNS to your clients

The listing's security group allows SSH only. To serve DNS, add inbound rules to the instance's security group from your own VPC or VPN CIDR ranges only, never from 0.0.0.0/0:

Protocol Port Source Purpose
UDP 53 your VPC / VPN CIDRs DNS
TCP 53 your VPC / VPN CIDRs DNS (large answers, DNSSEC)

In the EC2 console choose the instance, open Security, choose the security group, then Edit inbound rules. Point clients at the instance's private IP (bind.private_ip in Step 2). These are the only ports the instance listens on:

sudo ss -Hltnu | awk '{print $1, $5}' | sort -u
tcp 0.0.0.0:22
tcp 127.0.0.1:53
tcp 127.0.0.1:953
tcp 127.0.0.53%lo:53
tcp 127.0.0.54:53
tcp 172.31.95.105:53
tcp [::]:22
udp 127.0.0.1:323
udp 127.0.0.1:53
udp 127.0.0.53%lo:53
udp 127.0.0.54:53
udp 172.31.95.105%ens5:68
udp 172.31.95.105:53
udp [::1]:323

named answers on the private IP and on loopback; rndc (953) is bound to loopback only. Port 323 is chrony's local control socket, 68 is the DHCP client, and 127.0.0.53 and 127.0.0.54 belong to the instance's own local resolver (systemd-resolved), which the instance uses for its own lookups.

From a client inside your VPC, test the server against its private IP:

dig @<private-ip> www.example.internal

To make your VPC's instances use the server, set its private IP as the DNS server in a DHCP options set for the VPC, or on each client's network configuration.

Step 5: Query the example authoritative zone

The image ships one authoritative zone, example.internal. The aa (Authoritative Answer) flag shows the answer comes from the zone itself, not from the cache.

dig @127.0.0.1 www.example.internal A | grep -E 'status|flags|^www'
dig @127.0.0.1 example.internal SOA +short
dig @127.0.0.1 example.internal MX +short
dig @127.0.0.1 example.internal TXT +short
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7687
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 1232
www.example.internal.   3600    IN  A   10.0.0.20
ns1.example.internal. admin.example.internal. 2026102001 3600 900 604800 3600
10 mail.example.internal.
"cloudimg BIND 9 example authoritative zone - replace with your own zones"

Step 6: Resolve a name with DNSSEC validation

For names outside your own zones, BIND resolves from the DNS root and validates DNSSEC signatures. The ad (Authenticated Data) flag shows the answer was validated; dnssec-failed.org, a public test domain with deliberately broken signatures, must fail with SERVFAIL rather than reach your clients.

dig @127.0.0.1 +dnssec nlnetlabs.nl A | grep -E 'status|flags|^nlnetlabs'
dig @127.0.0.1 dnssec-failed.org A | grep -E 'status|flags'
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3615
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags: do; udp: 1232
nlnetlabs.nl.       240 IN  A   128.140.76.106
nlnetlabs.nl.       240 IN  RRSIG   A 8 2 240 20261113084535 20261016084535 42393 nlnetlabs.nl. D7S8lXRBe8TAuzcqOX+FkoKAc/Cp4QKDOtOadepU0VNkHsMQOgHZZcA1 QIRr4M218muPvlSaZ8mVXOzqeCD8nA6qyvdt/gJjwCriX+HS7Tyui8Ra NeppwBe8Y1LgqqNiVjGu5sTI9uDzXsUgvvrP8DYBr0khlLI4iPcae6UW Dj4=
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 18489
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 1232

dig for www.example.internal returning an authoritative answer with the aa flag, dig for nlnetlabs.nl returning NOERROR with the ad flag and an RRSIG, and dig for dnssec-failed.org returning SERVFAIL because its signatures fail validation

Step 7: Confirm it is not an open resolver

The access list and the options that use it are in /etc/bind/named.conf.options:

grep -vE '^\s*//' /etc/bind/named.conf.options | grep -E 'acl|allow-|recursion|dnssec-validation|version|session-keyfile|/[0-9]+;'
acl "cloudimg-trusted" {
    127.0.0.0/8;
    ::1/128;
    10.0.0.0/8;
    172.16.0.0/12;
    192.168.0.0/16;
    100.64.0.0/10;
    fd00::/8;
    fe80::/10;
    allow-query { cloudimg-trusted; };
    recursion yes;
    allow-recursion { cloudimg-trusted; };
    allow-query-cache { cloudimg-trusted; };
    dnssec-validation auto;
    version none;                 // do not leak the BIND version string
    allow-transfer { none; };     // no zone transfers by default
    allow-update { none; };       // no dynamic updates
    session-keyfile none;         // no TSIG session key (nsupdate -l): dynamic updates are off anyway
        exempt-clients { 127.0.0.0/8; ::1/128; };

You can prove the policy from the instance itself. This adds a temporary public test address (192.0.2.53, from the TEST-NET-1 documentation range) to the loopback interface, sends queries from it to the private IP, sends the same queries from the private IP, and removes the address again. The public source is refused, for your zone and for a recursive lookup; the private source is answered:

PRIV=$(ip -4 -o route get 1.1.1.1 | sed -nE 's/.* src ([0-9.]+).*/\1/p')
sudo ip addr add 192.0.2.53/32 dev lo
dig -b 192.0.2.53 @"$PRIV" www.example.internal A | grep -oE 'status: [A-Z]+'
dig -b 192.0.2.53 @"$PRIV" isc.org A | grep -oE 'status: [A-Z]+'
dig -b "$PRIV" @"$PRIV" www.example.internal A | grep -oE 'status: [A-Z]+'
dig -b "$PRIV" @"$PRIV" isc.org A | grep -oE 'status: [A-Z]+'
sudo ip addr del 192.0.2.53/32 dev lo
status: REFUSED
status: REFUSED
status: NOERROR
status: NOERROR

The cloudimg-trusted access list governing allow-query, allow-recursion and allow-query-cache, and queries from the public test address 192.0.2.53 refused while the same queries from the private IP are answered

Step 8: Add your own zone

Zone files live on the data volume in /var/lib/bind, and each zone is declared in /etc/bind/named.conf.local (a commented template is already there). This example creates corp.example, declares it, checks both, reloads named and queries the new record:

sudo tee /var/lib/bind/db.corp.example >/dev/null <<'ZONE'
$TTL 3600
@     IN  SOA  ns1.corp.example. hostmaster.corp.example. ( 2026102001 3600 900 604800 3600 )
@     IN  NS   ns1.corp.example.
ns1   IN  A    10.0.1.10
app   IN  A    10.0.1.20
ZONE
sudo chown bind:bind /var/lib/bind/db.corp.example
grep -q '^zone "corp.example"' /etc/bind/named.conf.local || printf '%s\n' \
  'zone "corp.example" {' \
  '    type primary;' \
  '    file "/var/lib/bind/db.corp.example";' \
  '    allow-transfer { none; };' \
  '};' | sudo tee -a /etc/bind/named.conf.local >/dev/null
sudo named-checkzone corp.example /var/lib/bind/db.corp.example
sudo named-checkconf && sudo rndc reload
sleep 2
dig @127.0.0.1 app.corp.example A +short
zone corp.example/IN: loaded serial 2026102001
OK
server reload successful
10.0.1.20

Replace corp.example and the records with your own. Increase the serial number each time you edit a zone file, then run sudo rndc reload <zone>. To delete the example zone, remove its zone "example.internal" block from named.conf.local and reload.

If you run secondary servers, list their addresses in that zone's allow-transfer instead of none.

Step 9: Allow another of your own networks (optional)

Queries and recursion are answered for the local host and the private ranges above. To serve another network you trust, for example a VPN range that uses public addresses, add it to the access list at the top of /etc/bind/named.conf.options:

acl "cloudimg-trusted" {
    127.0.0.0/8;
    ...
    203.0.113.0/24;
};

Replace 203.0.113.0/24 with your own range. Never add any or 0.0.0.0/0: that turns the server into an open resolver. To serve one of your zones publicly on the internet (as an authoritative server only), give just that zone its own allow-query { any; }; in named.conf.local and leave recursion on the access list. Check the configuration and apply it:

sudo named-checkconf && sudo rndc reconfig && echo "configuration reloaded"
configuration reloaded

Remember to allow the same range in the security group (Step 4).

Step 10: IPv4 and IPv6 upstream lookups

Most EC2 subnets are IPv4 only. Left to itself, named would still try the IPv6 addresses of the internet's DNS servers and some lookups would fail. Each time named starts, a small helper (/usr/local/sbin/bind9-ipfamily.sh) checks for an IPv6 default route: without one, named runs with -4 (IPv4 only); in a dual-stack subnet it uses both.

ps -o args= -C named
ip -6 route show default | grep -c default || true
/usr/sbin/named -f -u bind -4
0

If you enable IPv6 on the subnet later, run sudo systemctl restart named and named picks up IPv6 by itself.

Step 11: Metadata access is blocked for the DNS server

named.service and the first-boot unit carry a systemd IPAddressDeny rule for the instance metadata addresses, so nothing running inside them can reach the metadata service. Each entry is a single address, so the Amazon Time Sync Service at 169.254.169.123 keeps working, as chrony shows:

systemctl show -p IPAddressDeny named.service bind9-firstboot.service
chronyc -n sources | grep 169.254
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32

IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
^* 169.254.169.123               3   4   377     8    -34us[  -36us] +/-  621us

The Amazon VPC resolver at 169.254.169.253 is not blocked either. If your subnet has no internet route and named cannot reach the root servers, forward to it by adding this to the options block in /etc/bind/named.conf.options, then run sudo named-checkconf && sudo rndc reconfig:

    forwarders { 169.254.169.253; };
    forward only;

Step 12: Manage BIND with rndc

The rndc key is readable only by root and the bind group, and zone data lives on its own volume:

sudo stat -c '%a %U:%G %n' /etc/bind/rndc.key
findmnt -n /var/lib/bind
sudo rndc status | tail -1
640 root:bind /etc/bind/rndc.key
/var/lib/bind /dev/nvme1n1 ext4   rw,relatime
server is up and running

Other common commands:

  • sudo rndc reload re-reads the configuration and every changed zone; sudo rndc reload <zone> reloads one zone.
  • sudo rndc flush empties the recursive cache.
  • sudo rndc querylog on logs every query to the journal (journalctl -u named) until you turn it off.

To grow the zone volume, increase the EBS volume size in the EC2 console, then run sudo resize2fs on its device (shown by findmnt above).

Step 13: Security recommendations

  • Keep the security group tight. Open 53 only to the networks that need the server, and SSH only to your management address.
  • Keep the access list scoped. Only add networks you trust (Step 9), never any. The access list is a second layer of defence behind the security group.
  • Leave DNSSEC validation on. It protects your clients from spoofed or tampered answers. To sign your own zones, use a dnssec-policy in the zone's block; BIND keeps the keys on the data volume.
  • Keep BIND patched. Unattended upgrades apply ISC's 9.20 updates and Ubuntu's security updates.
  • Back up /var/lib/bind with EBS snapshots to keep your zone files and any DNSSEC keys.

Support and licensing

BIND 9 is developed by Internet Systems Consortium (ISC) and distributed under the Mozilla Public License 2.0. This image ships ISC's unmodified BIND 9.20 packages. cloudimg provides the packaging, the not-an-open-resolver defaults, DNSSEC validation, the per-instance control key, the metadata block, and 24/7 support.

cloudimg is not affiliated with or endorsed by Internet Systems Consortium. BIND is a mark of its owner and is used here only to identify the software.

For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.