Networking AWS

dnscrypt-proxy 2.1.20 on AWS User Guide

| Product: dnscrypt-proxy 2.1.20 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying dnscrypt-proxy 2.1.20 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.

dnscrypt-proxy is an open-source encrypting DNS proxy. Your clients send it ordinary DNS queries on port 53, and it forwards every one of them over an encrypted protocol (DNSCrypt v2 or DNS-over-HTTPS) to public resolvers, so the networks between your VPC and the resolver cannot read or alter the names you look up. It picks the fastest resolvers automatically from a signed public resolver list and caches answers in memory.

On this image dnscrypt-proxy is a ready-to-use resolver for your VPC: point your instances (or a DHCP options set) at its private address and their lookups leave the VPC encrypted. It answers only clients inside your VPC's address range, which it reads from the instance metadata on first boot, so it can never be used as an open resolver.

dnscrypt-proxy 2.1.20 is installed from the official signed release: cloudimg checks the upstream minisign signature and pins the release and binary checksums. The build record is in /usr/share/doc/dnscrypt-proxy/BUILD-PROVENANCE.txt.

What is on the image:

  • dnscrypt-proxy 2.1.20 (dnscrypt-proxy.service) running as the unprivileged dnscrypt-proxy user, allowed only to bind port 53
  • Port 53 (UDP and TCP) for your VPC only: an nftables allow-list (cloudimg-dnscrypt-acl.service) written on first boot from your VPC's CIDR blocks; every other source is dropped
  • Encrypted upstreams only: DNSCrypt v2 and DNS-over-HTTPS resolvers that validate DNSSEC, keep no logs and do not filter
  • AWS names keep working: *.internal host names and amazonaws.com service endpoints (including VPC endpoints with private DNS) are answered by the Amazon-provided VPC resolver
  • Query logging off and no blocklist: dnscrypt-proxy keeps no record of the names your clients look up
  • The instance itself resolves through its own encrypted resolver (/etc/resolv.conf points at 127.0.0.1)
  • Helpers: dnscrypt-proxy-selftest and dnscrypt-proxy-acl (the client allow-list)
  • No metadata access: dnscrypt-proxy cannot reach the instance metadata service

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

Security posture

  • Not an open resolver. dnscrypt-proxy listens on port 53 on every interface, but an nftables table drops every port-53 packet whose source is not on /etc/dnscrypt-proxy/allowed-clients.txt. First boot fills that file with your VPC's IPv4 (and IPv6, if any) CIDR blocks; loopback is always allowed. The listing's default security group opens SSH only, so port 53 is closed until you open it to your own networks.
  • Encrypted and validated answers. Every query from your clients leaves the instance encrypted. Only resolvers that validate DNSSEC, keep no logs and do not filter are used, so a name with a broken DNSSEC signature fails instead of being answered.
  • No query log. Query logging and the suspicious-name log are off, the built-in monitoring dashboard is off, and no blocklist ships. The resolver list cache starts empty and is downloaded and signature-checked on first boot.
  • No default credential. dnscrypt-proxy has no login. Its optional monitoring dashboard, which upstream ships with the password changeme, stays disabled; on this image it has a 32-character password generated on your instance's first boot instead, so turning it on never exposes a default login. The start-up check refuses to start dnscrypt-proxy with the upstream default, an empty or a placeholder password.
  • No SSRF to instance credentials. dnscrypt-proxy.service carries an IPAddressDeny rule for the metadata addresses (169.254.169.254, fd00:ec2::254, 168.63.129.16), and the start-up check refuses a forwarding rule that points at the metadata service. The Amazon VPC resolver (169.254.169.253) stays reachable, because dnscrypt-proxy uses it to resolve the encrypted resolvers' own host names and to answer AWS names. The AMI is registered IMDSv2 only.
  • Least privilege. dnscrypt-proxy runs as the dnscrypt-proxy system user with one capability (binding port 53), a read-only system and a private /tmp; it can write only its resolver-list cache.
  • Fails closed. dnscrypt-proxy starts only after first boot has written this instance's allow-list and a ready marker, and only while the allow-list is loaded. If first boot cannot finish (for example because the instance has no outbound HTTPS), dnscrypt-proxy stays stopped, the instance keeps using the VPC resolver, and first boot tries again on the next boot.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.
  • Security notes. govulncheck finds no reachable vulnerability in dnscrypt-proxy 2.1.20. On the stripped release binary it reports GO-2026-5932 (the unmaintained golang.org/x/crypto/openpgp package, which has no fix) at module level only: dnscrypt-proxy does not import that package and the binary contains none of its code.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. A subnet with outbound HTTPS to the internet (a public subnet, or a private subnet with a NAT gateway): dnscrypt-proxy reaches its encrypted resolvers over the internet

Recommended instance type: m5.large. dnscrypt-proxy is light; a small instance serves a large VPC.

Connecting to your instance

Version Operating system SSH user
dnscrypt-proxy 2.1.20 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access 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 dnscrypt-proxy 2.1.20 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 SSH to your own IP address. You open DNS to your VPC in Step 6.
  6. Choose the VPC whose clients will use the resolver and a subnet with outbound HTTPS, 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 the 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 dnscrypt-proxy \
  --description "dnscrypt-proxy encrypted DNS resolver" --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 <your-vpc-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 53 --cidr <your-vpc-cidr>
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=dnscrypt-proxy}]'

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 or two of launch. It writes the client allow-list from your VPC's CIDR blocks, generates the dashboard password, downloads and checks the signed resolver list, starts dnscrypt-proxy and points the instance's own DNS at it. The details are in /root/dnscrypt-proxy-credentials.txt, readable only by root. This command shows everything except the dashboard password:

sudo grep -v '^MONITORING_UI_PASSWORD=' /root/dnscrypt-proxy-credentials.txt
# dnscrypt-proxy 2.1.20 - generated on first boot by dnscrypt-proxy-firstboot.service.
# The monitoring dashboard ships DISABLED. Its basic-auth password below was generated on this instance and no
# other instance shares it. To use the dashboard, enable it in /etc/dnscrypt-proxy/dnscrypt-proxy.toml and reach
# it through an SSH tunnel (see the guide). Keep this file private.

MONITORING_UI_URL=http://127.0.0.1:8080/
MONITORING_UI_USER=admin

DNS_LISTEN=[::]:53 (UDP and TCP)
DNS_PRIVATE_ADDRESS=172.31.92.201
ALLOWED_CLIENTS=172.31.0.0/16 (+ loopback; /etc/dnscrypt-proxy/allowed-clients.txt)
INSTANCE_HOSTNAME=ip-172-31-92-201.ec2.internal
CONFIG=/etc/dnscrypt-proxy/dnscrypt-proxy.toml
SELF_TEST=sudo dnscrypt-proxy-selftest
GUIDE=https://www.cloudimg.co.uk/guides/dnscrypt-proxy-2-1-20-on-ubuntu-24-04-aws/

DNS_PRIVATE_ADDRESS is the address your clients use. Your values differ.

Step 3: Confirm dnscrypt-proxy is running

dnscrypt-proxy and the allow-list are active and first boot has completed. dnscrypt-proxy owns port 53 over UDP and TCP on every interface (* is IPv4 and IPv6):

systemctl is-active dnscrypt-proxy.service cloudimg-dnscrypt-acl.service
dnscrypt-proxy -version
sudo ss -Htulnp 'sport = :53' | awk '{print $1, $5}'
active
active
2.1.20
udp *:53
tcp *:53

Step 4: Run the self-test

The shipped self-test checks the resolver end to end from the instance itself: dnscrypt-proxy runs as its own user, the allow-list is loaded, a name resolves over UDP and TCP, encrypted upstreams are in use, the instance resolves through its own resolver, query logging is off, and the instance metadata service is unreachable from inside dnscrypt-proxy while the Amazon resolver answers there:

sudo dnscrypt-proxy-selftest
ok: dnscrypt-proxy is active (pid 2925, user dnscrypt-proxy)
ok: port-53 allow-list loaded: 172.31.0.0/16 (+ loopback)
ok: example.com resolves over UDP and TCP on 127.0.0.1:53 (172.66.147.243)
ok: encrypted upstreams in use: 313 ] OK (DNSCrypt); 155 ] OK (DoH);
ok: this instance resolves through its own encrypted resolver (/etc/resolv.conf -> 127.0.0.1)
ok: query logging and NX logging are off
ok: metadata: 169.254.169.254 is unreachable from inside dnscrypt-proxy; the Amazon resolver answers there
SELFTEST_OK

Step 5: Query the resolver

Names resolve over UDP and TCP. A name whose DNSSEC signature is deliberately broken (dnssec-failed.org) is refused with SERVFAIL, because only DNSSEC-validating resolvers are used. The log shows which encrypted resolver answered fastest at start-up:

dig +short @127.0.0.1 example.com A
dig +tcp +short @127.0.0.1 example.com A
dig @127.0.0.1 dnssec-failed.org A | grep -o 'status: [A-Z]*'
sudo journalctl -u dnscrypt-proxy.service --no-pager | grep -a 'lowest initial latency' | tail -1 | sed 's/^.*\]: //'
172.66.147.243
104.20.23.154
172.66.147.243
104.20.23.154
status: SERVFAIL
[2026-10-27 00:15:46] [NOTICE] Server with the lowest initial latency: quad9-dnscrypt-ip4-nofilter-pri (rtt: 0ms), live servers: 336

Step 6: Serve your VPC

The allow-list already contains your VPC's CIDR blocks; the security group decides whether DNS traffic reaches the instance at all. Open UDP and TCP 53 to your VPC (or to the subnets of your clients) in the instance's security group, never to 0.0.0.0/0:

aws ec2 authorize-security-group-ingress --group-id <your-sg> --protocol udp --port 53 --cidr <your-vpc-cidr>
aws ec2 authorize-security-group-ingress --group-id <your-sg> --protocol tcp --port 53 --cidr <your-vpc-cidr>

Then, from any instance in the VPC, query the resolver's private address (DNS_PRIVATE_ADDRESS from Step 2):

dig +short @<instance-private-ip> example.com A

To make every instance in the VPC use it, create a DHCP options set with domain-name-servers set to the resolver's private address (two resolvers in different Availability Zones for resilience) and associate it with the VPC, or set the address in each client's resolver configuration. Keep the Amazon resolver for anything that needs it: this image already sends AWS names there (Step 8).

A client outside the allow-list gets no answer at all, even if the security group lets its packets in: its queries are dropped and counted. The current list and the drop counter:

sudo grep -v '^#' /etc/dnscrypt-proxy/allowed-clients.txt
sudo dnscrypt-proxy-acl show
172.31.0.0/16
table inet cloudimg_dns {
    set allow4 {
        type ipv4_addr
        flags interval
        elements = { 127.0.0.0/8, 172.31.0.0/16 }
    }

    set allow6 {
        type ipv6_addr
        flags interval
        elements = { ::1 }
    }

    chain input {
        type filter hook input priority filter - 10; policy accept;
        iifname "lo" accept
        meta l4proto { tcp, udp } th dport 53 ip saddr @allow4 accept
        meta l4proto { tcp, udp } th dport 53 ip6 saddr @allow6 accept
        meta l4proto { tcp, udp } th dport 53 counter packets 2 bytes 160 drop
    }
}

Step 7: Allow more client networks

Clients outside the VPC range (a VPN pool, a peered VPC, an on-premises network over Direct Connect) need their range on the allow-list. Add one CIDR per line and apply it; the change is atomic and needs no restart:

echo '10.8.0.0/24' | sudo tee -a /etc/dnscrypt-proxy/allowed-clients.txt
sudo dnscrypt-proxy-acl apply

dnscrypt-proxy-acl refuses 0.0.0.0/0 and ::/0, which would turn the instance into an open resolver. Remember to allow the same range in the security group.

Step 8: AWS names and private hosted zones

EC2 host names (*.internal) and AWS service endpoints (amazonaws.com) are forwarded to the Amazon VPC resolver, so instance names, Systems Manager and VPC interface endpoints with private DNS resolve exactly as they do without dnscrypt-proxy. The rules are in /etc/dnscrypt-proxy/forwarding-rules.txt; this resolves the instance's own private name through dnscrypt-proxy:

sudo grep -v '^#' /etc/dnscrypt-proxy/forwarding-rules.txt
TOKEN=$(curl -s -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token)
LH=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-hostname)
echo "$LH"; dig +short @127.0.0.1 "$LH" A
internal 169.254.169.253
amazonaws.com 169.254.169.253
ip-172-31-92-201.ec2.internal
172.31.92.201

If your VPC uses Route 53 private hosted zones, add one line per zone (for example corp.example.internal 169.254.169.253 is already covered by internal; example.com 169.254.169.253 would send a whole private zone to the VPC resolver), then restart dnscrypt-proxy:

echo 'example.com 169.254.169.253' | sudo tee -a /etc/dnscrypt-proxy/forwarding-rules.txt
sudo systemctl restart dnscrypt-proxy

Step 9: The instance's own DNS

After first boot the instance resolves through its own encrypted resolver; the VPC search domain is kept:

grep -v '^#' /etc/resolv.conf
nameserver 127.0.0.1
options edns0 trust-ad
search ec2.internal

To send the instance's own lookups to the VPC resolver again, run sudo ln -sfn /run/systemd/resolve/resolv.conf /etc/resolv.conf. dnscrypt-proxy keeps serving your clients either way.

Step 10: The monitoring dashboard (optional)

dnscrypt-proxy has a built-in monitoring dashboard (query counts, cache hit rate, resolver latencies). It is disabled on this image. To use it, set enabled = true in the [monitoring_ui] section of /etc/dnscrypt-proxy/dnscrypt-proxy.toml and restart dnscrypt-proxy. It listens on 127.0.0.1:8080 only, so reach it through an SSH tunnel from your workstation and open http://127.0.0.1:8080/:

sudo sed -i '/^\[monitoring_ui\]/,/^\[/ s/^enabled = false/enabled = true/' /etc/dnscrypt-proxy/dnscrypt-proxy.toml
sudo systemctl restart dnscrypt-proxy
ssh -i ~/.ssh/my-key-pair.pem -L 8080:127.0.0.1:8080 ubuntu@<public-ip>

Sign in as admin with the password generated on your instance, which you read on the instance with:

sudo grep '^MONITORING_UI_PASSWORD=' /root/dnscrypt-proxy-credentials.txt | cut -d= -f2-

On this image the dashboard shows aggregate figures only and keeps no list of recent queries (privacy_level = 2, enable_query_log = false). Keep it on 127.0.0.1.

Step 11: Metadata access is blocked

dnscrypt-proxy cannot reach the instance metadata service; the Amazon resolver (169.254.169.253) is deliberately not blocked:

systemctl show -p IPAddressDeny dnscrypt-proxy.service
IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128

Step 12: How the fail-closed start works

dnscrypt-proxy.service runs only when first boot's ready marker exists and only together with the allow-list unit, and every start runs a check as root first (the allow-list is loaded, the configuration parses, the dashboard password is a real one, and no forwarding rule points at the metadata service). The files that hold per-instance data are readable only by root or the service:

systemctl cat dnscrypt-proxy.service | grep -E '^(ConditionPathExists|Requires|ExecStartPre|ExecStart|User|AmbientCapabilities)='
sudo stat -c '%a %U:%G %n' /root/dnscrypt-proxy-credentials.txt /etc/dnscrypt-proxy/dnscrypt-proxy.toml /etc/dnscrypt-proxy/allowed-clients.txt
Requires=cloudimg-dnscrypt-acl.service
ConditionPathExists=/var/lib/cloudimg/dnscrypt-proxy-ready
User=dnscrypt-proxy
ExecStartPre=+/usr/local/sbin/dnscrypt-proxy-preflight
ExecStart=/usr/local/bin/dnscrypt-proxy -config /etc/dnscrypt-proxy/dnscrypt-proxy.toml
AmbientCapabilities=CAP_NET_BIND_SERVICE
600 root:root /root/dnscrypt-proxy-credentials.txt
640 root:dnscrypt-proxy /etc/dnscrypt-proxy/dnscrypt-proxy.toml
644 root:root /etc/dnscrypt-proxy/allowed-clients.txt

Step 13: Security recommendations

  • Keep SSH open only to your management addresses. Open UDP and TCP 53 only to the networks the resolver serves, never to 0.0.0.0/0.
  • Run two resolvers in different Availability Zones if your VPC depends on them, and list both in the DHCP options set.
  • Leave query logging off unless you need it; if you turn it on, treat the log as sensitive and rotate it.
  • Leave the dashboard off, or on 127.0.0.1 behind an SSH tunnel.
  • Leave unattended security upgrades on. dnscrypt-proxy is a single binary in /usr/local/bin/dnscrypt-proxy; cloudimg ships updated versions as new versions of this listing.

Support and licensing

dnscrypt-proxy is open-source software by Frank Denis and contributors, distributed under the ISC licence; the licence, the upstream example configuration and rule files, and cloudimg's build record (release, signature key and checksums) are in /usr/share/doc/dnscrypt-proxy/. cloudimg provides the packaging, the secure defaults, the VPC allow-list, the AWS name forwarding, the per-instance dashboard password, the metadata block, the fail-closed start, the self-test and helpers, and 24/7 support.

cloudimg is not affiliated with or endorsed by the DNSCrypt project. dnscrypt-proxy is used here only to identify the software. Quad9 and other resolver names shown are those of their operators.

For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.