Networking AWS

Unbound 1.19.2 on Ubuntu 24.04 on AWS User Guide

| Product: Unbound 1.19.2 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Unbound 1.19.2 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.

Unbound, from NLnet Labs, is a fast, lean validating, recursive, caching DNS resolver. It resolves names for your clients by walking the DNS from the root, checks every answer's DNSSEC signatures, and caches the results. It has no database and no web console: it is driven by plain text configuration and managed with unbound-control.

The image installs Unbound 1.19.2 from the Ubuntu 24.04 LTS archive, which the Ubuntu security team patches. Unattended security upgrades stay on for your instance.

What is on the image:

  • Unbound 1.19.2 (unbound.service), running as the unprivileged unbound user under its AppArmor profile
  • DNSSEC validation on, against the root trust anchor in /var/lib/unbound/root.key, which keeps itself current (RFC 5011)
  • DNS-over-TLS (DoT) on port 853, using a key and self-signed certificate created on your instance's first boot
  • A not an open resolver policy: recursion is answered for the local host and private address ranges only, and every other source gets REFUSED
  • unbound-control on a root-only unix socket (/run/unbound.ctl), with no network control port and no control certificate
  • Hardening and privacy defaults: hide-identity, hide-version, the harden-* options, QNAME minimisation, prefetching and rate limiting
  • A self-test, /usr/local/sbin/unbound-selftest.sh

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3

Security posture

  • Not an open resolver. Unbound listens on every interface so clients in your VPC can reach it, but its access-control rules refuse 0.0.0.0/0 and ::0/0 and allow only 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 and fe80::/10. The same rules apply to plain DNS and to DNS-over-TLS. Open resolvers are abused for DNS amplification attacks, so this one answers private networks only.
  • The listing security group opens SSH only. DNS (53, UDP and TCP) and DNS-over-TLS (853/tcp) stay closed until you open them to your own VPC or VPN ranges. Clients use the instance's private IP.
  • Per-instance DoT key. The DNS-over-TLS private key and certificate are created on each instance's first boot. No key ships in the image, so no two instances share one.
  • No credential to steal. Unbound has no login. The only management interface is unbound-control over a unix socket that only root can use.
  • No metadata access for the resolver. Unbound can be configured to fetch zones from URLs, so unbound.service is blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Amazon Time Sync (169.254.169.123) still works.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • Fails closed. If first boot cannot finish its setup, it stops Unbound 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 resolver, and the CIDR ranges of the clients that will use it

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

Connecting to your instance

Version Operating system SSH user
Unbound 1.19.2 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 Unbound 1.19.2 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 the DNS ports 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 unbound-resolver \
  --description "Unbound resolver" --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 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 853 --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=unbound-resolver}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required.

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 DNS-over-TLS key and certificate, turns on DoT, and writes /root/unbound-info.txt with the private IP, the certificate fingerprint and the public key pin. None of these values are secret: the private key itself stays in /etc/unbound/tls/, readable only by root and the unbound group.

sudo cat /root/unbound-info.txt
# Unbound 1.19.2 on Ubuntu 24.04 LTS (cloudimg AWS Marketplace image) - THIS instance.
# Unbound has no login. The DNS-over-TLS private key was generated on this instance's
# first boot and exists nowhere else.

unbound.private_ip=172.31.83.101
unbound.dns=172.31.83.101:53 (udp+tcp)
unbound.dot=172.31.83.101:853/tcp (DNS-over-TLS; Unbound also answers plain DNS on udp/853 under the same access-control)
unbound.dot_cert=/etc/unbound/tls/server.pem
unbound.dot_cert_sha256=97:B7:B0:EA:4E:21:60:5E:D9:88:96:95:8C:14:F3:F7:83:FC:00:0E:0E:BE:04:C2:7C:4F:8E:D7:E9:8B:F1:AC
unbound.dot_pubkey_pin_sha256=U6mCWgx+hxwp1BV/AE3wFw2GlZMIxzjzwuk2QxPHVhg=
unbound.control=/run/unbound.ctl (root-only unix socket)
#
# NETWORK ACCESS: the listing security group opens SSH only. To serve clients, add inbound
# rules for 53 (udp+tcp) and 853/tcp ONLY from your own VPC / VPN CIDRs - never 0.0.0.0/0 -
# and point clients at the PRIVATE IP above. Recursion is answered ONLY for localhost +
# RFC1918/private ranges (access-control in /etc/unbound/unbound.conf.d/00-cloudimg.conf); every other source gets REFUSED.
# Add another range of your own with 'access-control: <cidr> allow', then:
#   sudo systemctl restart unbound          (NEVER allow 0.0.0.0/0)
#
# DNSSEC validation is ON:  dig @127.0.0.1 +dnssec nlnetlabs.nl   (expect the 'ad' flag)
# DoT check:                dig +tls -p 853 @127.0.0.1 nlnetlabs.nl
# Self-test:                sudo /usr/local/sbin/unbound-selftest.sh
# User guide: https://www.cloudimg.co.uk/guides/unbound-1-19-2-on-ubuntu-24-04-aws/

