BIND 9.20.30
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 AMI runs BIND 9.20.30 on Ubuntu 24.04 on Amazon EC2. It is one software version of the SecureAnyCloud Hardened Pi-hole, AdGuard Home & DNS Server AMIs listing on AWS Marketplace, which you select 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.30, ISC's security release of 21 October 2026, on the current-stable 9.20 branch. It fixes CVE-2026-19630, CVE-2026-94300, CVE-2026-96888 and CVE-2026-97313. It comes 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. - Continued security fixes. The repository is pinned to ISC's signing key, and unattended upgrades are allowed to apply ISC's 9.20 updates.
- BIND runs as
named.serviceunder the unprivilegedbinduser. - 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:953only. - 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. - Defaults: default user
ubuntu, x86_64 architecture, 8 GiB gp3 root volume, and a 20 GiB gp3 data volume at/var/lib/bind.
Security posture
- Not an open resolver.
namedlistens on the instance's addresses so that clients in your VPC can reach it. The access listsac-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. Open resolvers are abused for DNS amplification attacks, so this server answers private networks only. - The listing security group opens SSH only. DNS (port 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
rndckey is generated on each instance's first boot. No key ships in the image, so no two instances share one. The control channel listens on127.0.0.1:953only. - No credential to steal. BIND has no login. The only management interface is
rndcon the loopback address, using a key that only root can read. - No metadata access for the DNS server.
named.serviceand 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.
nameddoes not start until first boot has created its control key. If first boot cannot finish, it stopsnamedrather than leaving it half configured, and tries again on the next boot. - Hardened SSH. SSH accepts keys only, with no root login, no keyboard-interactive login and no X11 forwarding. These settings live in
/etc/ssh/sshd_config.d/00-sac.conf, so no later setting can override them. The root account has no password. - Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.
- Clean boot. The
fwupd-refreshtimer is masked, because EC2 instances have no firmware that the Linux Vendor Firmware Service can update. A fresh instance boots with no failed unit.
Prerequisites
- An AWS account with 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.
- The recommended instance type is
m5.large. A small DNS server runs comfortably ont3.smallor larger.
Connecting to your instance
On BIND 9.20.30 on Ubuntu 24.04, the operating system is Ubuntu 24.04 LTS and the SSH user is ubuntu.
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.
- When 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.30 on Ubuntu 24.04, then choose your Region.
- Pick an instance type and your key pair.
m5.largeis recommended, and the software charge is $0.04 per vCPU per hour. - For the security group, note that the listing's default group allows SSH (port 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 these commands on your own workstation. They create a security group that allows SSH from your management address and DNS from your VPC, then launch 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.
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. That file lists 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.30 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.89.250 bind.dns=172.31.89.250: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.89.250 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-30/Confirm the server is running
Confirm 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. It tests the access list with 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.shactive active BIND 9.20.30-1+ubuntu24.04.1+deb.sury.org+1-Ubuntu (Stable Release) <id:> version: BIND 9.20.30-1+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.89.250 answered [bind9-selftest] listening set OK (tcp: 22 53 ; udp: 53 68 ); rndc 127.0.0.1:953 only; no web server BIND_SELFTEST_OKThe 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.
Check the effective SSH settings, the boot state and the masked firmware-refresh timer:
systemctl is-system-running
systemctl is-enabled fwupd-refresh.timer
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|x11forwarding) 'running masked permitrootlogin no passwordauthentication no kbdinteractiveauthentication no x11forwarding noOpen DNS to your clients
The listing's security group allows SSH only. To serve DNS, add these inbound rules to the instance's security group from your own VPC or VPN CIDR ranges only, never from 0.0.0.0/0:
- UDP 53 from your VPC or VPN CIDRs: DNS.
- TCP 53 from your VPC or VPN CIDRs: DNS for large answers and 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 the instance listens on:
sudo ss -Hltnu | awk '{print $1, $5}' | sort -utcp 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.89.250: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.89.250%ens5:68 udp 172.31.89.250:53 udp [::1]:323named answers on the private IP and on loopback, and rndc (port 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 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.internalTo 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.
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.
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: 46201 ;; 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. 2026102201 3600 900 604800 3600 10 mail.example.internal. "sac BIND 9 example authoritative zone - replace with your own zones"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.
The domain dnssec-failed.org is a public test domain with deliberately broken signatures. It 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: 34318 ;; 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: 62745 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ; EDNS: version: 0, flags:; udp: 1232Confirm 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 prove the policy from the instance itself. The commands add a temporary public test address (192.0.2.53, from the TEST-NET-1 documentation range) to the loopback interface and send queries from it to the private IP. They send the same queries from the private IP, then remove 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 lostatus: REFUSED status: REFUSED status: NOERROR status: NOERRORAdd your own zone
Zone files live on the data volume in /var/lib/bind. Each zone is declared in /etc/bind/named.conf.local, which already holds a commented template.
This example creates corp.example, declares it, checks both files, 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 +shortzone corp.example/IN: loaded serial 2026102001 OK server reload successful 10.0.1.20Replace 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.
Allow another of your own networks (optional)
BIND answers queries and recursion 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 "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 reloadedRemember to allow the same range in the security group (Step 4).
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 0If you enable IPv6 on the subnet later, run sudo systemctl restart named. Then named picks up IPv6 by itself.
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.254IPAddressDeny=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] +/- 621usThe 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 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;Manage BIND with rndc
The rndc key is readable only by root and the bind group, and zone data lives on its own volume. Check both:
sudo stat -c '%a %U:%G %n' /etc/bind/rndc.key findmnt -n /var/lib/bind
sudo rndc status | tail -1640 root:bind /etc/bind/rndc.key /var/lib/bind /dev/nvme1n1 ext4 rw,relatime server is up and runningOther common commands:
sudo rndc reloadre-reads the configuration and every changed zone.sudo rndc reload <zone>reloads one zone.sudo rndc flushempties the recursive cache.sudo rndc querylog onlogs 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 the volume's device, which findmnt shows above.
Security recommendations
- Keep the security group tight. Open port 53 only to the networks that need the server, and open SSH only to your management address.
- Keep the access list scoped. Add only networks you trust (Step 9), never
any. The access list is a second layer of defense 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-policyin 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/bindwith 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. 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.30 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