Deployment guide

CoreDNS 1.14.7

Overview

This guide shows how to launch and verify CoreDNS 1.14.7 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. The image is one software version of the SecureAnyCloud Hardened Pi-hole, AdGuard Home & DNS Server AMIs listing on AWS Marketplace. Select it as the software version when you subscribe and launch. The listing's guide hub describes the other DNS servers in the listing.

CoreDNS is a fast, flexible DNS server and a graduated Cloud Native Computing Foundation project, best known as the default DNS server in Kubernetes. It is a single Go binary whose whole behaviour is defined by a plain-text Corefile. Each server block chains plugins (file for authoritative zones, forward and cache for recursion, tls for DNS-over-TLS, acl for access control), so you can compose exactly the DNS pipeline you need.

The CoreDNS binary is built from the upstream CoreDNS 1.14.7 source (tag v1.14.7) with a security-patched Go toolchain (go1.26.10) and security-patched Go dependencies; the build record and its govulncheck reports are in /usr/share/doc/coredns-sac/. Unattended security upgrades stay on for the operating system.

What you get:

  • CoreDNS 1.14.7 (coredns.service) at /opt/coredns/coredns, configured by /etc/coredns/Corefile, running as the unprivileged coredns user
  • An authoritative demo zone, sac.internal, whose records point at your instance's private IP
  • A caching forwarder to the Amazon VPC resolver (169.254.169.253), which also answers your Route 53 private hosted zones
  • DNS-over-TLS (DoT) on port 853, using a key and self-signed certificate created on your instance's first boot
  • Not an open resolver. Every server block answers only the local host and private address ranges, and every other source gets REFUSED
  • Health (127.0.0.1:8080) and Prometheus metrics (127.0.0.1:9153) on loopback only
  • A self-test, /opt/coredns/coredns-selftest.sh

Default user: ubuntu; Architecture: x86_64; Root volume: 8 GiB gp3

Security posture

  • Not an open resolver. CoreDNS listens on every interface so clients in your VPC can reach it, but every server block in the Corefile carries the same acl: queries are answered only from 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, fd00::/8 and fe80::/10, and refused from every other source, on plain DNS and on DNS-over-TLS. 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) and DNS-over-TLS (853/tcp) stay closed until you open them to your own VPC or VPN ranges. Clients use the instance's private IP.
  • Per-instance DoT key. The DNS-over-TLS private key and certificate are created on each instance's first boot. No key ships in the image, so no two instances share one.
  • Fails closed. CoreDNS will not start until first boot has finished and the key exists. If first boot cannot finish, CoreDNS stays stopped and first boot tries again on the next boot.
  • No credential to steal. CoreDNS has no login and no remote management interface. Health and metrics listen on 127.0.0.1 only.
  • Least privilege. coredns.service runs as the coredns user with only the capability to bind ports below 1024, a read-only view of the system and of /etc/coredns, and no access to home directories.
  • No metadata access for the DNS service. CoreDNS forwards queries for remote clients, so coredns.service and its first-boot unit are blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). The Amazon VPC resolver (169.254.169.253) and Amazon Time Sync (169.254.169.123) 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 for the DNS server, and the CIDR ranges of the clients that will use it

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

Connecting to your instance

Version Operating system SSH user

CoreDNS 1.14.7 on Ubuntu 24.04 Ubuntu 24.04 LTS 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 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 CoreDNS 1.14.7 on Ubuntu 24.04, then choose your Region.
  • Select an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  • Configure the security group. The listing's default group allows SSH (22) only, so restrict it to your own IP address. You open the DNS ports to your clients in Step 4.
  • Choose your VPC and subnet and leave the metadata defaults (the image already requires IMDSv2). Then choose Launch.

Option B: AWS CLI

Run the following script on your own workstation. It creates a security group that allows SSH from your management address and DNS from your VPC, and 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 coredns-server \
  --description "CoreDNS server" --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 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 853 --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=coredns-server}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required.

2

Connect and read the instance details

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 renders the demo zone with the instance's private IP, creates the instance's DNS-over-TLS key and certificate, and writes /root/coredns-info.txt with the private IP, the certificate fingerprint and the public key pin. None of these values are secret. The private key itself stays in /etc/coredns/tls/, readable only by root and the coredns group.

