Pi-hole 6.5 on AWS User Guide
Overview
This guide covers launching and verifying Pi-hole 6.5 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.
Pi-hole is a network-wide DNS sinkhole: every device that uses it as its DNS server has ads, trackers and malicious domains blocked, with no software to install on the device. Point a device, a VPC or a VPN at this instance on port 53 and its DNS lookups are filtered, logged and shown in Pi-hole's web dashboard.
The image installs the pinned Pi-hole 6.5 release set: Core v6.5, Web v6.6.1 and FTL v6.8, all published on 2026-10-17 and carrying that day's security fixes. The official installer is run from the Core v6.5 tag against sources checked out at the release commits and against the FTL v6.8 binary, which is verified against its published SHA-256. Nothing is downloaded from a moving "latest" address. The base is a fully patched Ubuntu 24.04 LTS, and unattended security upgrades stay on for your instance.
What is on the image:
- Pi-hole FTL v6.8 (
pihole-FTL.service), which is both the DNS server on port 53 (UDP and TCP) and Pi-hole's built-in web server for the dashboard and REST API. Pi-hole 6 needs no lighttpd, PHP or nginx. - The dashboard over HTTPS on port 443, with a certificate created on your instance's first boot. Port 80 only redirects to HTTPS.
- An admin password unique to your instance, created on first boot. Pi-hole will not start until it exists.
- The StevenBlack unified hosts list loaded into the blocklist (gravity), so ads and trackers are blocked from the first query.
- A not an open resolver policy: DNS is answered for loopback and private address ranges only, enforced by the kernel firewall.
pi-hole-set-hostname, a helper to serve the dashboard under your own DNS name.
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- Per-instance admin password. The dashboard password is generated on each instance's first boot. Pi-hole stores only a hash of it; the password itself is written to
/root/pi-hole-credentials.txt(mode 0600, root only). The image ships no password, and never an empty one (in Pi-hole 6 an empty password means no login at all). - Fail closed.
pihole-FTL.servicerefuses to start until first boot has created the password and the TLS certificate, and it also requires the DNS access list and the metadata block below. DNS and the dashboard are never served with build-time state. - HTTPS on first boot. The dashboard and API are served on 443 with a TLS key and self-signed certificate created on your instance. TLS 1.0 and 1.1 are refused. Port 80 redirects to the configured host name, never to the host name a request asks for.
- Not an open resolver. Pi-hole only answers DNS queries from
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,::1andfc00::/7. Queries from any other address are dropped by an nftables rule before they reach Pi-hole, so even a security group opened too widely cannot turn the instance into an open resolver. Open resolvers are abused for DNS amplification attacks. - DNS stays closed until you open it. The listing security group opens SSH (22) and the dashboard (80, 443). Port 53 is never opened for you: you open it to your own VPC or VPN ranges.
- No metadata access for Pi-hole. Pi-hole downloads the blocklist URLs that an administrator adds.
pihole-FTL.servicecarries a systemdIPAddressDenyrule, and everything running as thepiholeuser (the weekly blocklist update, the update checker) is blocked by an nftables rule, so none of them can reach the instance metadata service. - No DHCP or NTP server. Pi-hole's built-in DHCP and NTP servers are turned off, so nothing listens on 67 or 123. EC2 keeps its own time sync.
- IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests. - Hardened SSH and per-instance identity. SSH accepts keys only, with no root login and no X11 forwarding. 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 server, 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 |
|---|---|---|
| Pi-hole 6.5 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 Pi-hole 6.5 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 5.
- 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 dashboard 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 pi-hole \
--description "Pi-hole" --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=pi-hole}]'
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 generates the admin password, creates the HTTPS certificate for the instance's public DNS name, and only then lets Pi-hole start. Your login is in a root-only file:
sudo cat /root/pi-hole-credentials.txt
# Pi-hole - generated on first boot by pi-hole-firstboot.service
# This admin password is unique to this instance. Store it somewhere safe.
PIHOLE_URL=https://ec2-54-174-249-237.compute-1.amazonaws.com/admin
PIHOLE_API_URL=https://ec2-54-174-249-237.compute-1.amazonaws.com/api
pihole.admin.pass=<PIHOLE_ADMIN_PASSWORD>
# Admin dashboard: https://ec2-54-174-249-237.compute-1.amazonaws.com/admin (self-signed certificate minted on
# this instance's first boot; port 80 redirects to https)
# Change the name: sudo pi-hole-set-hostname <your-dns-name>
#
# DNS (port 53) is CLOSED in the security group by default. To use this Pi-hole
# as your DNS server:
# 1. open UDP and TCP 53 ONLY to your own VPC CIDR, peered/VPN client ranges or
# specific office IPs - never 0.0.0.0/0;
# 2. point your clients at this instance's PRIVATE IP: 172.31.88.72
# (never the public IP).
# Pi-hole answers only loopback, RFC 1918, 100.64.0.0/10 and IPv6 ULA clients
# (nftables ACL /etc/cloudimg/pihole-dns-acl.nft); every other source is dropped,
# so it is never an open resolver. To serve another range, add it there and run
# sudo systemctl restart cloudimg-pihole-dns-acl.service
pihole.admin.pass holds your instance's own password. The private IP in the file is the address your clients use for DNS.
Step 3: Confirm the services are running
First boot, the DNS access list, the metadata block and Pi-hole itself are active, and the installed release set is Core v6.5, Web v6.6.1 and FTL v6.8:
systemctl is-active pi-hole-firstboot.service cloudimg-pihole-dns-acl.service cloudimg-pihole-imds-deny.service pihole-FTL.service
pihole -v
cat /etc/cloudimg/pi-hole-release
active
active
active
active
Core version is v6.5 (Latest: v6.5)
Web version is v6.6.1 (Latest: v6.6.1)
FTL version is v6.8 (Latest: v6.8)
core=v6.5 f503a65d3a07cb71fd931389f1d3babf718249aa
web=v6.6.1 fb699c4e3fd6965d1c9a5253250d8a9b3c6f7a0e
ftl=v6.8 sha256:1f511e7053a5ddfae1db1f439ebb986dd8025bf7223e1f70fb8087d9c0fd2b0e
These are the only ports the instance listens on:
sudo ss -Hltnu | awk '{print $1, $5}' | sort -u
tcp 0.0.0.0:22
tcp 0.0.0.0:443
tcp 0.0.0.0:53
tcp 0.0.0.0:80
tcp [::]:22
tcp [::]:443
tcp [::]:53
tcp [::]:80
udp 0.0.0.0:53
udp 127.0.0.1:323
udp 172.31.88.72%ens5:68
udp [::1]:323
udp [::]:53
Port 53 is DNS, 80 and 443 are the dashboard, 323 is chrony's local control socket and 68 is the DHCP client. There is no DHCP server (67) and no NTP server (123).
Step 4: Sign in to the dashboard
Browse to the PIHOLE_URL from Step 2, for example https://ec2-54-174-249-237.compute-1.amazonaws.com/admin. The certificate is self-signed, so your browser warns once; accept it, or install your own certificate (Step 8). Enter the password from Step 2.