Step 3: Confirm the resolver is running

unbound.service and its first-boot unit are active, unbound-control reaches the daemon over the unix socket with the validator (DNSSEC) module loaded, and the self-test checks the version, the access-control rules, DNS on 53, DNS-over-TLS on 853 with this instance's certificate, and the exact set of listening ports.

systemctl is-active unbound.service unbound-firstboot.service
sudo unbound-control status
sudo /usr/local/sbin/unbound-selftest.sh
active
active
version: 1.19.2
verbosity: 1
threads: 1
modules: 3 [ subnetcache validator iterator ]
uptime: 51 seconds
options: reuseport control(namedpipe)
unbound (pid 3003) is running...
[unbound-selftest] DNS 53 + DoT 853 answer; DoT certificate is the per-instance one (sha256 97:B7:B0:EA:4E:21:60:5E:D9:88:96:95:8C:14:F3:F7:83:FC:00:0E:0E:BE:04:C2:7C:4F:8E:D7:E9:8B:F1:AC)
[unbound-selftest] listening set OK (tcp: 22 53 853 ; udp: 53 68 853 ); control is the unix socket /run/unbound.ctl
UNBOUND_SELFTEST_OK

unbound.service active, unbound-control status showing version 1.19.2 with the validator module, and the self-test confirming DNS on 53, DNS-over-TLS on 853 with the per-instance certificate and the exact listening set

Step 4: Open the resolver 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, zone transfers of cached data)
TCP 853 your VPC / VPN CIDRs DNS-over-TLS

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 (unbound.private_ip in Step 2). These are the only ports the instance listens on besides SSH:

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

Port 323 is chrony's local control socket and 68 is the DHCP client. Unbound opens UDP as well as TCP on its DNS-over-TLS interface, so it also answers plain DNS on udp/853, under the same access-control rules. You do not need to open udp/853.

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

dig @<private-ip> +dnssec example.com

To make a Linux client use the resolver permanently, set the resolver's private IP as the DNS server on the client's network interface (with netplan or systemd-resolved), or in a DHCP options set for the VPC.

Step 5: Resolve a name with DNSSEC validation

Query a DNSSEC-signed name. The ad (Authenticated Data) flag in the header shows Unbound validated the signatures against the root trust anchor, and the RRSIG record is the signature itself.

dig @127.0.0.1 +dnssec nlnetlabs.nl A
; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> @127.0.0.1 +dnssec nlnetlabs.nl A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39520
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;nlnetlabs.nl.          IN  A

;; ANSWER SECTION:
nlnetlabs.nl.       239 IN  A   128.140.76.106
nlnetlabs.nl.       239 IN  RRSIG   A 8 2 240 20261113084535 20261016084535 42393 nlnetlabs.nl. D7S8lXRBe8TAuzcqOX+FkoKAc/Cp4QKDOtOadepU0VNkHsMQOgHZZcA1 QIRr4M218muPvlSaZ8mVXOzqeCD8nA6qyvdt/gJjwCriX+HS7Tyui8Ra NeppwBe8Y1LgqqNiVjGu5sTI9uDzXsUgvvrP8DYBr0khlLI4iPcae6UW Dj4=

;; Query time: 174 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Mon Oct 19 09:29:45 UTC 2026
;; MSG SIZE  rcvd: 229

Step 6: Confirm DNSSEC rejects bad data

dnssec-failed.org is a public test domain whose signatures are deliberately broken. A validating resolver must answer SERVFAIL rather than pass the data to your clients. unbound-checkconf checks the whole configuration.

dig @127.0.0.1 dnssec-failed.org A
sudo unbound-checkconf
; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> @127.0.0.1 dnssec-failed.org A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 52355
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;dnssec-failed.org.     IN  A

;; Query time: 239 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Mon Oct 19 09:29:46 UTC 2026
;; MSG SIZE  rcvd: 46

unbound-checkconf: no errors in /etc/unbound/unbound.conf

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: Use DNS-over-TLS

DNS-over-TLS encrypts queries between your clients and the resolver on port 853. The certificate is self-signed and was created on this instance's first boot, with the instance's private IP, private DNS name, public IP and public DNS name in its Subject Alternative Names. Check a query over TLS and the certificate the server presents:

dig +tls -p 853 @127.0.0.1 +dnssec nlnetlabs.nl A | grep -E "status|flags|SERVER|^nlnetlabs"
openssl s_client -connect 127.0.0.1:853 </dev/null 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName -fingerprint -sha256
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42440
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags: do; udp: 1232
nlnetlabs.nl.       237 IN  A   128.140.76.106
nlnetlabs.nl.       237 IN  RRSIG   A 8 2 240 20261113084535 20261016084535 42393 nlnetlabs.nl. D7S8lXRBe8TAuzcqOX+FkoKAc/Cp4QKDOtOadepU0VNkHsMQOgHZZcA1 QIRr4M218muPvlSaZ8mVXOzqeCD8nA6qyvdt/gJjwCriX+HS7Tyui8Ra NeppwBe8Y1LgqqNiVjGu5sTI9uDzXsUgvvrP8DYBr0khlLI4iPcae6UW Dj4=
;; SERVER: 127.0.0.1#853(127.0.0.1) (TLS)
subject=CN = 172.31.83.101, O = cloudimg Unbound DNS-over-TLS (per-instance)
X509v3 Subject Alternative Name: 
    IP Address:172.31.83.101, IP Address:127.0.0.1, DNS:localhost, DNS:ip-172-31-83-101, DNS:ip-172-31-83-101.ec2.internal, IP Address:52.90.113.164, DNS:ec2-52-90-113-164.compute-1.amazonaws.com
sha256 Fingerprint=97:B7:B0:EA:4E:21:60:5E:D9:88:96:95:8C:14:F3:F7:83:FC:00:0E:0E:BE:04:C2:7C:4F:8E:D7:E9:8B:F1:AC

The key is readable only by root and the unbound group; the certificate is public:

sudo openssl x509 -in /etc/unbound/tls/server.pem -noout -subject -dates
sudo stat -c '%a %U:%G %n' /etc/unbound/tls/server.key /etc/unbound/tls/server.pem
subject=CN = 172.31.83.101, O = cloudimg Unbound DNS-over-TLS (per-instance)
notBefore=Oct 19 09:28:16 2026 GMT
notAfter=Oct 17 09:28:16 2036 GMT
640 root:unbound /etc/unbound/tls/server.key
644 root:unbound /etc/unbound/tls/server.pem

Because the certificate is self-signed, configure DoT clients to trust it explicitly:

  • Pin the public key. Clients such as Stubby accept an SPKI pin. Use unbound.dot_pubkey_pin_sha256 from /root/unbound-info.txt, for example tls_pubkey_pinset: [{digest: "sha256", value: <pin>}] with tls_auth_name set to the private IP.
  • Or trust the certificate. Copy /etc/unbound/tls/server.pem to the client and add it to the client's trust store, or pass it as the CA file, for example kdig +tls-ca=server.pem @<private-ip>.

To use a certificate from your own certificate authority instead, replace server.key and server.pem in /etc/unbound/tls/ (keep the key 0640 root:unbound) and run sudo systemctl restart unbound.

Step 8: Confirm it is not an open resolver

The access-control rules live in /etc/unbound/unbound.conf.d/00-cloudimg.conf:

grep -E 'access-control|control-interface|control-use-cert' /etc/unbound/unbound.conf.d/00-cloudimg.conf
    # ---- listen for clients inside your VPC (access-control decides who) ----
    #   access-control: <your-cidr> allow
    # NEVER 'access-control: 0.0.0.0/0 allow' (that is an open resolver).
    access-control: 0.0.0.0/0 refuse
    access-control: ::0/0 refuse
    access-control: 127.0.0.0/8 allow
    access-control: ::1 allow
    access-control: 10.0.0.0/8 allow
    access-control: 172.16.0.0/12 allow
    access-control: 192.168.0.0/16 allow
    access-control: 100.64.0.0/10 allow
    access-control: fd00::/8 allow
    access-control: fe80::/10 allow
    control-interface: /run/unbound.ctl
    control-use-cert: no

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 a query from it over DNS and over DNS-over-TLS, sends the same query from 127.0.0.1, and removes the address again. The public source is refused; the local one is answered:

sudo ip addr add 192.0.2.53/32 dev lo
dig +tcp -b 192.0.2.53 @192.0.2.53 nlnetlabs.nl A | grep -oE "status: [A-Z]+"
dig +tls -p 853 -b 192.0.2.53 @192.0.2.53 nlnetlabs.nl A | grep -oE "status: [A-Z]+"
dig +tcp -b 127.0.0.1 @127.0.0.1 nlnetlabs.nl A | grep -oE "status: [A-Z]+"
sudo ip addr del 192.0.2.53/32 dev lo
status: REFUSED
status: REFUSED
status: NOERROR

The access-control rules refusing 0.0.0.0/0 and ::0/0 and allowing private ranges, and a query from the public test address 192.0.2.53 refused over DNS and DNS-over-TLS while the same query from 127.0.0.1 is answered

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

Recursion is 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 an allow line inside the server: block of /etc/unbound/unbound.conf.d/00-cloudimg.conf:

    access-control: 203.0.113.0/24 allow

Replace 203.0.113.0/24 with your own range. Never add access-control: 0.0.0.0/0 allow: that turns the server into an open resolver. Check the configuration and apply it:

sudo unbound-checkconf
sudo unbound-control reload

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

Step 10: Metadata access is blocked for the resolver

unbound.service carries a systemd IPAddressDeny rule for the instance metadata addresses, so nothing running inside the resolver can reach them. 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 unbound.service
chronyc -n sources | grep 169.254
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
^* 169.254.169.123               3   4   377    14    -11us[  -27us] +/-  652us

Step 11: Manage Unbound with unbound-control

The control channel is a unix socket that only root can use, so unbound-control works locally with nothing to configure:

sudo unbound-control stats_noreset | grep -E '^total.num.(queries|cachehits|cachemiss)='
total.num.queries=10
total.num.cachehits=8
total.num.cachemiss=2

Other common commands:

sudo unbound-control status
sudo unbound-control flush_zone example.com

sudo unbound-control reload re-reads the configuration and clears the cache; sudo unbound-control dump_cache prints the cache.

Step 12: Security recommendations

  • Keep the security group tight. Open 53 and 853 only to the networks that need the resolver, and SSH only to your management address.
  • Keep recursion scoped. Only widen access-control to networks you trust (Step 9), never to 0.0.0.0/0. The access-control rules are a second layer of defence behind the security group.
  • Leave DNSSEC validation on. It protects your clients from spoofed or tampered answers.
  • Prefer DNS-over-TLS for clients that support it, so queries cannot be read or altered on the network.
  • Forwarding is optional. If your environment requires resolving through an upstream resolver, add a forward-zone. Resolving directly from the root, the default, is more private.
  • Keep the OS and Unbound patched. Unattended security upgrades are on.
  • Monitor the statistics. unbound-control stats reports query counts, the cache hit ratio and validation failures for your monitoring system.

Support and licensing

Unbound is developed by NLnet Labs and distributed under the BSD 3-Clause licence. This image ships the unmodified Ubuntu 24.04 Unbound package. cloudimg provides the packaging, the hardening and not an open resolver defaults, DNSSEC validation, per-instance DNS-over-TLS, and 24/7 support.

cloudimg is not affiliated with or endorsed by NLnet Labs. Unbound 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.