Deployment guide

BIND 9.20.29

Overview

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 you manage the daemon (named) with rndc.

This SecureAnyCloud image installs BIND 9.20.29, the current-stable 9.20 branch, from ISC's official package repository (ppa:isc/bind). It does not use 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 apply ISC's 9.20 updates, so your instance keeps receiving BIND security fixes.

The image runs on Ubuntu 24.04 on Amazon EC2. It is one version of the SecureAnyCloud Hardened Pi-hole, AdGuard Home & DNS Server AMIs listing on AWS Marketplace. You select it as the software version when you subscribe and launch. The listing's guide hub describes the other DNS and time servers in the listing.

What you get:

  • 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 notes

  • Not an open resolver. named listens on the instance's addresses so that clients in your VPC can reach it. The access list sac-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 the server, who may use recursion and who may read the cache. Any other source is refused, for your zones and for recursive lookups. Attackers abuse open resolvers 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 that 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.
  • Fail-closed startup. named does not start until first boot has created its control key. If first boot cannot finish, it stops named instead of 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

  • An AWS account and permission to launch EC2 instances
  • An EC2 key pair in the Region you launch in
  • 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

1

Launch the instance

Option A: AWS Marketplace console

  • Open the SecureAnyCloud Hardened Pi-hole, AdGuard Home & DNS Server AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  • Once the subscription is active, choose Launch your software.
  • 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.
  • Pick an instance type (m5.large is recommended, and the software charge is $0.04 per vCPU per hour) and your key pair.
  • Set the security group. The listing's default group allows SSH (22) only. Restrict it to your own IP address. You open DNS to your clients in Step 4.
  • Choose your VPC and subnet and leave the metadata defaults, because the image already requires IMDSv2. Then choose Launch.

Option B: AWS CLI

Run the following script 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. Replace 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, because the AMI already requires IMDSv2. The AMI's block device mapping includes the 20 GiB data volume for /var/lib/bind.

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 (sac 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 "sac-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://secureanycloud.com/stacks/bind-9-20-29/
3

Confirm the server is running

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

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 from leaking to the internet.

4

Open DNS to your clients

The listing's security group allows SSH only. To serve DNS, add the following 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 choose Edit inbound rules. Point clients at the instance's private IP (bind.private_ip in Step 2). These are the only ports that 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, and port 68 is the DHCP client. Addresses 127.0.0.53 and 127.0.0.54 belong to the instance's 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 in each client's network configuration.

5

Query the example authoritative zone

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

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. "sac BIND 9 example authoritative zone - replace with your own zones"
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 that the answer was validated. dnssec-failed.org is a public test domain with deliberately broken signatures. It must fail with SERVFAIL instead of reaching 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
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 "sac-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 { sac-trusted; }; recursion yes; allow-recursion { sac-trusted; }; allow-query-cache { sac-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 check the policy from the instance itself. The following script adds a temporary public test address (192.0.2.53, from the TEST-NET-1 documentation range) to the loopback interface and sends queries from it to the private IP. It sends the same queries from the private IP, then removes the test address. 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
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, which already contains a commented template. The following 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.

9

Allow another of your own networks (optional)

The server answers queries and recursion for the local host and the private ranges above. To serve another network that 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 "sac-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, because 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).

10

IPv4 and IPv6 upstream lookups

Most EC2 subnets are IPv4 only. By default, 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. named then picks up IPv6 by itself.

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. Chrony confirms this:

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 queries to the VPC resolver. Add the following 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;
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).

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. Add only networks that 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

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

SecureAnyCloud 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 sales@secureanycloud.com or visit SecureAnyCloud support.

Deploy on AWS

Launch BIND 9.20.29 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS