SmartDNS 48.4 on AWS User Guide
Overview
This guide covers launching and verifying SmartDNS 48.4 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.
SmartDNS is an open source local DNS server by Nick Peng (pymumu). It sends each lookup to several upstream resolvers at the same time, measures how quickly each returned address actually responds, and hands your client the fastest address. A name such as github.com or a large CDN hostname resolves to different addresses depending on which resolver you ask, so the address you get matters as much as how fast you got it. SmartDNS also caches aggressively, speaks encrypted upstream protocols (DNS over TLS, DNS over HTTPS and QUIC) and ships a web interface with query logs and per upstream statistics.
The image installs the official SmartDNS Release 48.4 Debian package published by the upstream project (the newest stable release, with no published advisory open against it). The build downloads that exact release asset and refuses to continue unless its SHA256 checksum matches the pinned value, which is also the digest GitHub records for the asset.
What is on the image:
- SmartDNS Release 48.4 (
smartdns.service), running as the unprivileged system usersmartdns - Fastest address selection:
response-mode fastest-ipwith a TCP based speed check (tcp:443,tcp:80), chosen over ICMP ping because outbound ICMP is often blocked in cloud networks - Six upstream resolvers, encrypted first: DNS over TLS to Cloudflare, Google and Quad9 with hostname verification, plus their plain UDP addresses as a fallback for networks that block port 853
- A cache with prefetch and stale answer serving, so repeat lookups are answered in a millisecond or two
- The SmartDNS web interface on
127.0.0.1:6080(dashboard, query log, upstream servers, clients) with a password generated for your instance on first boot - Two admission boundaries, SmartDNS's own access list and an nftables firewall, plus
smartdns-openresolver-check.sh, an audit tool that proves both work digfor testing, and unattended security upgrades for the operating system
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- Not an open resolver. SmartDNS listens on every interface so clients anywhere in your VPC can reach it, but two independent boundaries decide who is served. SmartDNS's own access list (
acl-enable yeswithclient-rules) answersREFUSED, with no answer data, to any source outside loopback,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,fc00::/7andfe80::/10. In front of it, nftables drops DNS (UDP and TCP 53) from every other source without a reply. 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) stays closed until you open it to your own VPC or VPN ranges. Clients use the instance's private IP.
- No default credentials. The SmartDNS web interface documents a default login of
admin/password. That login is never configured: in the shipped image the web interface is not loaded at all, and on first boot your instance generates its own 32 character password, starts the interface with it, and checks that the published default is refused. The password is stored in root-only files. - Fails closed. If first boot cannot finish (the resolver does not answer, the login checks fail or the open resolver audit fails), it removes the web interface configuration again, stops SmartDNS and tries again on the next boot.
- Web interface on loopback only. It listens on
127.0.0.1:6080over plain HTTP, so you reach it through an SSH tunnel. Its in-browser terminal and cross-origin requests are switched off. - Least privilege. SmartDNS binds port 53 as root and then switches to the
smartdnsuser, keeping only the network capabilities it needs. - No metadata access for the resolver. SmartDNS connects to the addresses it is about to return (that is how it measures speed), so
smartdns.serviceand its 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 VPC resolver still work. - IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests. - Key-only SSH. Password logins, keyboard-interactive logins, root logins and X11 forwarding are disabled. 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 resolver, and the CIDR ranges of the clients that will use it
Recommended instance type: m5.large. A resolver for a small network runs comfortably on t3.small or larger.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| SmartDNS 48.4 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- 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.
- 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 SmartDNS 48.4 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - 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 7.
- 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 smartdns-resolver \
--description "SmartDNS 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 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=smartdns-resolver}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required.
Step 2: Connect and find your credentials
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 generates this instance's web interface password, starts the interface, proves the resolver answers and that it is not an open resolver, and then writes /root/smartdns-credentials.txt (mode 0600, root only). Read it with sudo cat; the listing below hides the password so it does not appear in this guide:
sudo stat -c '%a %U:%G %n' /root/smartdns-credentials.txt
sudo sed -E 's/^(smartdns\.ui\.password)=.*/\1=<hidden>/' /root/smartdns-credentials.txt | grep -E '^smartdns'
600 root:root /root/smartdns-credentials.txt
smartdns.host=34.204.6.143
smartdns.private.ip=172.31.87.111
smartdns.dns.udp=172.31.87.111:53
smartdns.dns.tcp=172.31.87.111:53
smartdns.ui.user=cloudimg
smartdns.ui.password=<hidden>
smartdns.ui.url=http://127.0.0.1:6080/
smartdns.ui.port=6080
smartdns.openresolver=verified at first boot (nftables drops public sources, SmartDNS REFUSES sources outside client-rules, private sources served)
| Entry | Used for |
|---|---|
smartdns.private.ip |
the address your clients query (Step 7) |
smartdns.ui.user, smartdns.ui.password |
signing in to the web interface (Step 8) |
smartdns.host |
the address in the SSH tunnel command (the public IP, or the private IP in a private subnet) |
Step 3: Confirm the resolver is running
SmartDNS and the nftables firewall are active and enabled, so both come back after a reboot. SmartDNS runs as the smartdns user, DNS listens on port 53 (UDP and TCP) on every interface, and the web interface listens on 127.0.0.1:6080 only:
systemctl is-active smartdns.service nftables.service smartdns-firstboot.service
systemctl is-enabled smartdns.service nftables.service
smartdns -v
ps -o user=,pid=,comm= -C smartdns
sudo ss -tulnp | grep -E ':53 |:6080 '
active
active
active
enabled
enabled
smartdns 1.2026.08.05-0921 (Release48.4)
smartdns 1050 smartdns
udp UNCONN 0 0 *:53 *:* users:(("smartdns",pid=1050,fd=9))
tcp LISTEN 0 128 127.0.0.1:6080 0.0.0.0:* users:(("smartdns",pid=1050,fd=29))
tcp LISTEN 0 256 *:53 *:* users:(("smartdns",pid=1050,fd=10))
The installed package is the pinned upstream release, and its checksum is recorded on the image:
cat /etc/cloudimg/smartdns-release
release=Release48.4
package_version=2026.08.05-0921
deb_sha256=4cc1b8f9db10110e1212d981cfee7eb89a3c80f5766760e3b5f7541c8da8b546
Step 4: Resolve a name
Query the resolver on the instance:
dig @127.0.0.1 github.com A +short
140.82.114.4
Run the same lookup twice and compare the timings. The first query is resolved upstream; the second is answered from the cache:
dig @127.0.0.1 cloudflare.com A | grep -E 'status:|Query time'
dig @127.0.0.1 cloudflare.com A | grep -E 'Query time'
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3567
;; Query time: 4 msec
;; Query time: 1 msec
Step 5: See how SmartDNS chooses the fastest address
The cloudimg resolver configuration is in /etc/smartdns/conf.d/10-cloudimg-resolver.conf:
grep -E '^(response-mode|speed-check-mode|server|server-tls) ' /etc/smartdns/conf.d/10-cloudimg-resolver.conf
server-tls 1.1.1.1:853 -host-name cloudflare-dns.com -tls-host-verify cloudflare-dns.com
server-tls 8.8.8.8:853 -host-name dns.google -tls-host-verify dns.google
server-tls 9.9.9.9:853 -host-name dns.quad9.net -tls-host-verify dns.quad9.net
server 1.1.1.1
server 8.8.8.8
server 9.9.9.9
speed-check-mode tcp:443,tcp:80
response-mode fastest-ip
response-mode fastest-ip tells SmartDNS to return the address that answered its speed check first, and speed-check-mode tcp:443,tcp:80 is how it measures: it opens a TCP connection to port 443 (or 80) of each candidate address. The six server and server-tls lines are the resolvers it asks in parallel. The web interface shows what it measured for each upstream (Step 8).
Step 6: Verify it is not an open resolver
This is the most important check on a DNS server, and the image proves it rather than just claiming it. Run the bundled audit:
sudo /usr/local/sbin/smartdns-openresolver-check.sh
cloudimg SmartDNS open-resolver audit
[ok] nftables boundary loaded: DNS + Web UI dropped by default, no blanket accept
[ok] SmartDNS ACL on (acl-enable yes), no client-rules for 0.0.0.0/0 or ::/0
[ok] private-source query from 10.255.254.2 was served (NOERROR)
[ok] public-source query from 198.51.100.2 was DROPPED by nftables (no response)
[ok] query from 192.0.2.53 (outside the ACL, admitted by nftables via lo) was REFUSED by SmartDNS
OPEN-RESOLVER-CHECK PASS: DNS is served to private clients and refused to everyone else (nftables drop + SmartDNS ACL REFUSED).
The audit tests each boundary on its own. It creates two temporary network namespaces attached to the instance, one with a private source address and one with a public documentation address, and queries SmartDNS from each: the private client must be answered and the public one must get no reply at all (nftables drops it). It then puts the documentation address 192.0.2.53 on the loopback interface, which nftables allows, and queries from it: SmartDNS itself must answer REFUSED, because that address is in none of its client-rules. The audit fails if either boundary is missing or has been widened to the whole internet. First boot runs it too, and refuses to finish if it fails.
You can read both boundaries directly:
grep -E '^(acl-enable|client-rules) ' /etc/smartdns/conf.d/10-cloudimg-resolver.conf
sudo nft list table inet cloudimg_smartdns | grep -E 'dport (53|6080)'
acl-enable yes
client-rules 127.0.0.0/8
client-rules ::1/128
client-rules 10.0.0.0/8
client-rules 172.16.0.0/12
client-rules 192.168.0.0/16
client-rules 100.64.0.0/10
client-rules fc00::/7
client-rules fe80::/10
ip saddr @cloudimg_private_v4 udp dport 53 accept
ip saddr @cloudimg_private_v4 tcp dport 53 accept
ip6 saddr @cloudimg_private_v6 udp dport 53 accept
ip6 saddr @cloudimg_private_v6 tcp dport 53 accept
udp dport 53 drop
tcp dport 53 drop
ip saddr @cloudimg_private_v4 tcp dport 6080 accept
ip6 saddr @cloudimg_private_v6 tcp dport 6080 accept
tcp dport 6080 drop
When cloudimg tested this image with port 53 temporarily open in the security group, a query to the public IP from a workstation on the internet timed out (dropped), and a query to the private IP from another instance in the same VPC was answered with NOERROR.
Step 7: Point your VPC clients at the resolver
Open DNS to your clients in the instance's security group, only from your own ranges (your VPC CIDR, peered VPCs or VPN client ranges), never from 0.0.0.0/0. DNS uses UDP 53 for most queries and TCP 53 for large answers, so open both:
aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol udp --port 53 --cidr 10.0.0.0/16
aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 53 --cidr 10.0.0.0/16
Clients always use the resolver's private IP (smartdns.private.ip in the credentials file). This query, run on the instance itself, uses that address the same way a client in your VPC would:
dig @<SMARTDNS_PRIVATE_IP> github.com A +short
140.82.114.3
140.82.112.4
140.82.113.4
140.82.113.3
140.82.114.4
To make it the resolver of one Ubuntu client, set it on the client's interface (the file /etc/resolv.conf is managed, so do not edit it directly):
sudo resolvectl dns ens5 <SMARTDNS_PRIVATE_IP>
sudo resolvectl domain ens5 '~.'
resolvectl status ens5
To give it to every instance in a VPC, create a DHCP options set whose domain-name-servers is the resolver's private IP and associate it with the VPC. Instances pick it up when they renew their DHCP lease or reboot:
aws ec2 create-dhcp-options --dhcp-configurations "Key=domain-name-servers,Values=<SMARTDNS_PRIVATE_IP>"
aws ec2 associate-dhcp-options --dhcp-options-id dopt-0123456789abcdef0 --vpc-id vpc-0123456789abcdef0
If your clients are outside the private ranges listed in Step 6, add their range to both boundaries first (Step 9).
Step 8: Open the web interface
The web interface listens on 127.0.0.1 only, so reach it from your workstation through an SSH tunnel to the instance:
ssh -i ~/.ssh/my-key-pair.pem -L 6080:127.0.0.1:6080 ubuntu@<public-ip>
With the tunnel open, browse to http://127.0.0.1:6080/ and sign in as cloudimg with the smartdns.ui.password from /root/smartdns-credentials.txt.

