AdGuard Home 0.107.79 with Unbound on Ubuntu 24.04 on AWS User Guide
Overview
This guide covers launching and verifying AdGuard Home 0.107.79 with Unbound 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.
AdGuard Home is a network-wide DNS server that blocks ads, trackers and malicious domains for every device that uses it as its DNS server, with no software to install on each device. This version pairs it with Unbound, a validating, recursive, caching DNS resolver from NLnet Labs, running privately on the loopback address 127.0.0.1:5335. AdGuard sends every query it does not block to Unbound, and Unbound resolves it directly from the DNS root servers with DNSSEC validation. No third-party public resolver sees your lookups, and every signed answer is cryptographically checked.
The image installs the pinned AdGuard Home 0.107.79 release (the AdGuardHome_linux_amd64.tar.gz asset, checked against its published SHA-256) and Ubuntu's own Unbound 1.19.2 package on a fully patched Ubuntu 24.04 LTS base. Unattended security upgrades stay on for your instance.
What is on the image:
- AdGuard Home 0.107.79 (
AdGuardHome.service), serving DNS on port 53 (UDP and TCP), with the self-updater turned off - Unbound (
unbound.service) on127.0.0.1:5335only, as AdGuard's only upstream and bootstrap resolver, with no fallback to a public resolver - The AdGuard admin bound to
127.0.0.1:3080and published by nginx over HTTPS on port 443, with a certificate created on your instance's first boot; port 80 redirects to HTTPS - No setup wizard. First boot writes a complete configuration with an admin user whose password is unique to your instance
- The AdGuard DNS filter enabled, so ads and trackers are blocked from the first query
- A not an open resolver policy: DNS is answered for the local host and private address ranges only
- A dedicated 20 GiB gp3 data volume at
/var/lib/adguardhomefor the configuration, query log and statistics adguardhome-set-hostname, a helper to serve the admin under your own DNS name
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3 · Data volume: 20 GiB gp3
Security posture
- Per-instance admin password. The
adminpassword is generated on each instance's first boot and stored only as a bcrypt hash in AdGuard's configuration. The plain password is written to/root/adguardhome-credentials.txt(mode 0600, root only). No password, hash or configuration ships in the image. - No setup wizard, ever. AdGuard Home normally starts a first-run wizard that lets the first visitor create the admin account. On this image AdGuard and nginx refuse to start until first boot has written the configuration, so the wizard is never reachable and nothing listens on its port 3000.
- HTTPS on first boot. nginx serves the admin on 443 with a TLS key and self-signed certificate created on your instance (TLS 1.2 and 1.3 only). Port 80 only redirects to HTTPS.
- Fixed host name. The admin is served under a name taken from the instance metadata at first boot (or one you set), never from the request. Requests for any other host name are dropped, and cross-site form posts and WebSocket connections are refused.
- Real client addresses. nginx passes the connecting address to AdGuard and ignores any
X-Forwarded-Forheader a client sends. - Not an open resolver. AdGuard's allowed clients list ships as
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/10andfc00::/7. Queries from any other address are refused. Open resolvers are abused for DNS amplification attacks. - Unbound is private. Unbound listens on
127.0.0.1:5335only and its access control refuses every address except loopback, so only AdGuard can use it. It has no network control port and no control certificate or key. - DNSSEC validation. Unbound validates signed answers against the root trust anchor and refuses answers whose signatures do not verify.
- DNS stays closed until you open it. The listing security group opens SSH (22) and the admin (80, 443). Port 53 is never opened for you: you open it to your own VPC or VPN ranges.
- No metadata access for the DNS services. AdGuard fetches blocklists and upstream answers from URLs that an administrator types in, so
AdGuardHome.service,nginx.serviceandunbound.serviceare blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). DNS resolution and Amazon Time Sync still work. - IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests. - 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 with outbound internet access (Unbound resolves from the DNS root servers), and the CIDR ranges of the clients that will use it for DNS
Recommended instance type: m5.large. A small network runs comfortably on t3.small or larger.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| AdGuard Home 0.107.79 with Unbound 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 AdGuard Home 0.107.79 with Unbound 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), HTTP (80) and HTTPS (443). Restrict all three to your own IP address. You will open DNS to your clients in Step 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 and the admin 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 adguard-home-unbound \
--description "AdGuard Home with Unbound" --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 tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --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=adguard-home-unbound}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required.
Step 2: Connect and read your admin login
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 checks that Unbound is answering, generates the admin password, writes AdGuard's configuration, creates the HTTPS certificate and starts AdGuard and nginx. Your login is in a root-only file:
sudo cat /root/adguardhome-credentials.txt
# AdGuard Home with Unbound - generated on first boot by adguardhome-firstboot.service
# These credentials are unique to this instance. Store them somewhere safe.
ADGUARD_URL=https://ec2-18-234-190-87.compute-1.amazonaws.com/
ADGUARD_USERNAME=admin
ADGUARD_PASSWORD=<ADGUARD_PASSWORD>
ADGUARD_DNS_SERVER=172.31.89.132
# Admin dashboard: open the URL above (per-instance self-signed certificate; your browser will
# warn until you install your own) and sign in with the username and password.
#
# DNS (port 53): closed in the default security group. To use this instance as a DNS server,
# open 53/udp and 53/tcp ONLY to your own VPC CIDR, peered/VPN client ranges or specific office
# IPs - never 0.0.0.0/0 - and point clients at the instance's PRIVATE IP (172.31.89.132),
# never at its public IP. AdGuard Home answers only clients in its allowed list, which ships
# restricted to loopback and the private ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
# 100.64.0.0/10 and fc00::/7. Add your own client CIDRs under
# Settings > DNS settings > Access settings.
#
# AdGuard resolves every query recursively through the private Unbound resolver on
# 127.0.0.1:5335 (DNSSEC-validating, loopback-only) - no third-party public resolver is used.
#
# Change the hostname the admin is served as: sudo adguardhome-set-hostname <your-dns-name>
ADGUARD_PASSWORD holds your instance's own password. ADGUARD_DNS_SERVER is the instance's private IP, the address your clients use for DNS.
Step 3: Confirm the services are running
First boot, Unbound, AdGuard Home and nginx are active, the installed releases are AdGuard Home 0.107.79 and Unbound 1.19.2, and the data volume is mounted:
systemctl is-active adguardhome-firstboot.service unbound.service AdGuardHome.service nginx.service
/opt/AdGuardHome/AdGuardHome --version
unbound -V | head -1
findmnt /var/lib/adguardhome
curl -sk https://localhost/healthz
active
active
active
active
AdGuard Home, version v0.107.79
Version 1.19.2
TARGET SOURCE FSTYPE OPTIONS
/var/lib/adguardhome /dev/nvme1n1 ext4 rw,relatime
ok
/healthz answers ok without a login, so you can use it for a load balancer health check over HTTPS.
These are the only ports the instance listens on:
sudo ss -Hltnu | awk '{print $1, $5}' | sort -u
tcp *:53
tcp 0.0.0.0:22
tcp 0.0.0.0:443
tcp 0.0.0.0:80
tcp 127.0.0.1:3080
tcp 127.0.0.1:5335
tcp [::]:22
tcp [::]:443
tcp [::]:80
udp *:53
udp 127.0.0.1:323
udp 127.0.0.1:5335
udp 172.31.89.132%ens5:68
udp [::1]:323
The AdGuard admin (3080) and Unbound (5335) listen on loopback only; nginx publishes the admin on 443. Port 323 is chrony's local control socket and 68 is the DHCP client.
Step 4: Confirm recursive DNSSEC resolution through Unbound
Query Unbound directly on 127.0.0.1:5335. A normal name resolves, a DNSSEC-signed domain comes back with the ad (Authenticated Data) flag, and a domain whose signatures are deliberately broken (dnssec-failed.org) is refused with SERVFAIL, which shows that validation is enforced:
dig @127.0.0.1 -p 5335 example.com +short
dig @127.0.0.1 -p 5335 +dnssec cloudflare.com A | grep -E '^;; flags'
dig @127.0.0.1 -p 5335 dnssec-failed.org A | grep -oE 'status: [A-Z]+'
104.20.23.154
172.66.147.243
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
status: SERVFAIL
AdGuard uses Unbound as its only upstream and bootstrap resolver, with no fallback. The configuration lives in /var/lib/adguardhome/AdGuardHome.yaml (root only):
sudo python3 -c 'import yaml; d = yaml.safe_load(open("/var/lib/adguardhome/AdGuardHome.yaml"))["dns"]; print("upstream_dns:", d["upstream_dns"]); print("bootstrap_dns:", d["bootstrap_dns"]); print("fallback_dns:", d.get("fallback_dns"))'
upstream_dns: ['127.0.0.1:5335']
bootstrap_dns: ['127.0.0.1:5335']
fallback_dns: []
Unbound answers only loopback. This adds a temporary test address (192.0.2.53, from the TEST-NET-1 documentation range) to the loopback interface, sends Unbound a query from it and from 127.0.0.1, and removes the address again. The non-loopback source is refused:
sudo unbound-checkconf -o interface
sudo ip addr add 192.0.2.53/32 dev lo
dig +tcp -b 192.0.2.53 @127.0.0.1 -p 5335 example.com A +time=3 +tries=1 | grep -oE "status: [A-Z]+"
dig +tcp -b 127.0.0.1 @127.0.0.1 -p 5335 example.com A | grep -oE "status: [A-Z]+"
sudo ip addr del 192.0.2.53/32 dev lo
127.0.0.1@5335
status: REFUSED
status: NOERROR
Unbound's settings are in /etc/unbound/unbound.conf.d/cloudimg-adguardhome.conf. Names in Route 53 private hosted zones are not visible from the DNS root; to resolve them, add the VPC resolver (the VPC base address plus two, for example 10.0.0.2) as an upstream for those domains in AdGuard, for example [/internal.example.com/]10.0.0.2.
Step 5: Sign in to the dashboard
Browse to the ADGUARD_URL from Step 2, for example https://ec2-18-234-190-87.compute-1.amazonaws.com/. The certificate is self-signed, so your browser warns once; accept it, or install your own certificate (Step 9). Sign in as admin with the password from Step 2.

