Networking AWS

CoreDNS 1.14.7 on Ubuntu 24.04 on AWS User Guide

| Product: CoreDNS 1.14.7 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying CoreDNS 1.14.7 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 servers in the listing are described on the listing's guide hub.

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 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-cloudimg/. Unattended security upgrades stay on for the operating system.

What is on the image:

  • 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, cloudimg.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
  • A not an open resolver policy: 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

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. 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

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. 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.
  2. Once the subscription is active, choose Launch your software.
  3. 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.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group allows SSH (22) only. Restrict it to your own IP address. You will open the DNS ports to your clients in Step 4.
  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 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 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.

Step 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 (cloudimg 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=cloudimg.internal (demo.cloudimg.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.cloudimg.internal
#          dig +tls -p 853 @127.0.0.1 A demo.cloudimg.internal
#          sudo /opt/coredns/coredns-selftest.sh
# User guide: https://www.cloudimg.co.uk/guides/coredns-1-14-7-on-ubuntu-24-04-aws/

Step 3: Confirm CoreDNS is running

coredns.service and its first-boot unit are active, the binary reports 1.14.7, and 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.cloudimg.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

coredns.service active, coredns -version reporting 1.14.7, and the self-test confirming the demo zone over UDP, TCP and DNS-over-TLS, the per-instance certificate, recursion, the access control lists and the exact listening set

Step 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, then 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 68 is the DHCP client.

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

dig @<private-ip> demo.cloudimg.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.

Step 5: Query the authoritative demo zone

The cloudimg.internal zone is served from /etc/coredns/zones/db.cloudimg.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.cloudimg.internal A
dig +tcp +short @127.0.0.1 demo.cloudimg.internal A
dig +short @127.0.0.1 cloudimg.internal SOA
; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> @127.0.0.1 demo.cloudimg.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.cloudimg.internal.        IN  A

;; ANSWER SECTION:
demo.cloudimg.internal. 3600    IN  A   172.31.87.132

;; AUTHORITY SECTION:
cloudimg.internal.  3600    IN  NS  ns1.cloudimg.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.cloudimg.internal. hostmaster.cloudimg.internal. 2026102017 7200 3600 1209600 3600

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

Step 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.cloudimg.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.cloudimg.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.cloudimg.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 = cloudimg 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.cloudimg.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 = cloudimg 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

dig over DNS-over-TLS answering the demo zone on port 853, and the per-instance certificate with the private IP in its Subject Alternative Names, its fingerprint and the key readable only by root and the coredns group

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.

Step 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:cloudimg.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://cloudimg.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 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 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 the forwarder and for the demo zone; the local one 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.cloudimg.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

The acl in all four Corefile server blocks allowing only loopback and private ranges, and queries from the public test address 192.0.2.53 refused over DNS and DNS-over-TLS while the same query from 127.0.0.1 is answered

Step 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.cloudimg.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 cloudimg.internal can be removed once you no longer need it: delete its two server blocks from the Corefile and restart CoreDNS.

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

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

Step 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. cloudimg provides the packaging, the hardening and not an open resolver defaults, per-instance DNS-over-TLS, and 24/7 support.

cloudimg 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 support@cloudimg.co.uk or visit cloudimg support.