sudo cat /root/coredns-info.txt
# CoreDNS 1.14.7 on Ubuntu 24.04 LTS (sac AWS Marketplace image) - THIS instance.
# CoreDNS has no login. The DNS-over-TLS private key was generated on this instance's # first boot and exists nowhere else. coredns.version=1.14.7 coredns.private_ip=172.31.87.132 coredns.dns=172.31.87.132:53 (udp+tcp) coredns.dot=172.31.87.132:853/tcp (DNS-over-TLS) coredns.dot_cert=/etc/coredns/tls/server.crt coredns.dot_cert_sha256=0A:4B:3C:00:35:47:03:14:59:55:A2:E6:2F:61:D8:FE:12:F7:8D:7E:27:67:29:87:FC:CD:3E:30:1D:DA:18:AA coredns.dot_pubkey_pin_sha256=U+dFaqfSrIOlwEiQzu40DlInfob2ddInxgugymFFl3s= coredns.demo_zone=sac.internal (demo.sac.internal -> 172.31.87.132) coredns.upstream=169.254.169.253 (Amazon VPC resolver) coredns.corefile=/etc/coredns/Corefile # # NETWORK ACCESS: the listing security group opens SSH only. To serve clients, add inbound # rules for 53 (udp+tcp) and 853/tcp ONLY from your own VPC / VPN CIDRs - never 0.0.0.0/0 - # and point clients at the PRIVATE IP above. Queries are answered ONLY for loopback and the # private ranges (RFC 1918, 100.64.0.0/10, fd00::/8, fe80::/10); every other source gets # REFUSED. To admit another range of your own, add it to the 'allow net' line of EVERY block # in /etc/coredns/Corefile, then: # sudo systemctl restart coredns (NEVER allow 0.0.0.0/0 or ::/0) # Health (127.0.0.1:8080) and metrics (127.0.0.1:9153) are loopback-only; reach them over SSH. # # Checks: dig @127.0.0.1 A demo.sac.internal # dig +tls -p 853 @127.0.0.1 A demo.sac.internal # sudo /opt/coredns/coredns-selftest.sh # User guide: https://secureanycloud.com/stacks/coredns-1-14-7/
3

Confirm CoreDNS is running

coredns.service and its first-boot unit are active, and the binary reports 1.14.7. The self-test checks the demo zone over UDP, TCP and DNS-over-TLS, the per-instance certificate, recursion, the access control lists in every server block, the loopback-only health and metrics endpoints, and the exact set of listening ports.

systemctl is-active coredns.service coredns-firstboot.service /opt/coredns/coredns -version sudo /opt/coredns/coredns-selftest.sh
active active CoreDNS-1.14.7 linux/amd64, go1.26.10, 427fc80 [coredns-selftest] demo.sac.internal -> 172.31.87.132 over UDP, TCP and DoT (authoritative) [coredns-selftest] DoT certificate is the per-instance one (sha256 0A:4B:3C:00:35:47:03:14:59:55:A2:E6:2F:61:D8:FE:12:F7:8D:7E:27:67:29:87:FC:CD:3E:30:1D:DA:18:AA) [coredns-selftest] recursion answered for loopback + 172.31.87.132; acl present in every block [coredns-selftest] listening set OK (tcp: 22 53 853 ; udp: 53 68 ); 8080/9153 loopback-only COREDNS_SELFTEST_OK
4

Open the DNS server to your clients

The listing's security group allows SSH only. 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)

TCP 853 your VPC / VPN CIDRs DNS-over-TLS

In the EC2 console, choose the instance, open Security, choose the security group, and then choose Edit inbound rules. Point clients at the instance's private IP (coredns.private_ip in Step 2). These are the only ports the instance listens on besides SSH:

sudo ss -Hltnu | awk '{print $1, $5}' | sort -u
tcp *:53 tcp *:853 tcp 0.0.0.0:22 tcp 127.0.0.1:8080 tcp 127.0.0.1:9153 tcp [::]:22 udp *:53 udp 127.0.0.1:323 udp 172.31.87.132%ens5:68 udp [::1]:323

Ports 8080 (health) and 9153 (metrics) listen on 127.0.0.1 only. Port 323 is chrony's local control socket, and port 68 is the DHCP client.

From a client inside your VPC, test the server against its private IP:

dig @<private-ip> demo.sac.internal A

To make a Linux client use the server permanently, set its private IP as the DNS server on the client's network interface (with netplan or systemd-resolved), or in a DHCP options set for the VPC.

5

Query the authoritative demo zone

The sac.internal zone is served from /etc/coredns/zones/db.sac.internal, which first boot rendered from a template with the instance's private IP. The aa (Authoritative Answer) flag shows CoreDNS answered from its own zone rather than by recursion. The same answer comes back over TCP:

dig @127.0.0.1 demo.sac.internal A dig +tcp +short @127.0.0.1 demo.sac.internal A dig +short @127.0.0.1 sac.internal SOA
; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> @127.0.0.1 demo.sac.internal A ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27092 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ; COOKIE: 9039a574666093f3 (echoed) ;; QUESTION SECTION: ;demo.sac.internal. IN A ;; ANSWER SECTION: demo.sac.internal. 3600 IN A 172.31.87.132 ;; AUTHORITY SECTION: sac.internal. 3600 IN NS ns1.sac.internal. ;; Query time: 0 msec ;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP) ;; WHEN: Tue Oct 20 17:54:06 UTC 2026 ;; MSG SIZE rcvd: 153 172.31.87.132 ns1.sac.internal. hostmaster.sac.internal. 2026102017 7200 3600 1209600 3600
6

Resolve names through the VPC resolver

Names outside your own zones are forwarded to the Amazon VPC resolver at 169.254.169.253 and cached for 30 seconds. The VPC resolver also answers your Route 53 private hosted zones, and it works from private subnets with no internet route.

dig @127.0.0.1 aws.amazon.com A | grep -E "status|flags|SERVER"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39144 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1 ; EDNS: version: 0, flags:; udp: 1232 ;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
7

Use DNS-over-TLS

DNS-over-TLS encrypts queries between your clients and the server on port 853. The certificate is self-signed and was created on this instance's first boot, with the instance's private IP, 127.0.0.1, localhost, ns1.sac.internal and the instance hostname in its Subject Alternative Names. Check a query over TLS and the certificate the server presents:

dig +tls -p 853 @127.0.0.1 demo.sac.internal A | grep -E "status|flags|SERVER|^demo" openssl s_client -connect 127.0.0.1:853 -servername localhost </dev/null 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName -fingerprint -sha256
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43981 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1 ; EDNS: version: 0, flags:; udp: 1232 demo.sac.internal. 3600 IN A 172.31.87.132 ;; SERVER: 127.0.0.1#853(127.0.0.1) (TLS) subject=CN = 172.31.87.132, O = sac CoreDNS DNS-over-TLS (per-instance) X509v3 Subject Alternative Name: IP Address:172.31.87.132, IP Address:127.0.0.1, DNS:localhost, DNS:ns1.sac.internal, DNS:ip-172-31-87-132 sha256 Fingerprint=0A:4B:3C:00:35:47:03:14:59:55:A2:E6:2F:61:D8:FE:12:F7:8D:7E:27:67:29:87:FC:CD:3E:30:1D:DA:18:AA

The key is readable only by root and the coredns group. The certificate is public:

sudo openssl x509 -in /etc/coredns/tls/server.crt -noout -subject -dates sudo stat -c '%a %U:%G %n' /etc/coredns/tls/server.key /etc/coredns/tls/server.crt
subject=CN = 172.31.87.132, O = sac CoreDNS DNS-over-TLS (per-instance) notBefore=Oct 20 17:53:31 2026 GMT notAfter=Oct 17 17:53:31 2036 GMT 640 root:coredns /etc/coredns/tls/server.key 644 root:coredns /etc/coredns/tls/server.crt

Because the certificate is self-signed, configure DoT clients to trust it explicitly:

  • Pin the public key. Clients such as Stubby accept an SPKI pin. Use coredns.dot_pubkey_pin_sha256 from /root/coredns-info.txt, for example tls_pubkey_pinset: [{digest: "sha256", value: <pin>}] with tls_auth_name set to the private IP.
  • Or trust the certificate. Copy /etc/coredns/tls/server.crt to the client and add it to the client's trust store, or pass it as the CA file, for example kdig +tls-ca=server.crt @<private-ip>.

To use a certificate from your own certificate authority instead, replace server.key and server.crt in /etc/coredns/tls/ (keep the key 0640 root:coredns) and run sudo systemctl restart coredns.

8

Confirm it is not an open resolver

Each of the four server blocks in /etc/coredns/Corefile (the demo zone and the forwarder, each on 53 and on DNS-over-TLS 853) carries the same acl: allow the local host and private ranges, block everything else.