The dashboard shows the number of DNS queries, how many were blocked by filters, the busiest clients and the most queried and most blocked domains.

Port 80 only redirects to the HTTPS address:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost/
301 https://ec2-18-234-190-87.compute-1.amazonaws.com/
You can also check the login from the command line. A wrong password is refused by AdGuard's login handler; the instance's own password returns 200 and a session cookie:
H=$(sudo sed -n 's/^set $agh_host "\(.*\)";$/\1/p' /etc/nginx/adguardhome-host.conf)
curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -X POST --data '{"name":"admin","password":"not-the-password"}' "https://$H/control/login"
sudo grep '^ADGUARD_PASSWORD=' /root/adguardhome-credentials.txt | cut -d= -f2- | tr -d '\n' | python3 -c 'import json,sys; print(json.dumps({"name":"admin","password":sys.stdin.read()}))' > /tmp/agh-login.json
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}\n' -c /tmp/agh-cookies.txt -H 'Content-Type: application/json' -X POST --data @/tmp/agh-login.json "https://$H/control/login"
grep -o agh_session /tmp/agh-cookies.txt
rm -f /tmp/agh-login.json /tmp/agh-cookies.txt
invalid username or password
200
agh_session
After five wrong passwords from one address, AdGuard locks that address out for 15 minutes.
In Settings > DNS settings, the Upstream DNS servers box shows 127.0.0.1:5335, the private Unbound resolver:

Keep it pointing at 127.0.0.1:5335 to keep recursive, DNSSEC-validated resolution. If you replace it with a public resolver, Unbound is no longer used.
Step 6: Open DNS to your clients
The listing's security group does not open port 53. 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) |
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 (ADGUARD_DNS_SERVER in Step 2), for example in a DHCP options set for the VPC, on a VPN server, or in each client's network settings.
Test from the instance: a normal name resolves through AdGuard and Unbound, and a known ad domain is answered with 0.0.0.0 because the AdGuard DNS filter blocks it:
dig @127.0.0.1 example.com +short
dig @127.0.0.1 ad.doubleclick.net +short
104.20.23.154
172.66.147.243
0.0.0.0
From a client inside your VPC, use the private IP instead:
dig @<private-ip> example.com
Every query appears in Query Log, with the client that asked and whether it was processed or blocked, and by which filter:

Step 7: Confirm it is not an open resolver
AdGuard answers only the clients in its allowed list:
sudo python3 -c 'import yaml; c = yaml.safe_load(open("/var/lib/adguardhome/AdGuardHome.yaml")); print("allowed_clients:", c["dns"]["allowed_clients"]); print("http.address:", c["http"]["address"])'
allowed_clients: ['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', 'fc00::/7']
http.address: 127.0.0.1:3080
You can prove the policy from the instance itself. This adds the temporary public test address 192.0.2.53 to the loopback interface again, sends AdGuard a query from it over TCP and over UDP, sends the same query from 127.0.0.1, and removes the address. The public source is refused over TCP and gets no answer over UDP; 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 example.com A +time=3 +tries=1 | grep -oE "status: [A-Z]+"
dig -b 192.0.2.53 @192.0.2.53 example.com A +time=3 +tries=1 | grep -oE "status: [A-Z]+|timed out" | head -1
dig +tcp -b 127.0.0.1 @127.0.0.1 example.com A | grep -oE "status: [A-Z]+"
sudo ip addr del 192.0.2.53/32 dev lo
status: REFUSED
timed out
status: NOERROR
To serve another network you trust, for example a VPN range that uses public addresses, add it under Settings > DNS settings > Access settings > Allowed clients and save. Never add 0.0.0.0/0. Remember to allow the same range in the security group (Step 6).

Step 8: Manage blocklists
Blocklists live under Filters > DNS blocklists. The AdGuard DNS filter is enabled; the AdAway Default Blocklist is listed but turned off. Add more with Add blocklist, write your own rules under Custom filtering rules, and allow specific domains under Allowlists. Blocklists update automatically.

