Deployment guide

Pi-hole 6.5

Overview

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.

This guide covers launching and verifying Pi-hole 6.5 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. It 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 listing's guide hub describes the other DNS and time servers in the listing.

The image installs the pinned Pi-hole 6.5 release set: Core v6.5, Web v6.6.1 and FTL v6.8. All three were published on 2026-10-17 and carry 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 you get:

  • 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 does not start until the password exists.
  • The StevenBlack unified hosts list loaded into the blocklist (gravity), so ads and trackers are blocked from the first query.
  • A policy that keeps the instance from being an open resolver. DNS is answered for loopback and private address ranges only, enforced by the kernel firewall.
  • pi-hole-set-hostname, a helper that serves 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, and 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.service refuses to start until first boot has created the password and the TLS certificate. It also requires the DNS access list and the metadata block described 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 that a request asks for.
  • Not an open resolver. Pi-hole answers DNS queries only 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, ::1 and fc00::/7. An nftables rule drops queries from any other address 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.service carries a systemd IPAddressDeny rule, and an nftables rule blocks everything that runs as the pihole user (the weekly blocklist update and the update checker). 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: Pi-hole 6.5 on Ubuntu 24.04

Operating system: Ubuntu 24.04 LTS

SSH user: ubuntu

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.
  • 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.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  • 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 5.
  • Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run the following 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. Replace the VPC, subnet, key pair and CIDR ranges with your own values:

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, because 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 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. Read it with:

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

3

Confirm the services are running

Confirm that first boot, the DNS access list, the metadata block and Pi-hole itself are active, and that the installed release set is Core v6.5, Web v6.6.1 and FTL v6.8:

systemctl is-active pi-hole-firstboot.service sac-pihole-dns-acl.service sac-pihole-imds-deny.service pihole-FTL.service pihole -v cat /etc/sac/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

List the 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

These are the only ports the instance listens on. 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).

The instance boots clean, with no failed unit. Pi-hole's three logs ship present and empty (owned by pihole), and every rule in /etc/logrotate.d/pihole carries missingok, so the daily log rotation never fails.

The fwupd-refresh timer is masked, because EC2 instances have no firmware that the Linux Vendor Firmware Service can update. Verify the system state, the timer and the logs:

systemctl is-system-running
systemctl is-enabled fwupd-refresh.timer
sudo ls -l /var/log/pihole | tail -n +2 | awk '{print $1, $3":"$4, $NF}' grep -c missingok /etc/logrotate.d/pihole
running masked -rw-r----- pihole:pihole FTL.log -rw-r----- pihole:pihole pihole.log -rw-r----- pihole:pihole webserver.log 2
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 you once. Accept the warning, 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. Check both:

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, and the instance's own password returns a valid session. The script reads the password from the root-only file and passes it 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.

5

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 / VPN CIDRs, for DNS
  • TCP 53 from your VPC / VPN CIDRs, for DNS (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 (shown in the credentials file in Step 2). You can do this 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 the query was forwarded upstream, answered from cache or blocked.

6

Confirm it is not an open resolver

Pi-hole listens on every interface, so clients in any private range of your VPC, peered VPCs and VPNs can use it. The kernel enforces the client restriction. The nftables table sac_pihole_dns_acl drops every DNS query whose source is not in the two allow sets, and it counts what it drops:

sudo pihole-FTL --config dns.listeningMode
sudo nft list table inet sac_pihole_dns_acl
ALL table inet sac_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 "sac-dns-acl-v4" meta l4proto { tcp, udp } th dport 53 ip6 saddr != @allow6 counter packets 0 bytes 0 drop comment "sac-dns-acl-v6" } }

You can prove the policy from the instance itself. The script adds a temporary public test address (192.0.2.53, from the TEST-NET-1 documentation range) to the loopback interface. It sends a query from that address to the private IP over UDP and over TCP, sends the same query from the private IP itself, and then removes the address again. The public source gets no answer and the drop counter rises, while 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 sac_pihole_dns_acl input | grep -o 'counter packets [0-9]* bytes [0-9]* drop comment "sac-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 "sac-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/sac/pihole-dns-acl.nft, then reload the list. Keep the security group restricted to the same ranges.

sudo nano /etc/sac/pihole-dns-acl.nft
sudo systemctl restart sac-pihole-dns-acl.service pihole-FTL.service

Never add 0.0.0.0/0 or ::/0.

7

Blocklists

The blocklist (gravity) ships loaded with the StevenBlack unified hosts list. Show the list and the number of blocked domains:

sudo sqlite3 /etc/pihole/gravity.db 'SELECT address FROM adlist; SELECT COUNT(*) FROM gravity;'
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts 71493

To add more lists, open Lists in the dashboard, then choose Tools > Update Gravity (or run sudo pihole -g). Pi-hole also refreshes every list once a week. To allow or deny individual domains, use Domains.

8

HTTPS certificate and host name

First boot creates the certificate on your instance for the instance's public DNS name, public IP, private IP and localhost. Only the pihole user that runs Pi-hole can read the key file. Inspect the certificate and file permissions:

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

9

Metadata access is blocked for Pi-hole

pihole-FTL.service carries a systemd IPAddressDeny rule for the instance metadata addresses. The nftables table sac_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 sac_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 sac_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 with sudo pihole setpassword, then update or delete the file.
  • Configuration. Settings you change in the dashboard are saved to /etc/pihole/pihole.toml. Change single settings from the shell with sudo pihole-FTL --config <key> <value>.
  • Upgrades. Run sudo pihole -up to move to a newer Pi-hole release when one is published. The image ships Pi-hole's own Core and Web checkouts (/etc/.pihole and /var/www/html/admin, at the v6.5 and v6.6.1 release tags, pointing at the official GitHub repositories), so pihole -up and the version check in the dashboard work. Take a snapshot first.
  • Security patches. unattended-upgrades remains enabled, so the OS keeps receiving security updates.
  • Logs. Pi-hole writes to /var/log/pihole/ (pihole.log rotates daily, FTL.log and webserver.log weekly). /etc/tmpfiles.d/sac-pihole-logs.conf recreates the three files at boot if they are missing. The missingok rules survive pihole -up, which leaves a customised logrotate file untouched.

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. Add only networks you trust to /etc/sac/pihole-dns-acl.nft, never 0.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. SecureAnyCloud provides the packaging, the hardening and open-resolver protection defaults, first-boot HTTPS and per-instance credentials, and 24/7 support.

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

Deploy on AWS

Launch Pi-hole 6.5 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