The dashboard shows the total number of queries, how many were blocked, the size of the blocklist, and the busiest clients and domains.

Port 80 only redirects to the HTTPS address, and the API refuses requests without a session:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost/admin/
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost/api/config/dns
308 https://ec2-54-174-249-237.compute-1.amazonaws.com/admin/
401
You can also check the login from the command line. A wrong password is refused with 401; the instance's own password returns a valid session. The password is read from the root-only file and passed to curl on standard input, so it never appears on a command line:
printf '{"password":"not-the-password"}' | curl -sk -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' --data-binary @- https://localhost/api/auth
sudo grep '^pihole.admin.pass=' /root/pi-hole-credentials.txt | cut -d= -f2- | tr -d '\n' | python3 -c 'import json,sys; print(json.dumps({"password":sys.stdin.read()}))' | curl -sk -H 'Content-Type: application/json' --data-binary @- https://localhost/api/auth | python3 -c 'import json,sys; s=json.load(sys.stdin)["session"]; print("valid:", s["valid"], "totp:", s["totp"])'
401
valid: True totp: False
To change the password later, use Settings > Web interface / API in the dashboard, or run sudo pihole setpassword and type the new password at the prompt.
Step 5: 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 (shown in the credentials file 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, and a domain on the blocklist is answered with 0.0.0.0:
dig @127.0.0.1 example.com +short
dig @127.0.0.1 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 the Query Log, with the client that asked and whether it was forwarded upstream, answered from cache or blocked:

Step 6: Confirm it is not an open resolver
Pi-hole listens on every interface so that clients in any private range of your VPC, peered VPCs and VPNs can use it. The client restriction is enforced by the kernel: the nftables table cloudimg_pihole_dns_acl drops every DNS query whose source is not in the two allow sets, and counts what it drops:
sudo pihole-FTL --config dns.listeningMode
sudo nft list table inet cloudimg_pihole_dns_acl
ALL
table inet cloudimg_pihole_dns_acl {
set allow4 {
type ipv4_addr
flags interval
elements = { 10.0.0.0/8, 100.64.0.0/10,
127.0.0.0/8, 172.16.0.0/12,
192.168.0.0/16 }
}
set allow6 {
type ipv6_addr
flags interval
elements = { ::1,
fc00::/7 }
}
chain input {
type filter hook input priority filter; policy accept;
meta l4proto { tcp, udp } th dport 53 ip saddr != @allow4 counter packets 0 bytes 0 drop comment "cloudimg-dns-acl-v4"
meta l4proto { tcp, udp } th dport 53 ip6 saddr != @allow6 counter packets 0 bytes 0 drop comment "cloudimg-dns-acl-v6"
}
}
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 to the private IP over UDP and over TCP, sends the same query from the private IP itself, and removes the address again. The public source gets no answer and the drop counter rises; the private source is answered:
PRIV=$(hostname -I | awk '{print $1}')
sudo ip addr add 192.0.2.53/32 dev lo
dig +short +time=2 +tries=1 -b 192.0.2.53 @"$PRIV" example.com A || true
dig +tcp +short +time=2 +tries=1 -b 192.0.2.53 @"$PRIV" example.com A || true
sudo ip addr del 192.0.2.53/32 dev lo
dig +short @"$PRIV" example.com A
sudo nft list chain inet cloudimg_pihole_dns_acl input | grep -o 'counter packets [0-9]* bytes [0-9]* drop comment "cloudimg-dns-acl-v4"'
;; communications error to 172.31.88.72#53: timed out
;; Connection to 172.31.88.72#53(172.31.88.72) for example.com failed: timed out.
104.20.23.154
172.66.147.243
counter packets 4 bytes 260 drop comment "cloudimg-dns-acl-v4"
To serve a range that is not private (for example a VPN pool or a partner network with public addresses), add it to allow4 (or allow6) in /etc/cloudimg/pihole-dns-acl.nft, then reload the list. Keep the security group restricted to the same ranges:
sudo nano /etc/cloudimg/pihole-dns-acl.nft
sudo systemctl restart cloudimg-pihole-dns-acl.service pihole-FTL.service
Never add 0.0.0.0/0 or ::/0.
Step 7: Blocklists
The blocklist (gravity) ships loaded with the StevenBlack unified hosts list:
sudo sqlite3 /etc/pihole/gravity.db 'SELECT address FROM adlist; SELECT COUNT(*) FROM gravity;'
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
71493
Add more lists under Lists in the dashboard, then choose Tools > Update Gravity (or run sudo pihole -g). Pi-hole also refreshes every list once a week. Allow or deny individual domains under Domains.

Step 8: 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 file is readable only by the pihole user that runs Pi-hole:
sudo openssl x509 -in /etc/pihole/tls.crt -noout -subject -dates -ext subjectAltName
sudo stat -c '%a %U:%G %n' /etc/pihole/tls.pem /root/pi-hole-credentials.txt
sudo pihole-FTL --config webserver.domain
subject=CN = ec2-54-174-249-237.compute-1.amazonaws.com, O = cloudimg
notBefore=Oct 21 02:42:20 2026 GMT
notAfter=Jan 23 02:42:20 2029 GMT
X509v3 Subject Alternative Name:
DNS:ec2-54-174-249-237.compute-1.amazonaws.com, IP Address:54.174.249.237, IP Address:172.31.88.72, IP Address:127.0.0.1, DNS:localhost
600 pihole:pihole /etc/pihole/tls.pem
600 root:root /root/pi-hole-credentials.txt
ec2-54-174-249-237.compute-1.amazonaws.com
To serve the dashboard 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, points the port-80 redirect at it, restarts Pi-hole and rewrites the URL in the credentials file:
sudo pi-hole-set-hostname pihole.example.com
To use a certificate from your own certificate authority, write the certificate followed by its private key to /etc/pihole/tls.pem (keep it 0600 pihole:pihole), then run sudo systemctl restart pihole-FTL. 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 9: Metadata access is blocked for Pi-hole
pihole-FTL.service carries a systemd IPAddressDeny rule for the instance metadata addresses, and the nftables table cloudimg_pihole_imds drops metadata traffic from the pihole user (uid 997), which runs the scheduled blocklist updates outside the 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 pihole-FTL.service
sudo nft list table inet cloudimg_pihole_imds
chronyc -n sources | grep -c '169.254.169.123'
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 ::ffff:169.254.169.254/128
table inet cloudimg_pihole_imds {
chain output {
type filter hook output priority filter; policy accept;
meta skuid 997 ip daddr { 168.63.129.16, 169.254.169.254 } counter packets 0 bytes 0 drop
meta skuid 997 ip6 daddr { ::ffff:169.254.169.254, fd00:ec2::254 } counter packets 0 bytes 0 drop
}
}
1
Maintenance
- Admin password: the per-instance password is in
/root/pi-hole-credentials.txt. Change it in Settings > Web interface / API or withsudo pihole setpassword, and update or delete the file afterwards. - Configuration: settings you change in the dashboard are saved to
/etc/pihole/pihole.toml. Change single settings from the shell withsudo pihole-FTL --config <key> <value>. - Upgrades: run
sudo pihole -upto move to a newer Pi-hole release when one is published. The image ships Pi-hole's own Core and Web checkouts (/etc/.piholeand/var/www/html/admin, at the v6.5 and v6.6.1 release tags, pointing at the official GitHub repositories) so thatpihole -upand the version check in the dashboard work. Take a snapshot first. - 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 DNS access list scoped. Only add networks you trust to
/etc/cloudimg/pihole-dns-acl.nft, never0.0.0.0/0. - Use your own certificate for the dashboard if other people sign in to it, so browsers trust it without a warning.
- Turn on two-factor authentication under Settings > Web interface / API if the dashboard is reachable from more than your own address.
- Review the query log settings under Settings > Privacy if your clients' browsing history must not be kept.
Support and licensing
Pi-hole is developed by Pi-hole LLC and distributed under the European Union Public Licence (EUPL) v1.2. This image ships the unmodified upstream release. cloudimg provides the packaging, the hardening and not an open resolver defaults, first-boot HTTPS and per-instance credentials, and 24/7 support.
cloudimg is not affiliated with or endorsed by Pi-hole LLC. Pi-hole is a registered trademark of Pi-hole LLC and is used here only to identify the software.
For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.