Step 9: HTTPS certificate and host name
The certificate was created on your instance's first boot for the instance's public DNS name, public IP, private IP and localhost. The key is readable only by root:
sudo openssl x509 -in /etc/ssl/adguardhome/adguardhome.crt -noout -subject -dates -ext subjectAltName
sudo stat -c '%a %U:%G %n' /etc/ssl/adguardhome/adguardhome.key /etc/ssl/adguardhome/adguardhome.crt /var/lib/adguardhome/AdGuardHome.yaml /root/adguardhome-credentials.txt
subject=CN = ec2-18-234-190-87.compute-1.amazonaws.com, O = cloudimg
notBefore=Oct 20 12:35:39 2026 GMT
notAfter=Jan 22 12:35:39 2029 GMT
X509v3 Subject Alternative Name:
DNS:ec2-18-234-190-87.compute-1.amazonaws.com, IP Address:18.234.190.87, IP Address:172.31.89.132, IP Address:127.0.0.1, DNS:localhost
600 root:root /etc/ssl/adguardhome/adguardhome.key
644 root:root /etc/ssl/adguardhome/adguardhome.crt
600 root:root /var/lib/adguardhome/AdGuardHome.yaml
600 root:root /root/adguardhome-credentials.txt
To serve the admin under your own DNS name, point the name at the instance, then run the helper. It records the name, creates a new self-signed certificate for it, updates nginx and rewrites the URL in the credentials file:
sudo adguardhome-set-hostname adguard.example.com
To use a certificate from your own certificate authority, replace /etc/ssl/adguardhome/adguardhome.key (keep it 0600 root) and /etc/ssl/adguardhome/adguardhome.crt, then run sudo systemctl reload nginx. If the instance's public address changes (for example after a stop and start without an Elastic IP), run the helper again with the new name.
Step 10: Metadata access is blocked for the DNS services
AdGuardHome.service, nginx.service and unbound.service 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 DNS and the Amazon Time Sync Service at 169.254.169.123 keep working, as chrony shows:
systemctl show -p IPAddressDeny AdGuardHome.service nginx.service unbound.service
chronyc -n sources | grep 169.254
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
^* 169.254.169.123 3 4 377 31 +84us[ +91us] +/- 546us
Maintenance
- Admin password: the per-instance password is in
/root/adguardhome-credentials.txt. AdGuard Home 0.107 has no password change page; to set a new one, stop AdGuard (sudo systemctl stop AdGuardHome), replace thepassword:value underusers:in/var/lib/adguardhome/AdGuardHome.yamlwith a bcrypt hash of your new password, start it again and update the credentials file. - Configuration: settings you change in the dashboard are saved to
/var/lib/adguardhome/AdGuardHome.yaml. After editing the file by hand, runsudo systemctl restart AdGuardHome. - Unbound: its settings are in
/etc/unbound/unbound.conf.d/cloudimg-adguardhome.conf; check a change withsudo unbound-checkconf, then runsudo systemctl restart unbound. Keep theinterface: 127.0.0.1@5335line and the loopback-onlyaccess-controllines so Unbound stays private. The DNSSEC root trust anchor in/var/lib/unbound/root.keyis kept current automatically. - Storage: the configuration, query log, statistics and blocklist caches live under
/var/lib/adguardhomeon the dedicated data volume. Snapshot that volume to back them up, and grow it independently if you keep a long query log. - Upgrades: the image runs AdGuard Home with
--no-check-update, so it never replaces itself. To move to a newer release, follow AdGuard's upgrade instructions for a manual install in/opt/AdGuardHomeand keep a snapshot first. Unbound is updated with the operating system's security updates. - Security patches: unattended-upgrades remains enabled so the OS keeps receiving security updates.
Security recommendations
- Keep the security group tight. Open 53 only to the networks that use the server, and 22, 80 and 443 only to your management address.
- Keep the allowed clients list scoped. Only add networks you trust, never
0.0.0.0/0. - Keep Unbound on loopback. Never change its interface or access control to answer other hosts; clients should always query AdGuard on port 53.
- Use your own certificate for the admin if other people sign in to it, so browsers trust it without a warning.
- Review the query log retention under Settings > General settings if your clients' browsing history must not be kept.
Support and licensing
AdGuard Home is developed by AdGuard Software Ltd and distributed under the GNU General Public License v3.0. This image ships the unmodified upstream release binary. Unbound is developed by NLnet Labs and distributed under the BSD 3-Clause License; this image uses Ubuntu's own package. cloudimg provides the packaging, the hardening and not an open resolver defaults, the private Unbound pairing, first-boot HTTPS and per-instance credentials, and 24/7 support.
cloudimg is not affiliated with or endorsed by AdGuard Software Ltd or NLnet Labs. AdGuard and Unbound are marks of their owners and are used here only to identify the software.
For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.