sudo grep -nE '^[a-z.:/]+[0-9]* \{|^ +allow net|block$' /etc/coredns/Corefile
18:sac.internal:53 { 20: allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 21: block 27:tls://sac.internal:853 { 30: allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 31: block 41:.:53 { 43: allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 44: block 53:tls://.:853 { 56: allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 57: block

You can verify the policy from the instance itself. The following script adds a temporary public test address (192.0.2.53, from the TEST-NET-1 documentation range) to the loopback interface, sends queries from it over DNS and over DNS-over-TLS, sends the same query from 127.0.0.1, and removes the address again. The public source is refused, for both the forwarder and the demo zone, and the local source is answered:

sudo ip addr add 192.0.2.53/32 dev lo dig -b 192.0.2.53 @192.0.2.53 aws.amazon.com A | grep -oE "status: [A-Z]+" dig +tls -p 853 -b 192.0.2.53 @192.0.2.53 aws.amazon.com A | grep -oE "status: [A-Z]+" dig -b 192.0.2.53 @192.0.2.53 demo.sac.internal A | grep -oE "status: [A-Z]+" dig -b 127.0.0.1 @127.0.0.1 aws.amazon.com A | grep -oE "status: [A-Z]+"
sudo ip addr del 192.0.2.53/32 dev lo
status: REFUSED status: REFUSED status: REFUSED status: NOERROR
9

Serve your own zone

To serve a zone of your own, copy the demo zone's template as a starting point and replace the names and addresses with yours:

sudo cp /etc/coredns/zones/db.sac.internal.template /etc/coredns/zones/db.<your-domain>
sudo nano /etc/coredns/zones/db.<your-domain>

Use a real serial number (for example 2026102001) in place of __SERIAL__. Then add a server block for the zone to /etc/coredns/Corefile, with the same acl as the other blocks:

corp.example:53 { acl { allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 block } file /etc/coredns/zones/db.corp.example log errors }

Keep the zone file readable by CoreDNS (sudo chown root:coredns and sudo chmod 0640), then apply the change and query the new zone:

sudo systemctl restart coredns dig @127.0.0.1 www.<your-domain> A

To forward one domain to a different resolver, for example an on-premises Active Directory DNS server reached over your VPN, add a block such as:

corp.internal:53 { acl { allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 block } forward . 10.20.0.10 10.20.0.11 cache 30 errors }

The demo zone sac.internal can be removed when you no longer need it. Delete its two server blocks from the Corefile and restart CoreDNS.

10

Allow another of your own networks (optional)

Queries are answered for the local host and the private ranges above. If some of your clients use other addresses, for example a VPC's public IPv6 range or a VPN that uses public addresses, add that range to the allow net line of every server block in /etc/coredns/Corefile:

allow net 127.0.0.0/8 ::1/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fd00::/8 fe80::/10 203.0.113.0/24

Replace 203.0.113.0/24 with your own range. Never add 0.0.0.0/0 or ::/0, and never remove an acl: either turns the server into an open resolver. Apply the change and re-run the self-test:

sudo systemctl restart coredns
sudo /opt/coredns/coredns-selftest.sh

Remember to allow the same range in the security group (Step 4).

11

Health, metrics and metadata access

Health and Prometheus metrics listen on 127.0.0.1 only. Read them over SSH, or scrape them with an agent running on the instance:

curl -s http://127.0.0.1:8080/health; echo
curl -s http://127.0.0.1:9153/metrics | grep -E "^coredns_(build_info|dns_requests_total)" | head -5
OK coredns_build_info{goversion="go1.26.10",revision="427fc80",version="1.14.7"} 1 coredns_dns_requests_total{family="1",proto="tcp",server="dns://:53",type="A",view="",zone="."} 2 coredns_dns_requests_total{family="1",proto="udp",server="dns://:53",type="A",view="",zone="."} 8

coredns.service and coredns-firstboot.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 the Amazon VPC resolver at 169.254.169.253 and the Amazon Time Sync Service at 169.254.169.123 keep working, as chrony shows:

systemctl show -p IPAddressDeny coredns.service coredns-firstboot.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 ^* 169.254.169.123 3 4 377 6 +5904ns[+4640ns] +/- 420us

CoreDNS logs every query and error to the journal: sudo journalctl -u coredns -f.

12

Security recommendations

  • Keep the security group tight. Open 53 and 853 only to the networks that need the server, and SSH only to your management address.
  • Keep the acl scoped. Only widen it to networks you trust (Step 10), never to 0.0.0.0/0 or ::/0. The acl is a second layer of defence behind the security group.
  • Prefer DNS-over-TLS for clients that support it, so queries cannot be read or altered on the network.
  • Keep the OS patched. Unattended security upgrades are on. To move to a newer CoreDNS release, launch the newer version from the listing when it is published.
  • Monitor the metrics. The Prometheus endpoint reports request counts, response codes, cache hits and forwarding latency for your monitoring system.

Support and licensing

CoreDNS is a Cloud Native Computing Foundation project distributed under the Apache License 2.0. This image ships CoreDNS built from the unmodified upstream 1.14.7 source, with only its Go toolchain and Go dependencies raised to security-patched releases. SecureAnyCloud provides the packaging, the hardening, the not-an-open-resolver defaults, per-instance DNS-over-TLS and 24/7 support.

SecureAnyCloud is not affiliated with or endorsed by the CoreDNS project or the Cloud Native Computing Foundation. CoreDNS is a mark of its owner 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 CoreDNS 1.14.7 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