The Dashboard shows live totals: queries answered, the cache hit rate, cache size and the average query time.

Upstream Servers is where the product's behaviour becomes visible. Each configured resolver is listed with its measured average response time, its success rate and how many queries it handled. The tls rows are the encrypted upstreams.

Query Log lists every lookup with its client, record type and the measured ping of the address that was returned.

You can check from the shell that the interface requires a login and that the published default login is refused:
curl -s -o /dev/null -w 'unauthenticated: HTTP %{http_code}\n' http://127.0.0.1:6080/api/stats/overview
curl -s -o /dev/null -w 'upstream default admin/password: HTTP %{http_code}\n' \
-X POST http://127.0.0.1:6080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"password"}'
unauthenticated: HTTP 401
upstream default admin/password: HTTP 401
Your own credentials return a session token. This reads the password from the credentials file and sends it to curl on standard input, so it never appears on a command line:
PW=$(sudo sed -n 's/^smartdns\.ui\.password=//p' /root/smartdns-credentials.txt)
printf '{"username":"cloudimg","password":"%s"}' "$PW" \
| curl -s -X POST http://127.0.0.1:6080/api/auth/login -H 'Content-Type: application/json' --data-binary @- \
| grep -o '"token"'
unset PW
"token"
Step 9: Serve additional networks
If you need to serve clients outside the private ranges allowed by default, add their range to both boundaries; never remove a boundary or open it to 0.0.0.0/0. For example, for 198.18.0.0/15:
- SmartDNS access list: add a
client-rulesline in a file of your own and restart SmartDNS.
echo 'client-rules 198.18.0.0/15' | sudo tee /etc/smartdns/conf.d/20-my-clients.conf
sudo systemctl restart smartdns
- nftables: edit
/etc/nftables.conf, add the range to thecloudimg_private_v4set (IPv6 ranges go incloudimg_private_v6), then load it:
sudo nano /etc/nftables.conf
sudo nft -f /etc/nftables.conf
Then run the audit from Step 6 again. Restarting SmartDNS also restarts the web interface, so sign in again afterwards.
Step 10: Customise the resolver
All cloudimg configuration is in /etc/smartdns/conf.d/, and /etc/smartdns/smartdns.conf simply includes that directory. Add your own *.conf file there to extend or override the behaviour, then run sudo systemctl restart smartdns. Useful directives: address /ads.example.com/# blocks a name, nameserver /internal.example.com/office sends a domain to an upstream group, and cache-size tunes the cache. The SmartDNS documentation has the full configuration reference.
The cache is deliberately not saved to disk (cache-persist no), and SmartDNS's query log database lives in /var/lib/smartdns/.
Step 11: Change the web interface password
There are two ways:
- In the web interface: open Settings, enter the current and the new password and choose Update. SmartDNS stores the new password (hashed) in its own database in
/var/lib/smartdns/, and that stored password takes precedence over the configuration file from then on. - In the configuration file: change the
smartdns-ui.passwordline in/etc/smartdns/conf.d/50-cloudimg-webui.conf(root only, mode 0600) and runsudo systemctl restart smartdns. This works as long as no password has been set in the web interface.
Either way, update /root/smartdns-credentials.txt so your record stays accurate.
Step 12: Security recommendations
- Keep port 53 closed to the internet in your security group. Both boundaries on the instance already refuse public clients, but the security group should agree.
- Never open port 6080 in the security group. Use the SSH tunnel.
- Re-run
smartdns-openresolver-check.shafter any change toclient-rulesor the firewall, and treat a failure as an incident. - Restrict SSH (TCP 22) to your management addresses.
- Leave unattended security upgrades on, so the operating system keeps receiving patches. SmartDNS itself is a pinned upstream package: cloudimg publishes a new version of this AMI for new SmartDNS releases.
Support and licensing
SmartDNS is free software licensed under the GNU General Public License, version 3 or later. The licence text is on the image at /usr/share/doc/cloudimg/smartdns/LICENSE, and the source is at github.com/pymumu/smartdns.
For help with this image, contact cloudimg support at support@cloudimg.co.uk.
Trademarks: SmartDNS is the name of the upstream open source project and is used here only to identify the software this image packages. cloudimg is not affiliated with or endorsed by the SmartDNS project or its author.