Deployment guide

AdGuard Home 0.107.79 with Unbound

Overview

AdGuard Home is a network-wide DNS server that blocks ads, trackers and malicious domains for every device that uses it, with no software to install on each device. This SecureAnyCloud image pairs AdGuard Home 0.107.79 with Unbound, a validating, recursive, caching DNS resolver from NLnet Labs, on Ubuntu 24.04 on Amazon EC2.

Unbound runs 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.

This is one version of the SecureAnyCloud 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.

What you get:

  • AdGuard Home 0.107.79 (AdGuardHome.service) serving DNS on port 53 (UDP and TCP), with the self-updater turned off
  • Unbound (unbound.service) on 127.0.0.1:5335 only, as AdGuard's only upstream and bootstrap resolver, with no fallback to a public resolver
  • The AdGuard admin bound to 127.0.0.1:3080 and 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/adguardhome for the configuration, query log and statistics
  • adguardhome-set-hostname, a helper that serves the admin under your own DNS name
  • AdGuard Home 0.107.79 built by SecureAnyCloud from the upstream v0.107.79 source tag with the current Go 1.27.3 toolchain and patched Go dependencies (golang.org/x/net, x/crypto, x/text and insomniacslk/dhcp), and Ubuntu's own Unbound 1.19.2 package, on a fully patched Ubuntu 24.04 LTS base
  • 24/7 SecureAnyCloud support

The upstream release binary was built with Go 1.26.6 and carries known Go vulnerabilities. This build fixes them, and govulncheck reports none for the shipped binary. Unattended security upgrades stay on for your instance.

Default user: ubuntu. Architecture: x86_64. Root volume: 8 GiB gp3. Data volume: 20 GiB gp3.

Security posture

  • Per-instance admin password. The admin password 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-For header 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/10 and fc00::/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:5335 only 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.service and unbound.service are 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.
  • Rebuilt for known Go vulnerabilities. /usr/share/doc/adguardhome-sac/BUILD-PROVENANCE.txt records the source commit, the toolchain, the dependency patches and the govulncheck results (0 vulnerabilities reachable from the binary or the source). Two advisories are listed for information only: GO-2026-5932 (golang.org/x/crypto/openpgp, no upstream fix) is in a module the build requires but no AdGuard Home package imports it, and GO-2024-2924 (CVE-2024-36586) affects AdGuard Home 0.107.51 and earlier only.

Prerequisites

You need:

  • 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, because Unbound resolves from the DNS root servers
  • The CIDR ranges of the clients that will use the server for DNS

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

Connecting to your instance

Connect over SSH as the ubuntu user. The image is AdGuard Home 0.107.79 with Unbound on Ubuntu 24.04 LTS.

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.
  • 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 AdGuard Home 0.107.79 with Unbound on Ubuntu 24.04, then choose your Region.
  • Pick an instance type and your key pair. m5.large is recommended. The software charge is $0.04 per vCPU per hour.
  • Set the security group. The listing's default group allows SSH (22), HTTP (80) and HTTPS (443). Restrict all three to your own IP address. You 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. Replace 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.

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.

3

Confirm the services are running

Run these commands to confirm that the first-boot service, Unbound, AdGuard Home and nginx are active, that the installed releases are AdGuard Home 0.107.79 and Unbound 1.19.2, and that 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, and nginx publishes the admin on 443. Port 323 is chrony's local control socket, and port 68 is the DHCP client.

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/sac-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.

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.

6

Open DNS to your clients

The listing's security group does not open port 53. 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, for DNS
  • TCP 53 from your VPC or VPN CIDRs, for DNS with large answers

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 (ADGUARD_DNS_SERVER in Step 2). You can set it 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, whether the query was processed or blocked, and which filter blocked it.

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).

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 blocklists with Add blocklist. Write your own rules under Custom filtering rules, and allow specific domains under Allowlists. Blocklists update automatically.

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 = sac 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.

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 the password: value under users: in /var/lib/adguardhome/AdGuardHome.yaml with 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, run sudo systemctl restart AdGuardHome.
  • Unbound: its settings are in /etc/unbound/unbound.conf.d/sac-adguardhome.conf; check a change with sudo unbound-checkconf, then run sudo systemctl restart unbound. Keep the interface: 127.0.0.1@5335 line and the loopback-only access-control lines so Unbound stays private. The DNSSEC root trust anchor in /var/lib/unbound/root.key is kept current automatically.
  • Storage: the configuration, query log, statistics and blocklist caches live under /var/lib/adguardhome on 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/AdGuardHome and 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. SecureAnyCloud provides the packaging, the hardening and not-an-open-resolver defaults, the private Unbound pairing, first-boot HTTPS, per-instance credentials, and 24/7 support.

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

Deploy on AWS

Launch AdGuard Home 0.107.79 with Unbound 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