This guide describes how to create a dedicated sys-dns qube running dnscrypt-proxy to provide encrypted, anonymized DNS resolution for downstream VMs in Qubes OS.
This specific configuration prioritizes DNSCrypt with anonymized DNS relays rather than plain DNS-over-HTTPS (DoH). Anonymized DNS works by routing your queries through two independent relays before they reach the resolver, similar to how Tor uses entry and exit nodes. This means no single relay knows both who you are and what you are querying. The config enforces strict policies - DNSSEC validation, no-logging, and no-filtering resolvers only - while disabling DoH and Oblivious DoH to avoid centralized infrastructure and browser-vendor dependencies. The result is a privacy-first, censorship-resistant DNS setup that does not rely on any single provider or protocol.
Install:
You can create a new fedora-minimal template and perform all the configuration there. I will instead show a simple setup using
/rw/config/rc.localto avoid maintaining multiple templates.
1. Install dnscrypt-proxy in the fedora-xfce template:
sudo dnf install -y dnscrypt-proxy
sudo systemctl enable dnscrypt-proxy
2. Add dnscrypt-proxy configuration in fedora-xfce template:
This setup enables DNSCrypt with anonymized DNS (via relays), enforces DNSSEC, no-logging, and no-filter policies, and disables DoH and Oblivious DoH:
sudo tee /etc/dnscrypt-proxy/dnscrypt-proxy.toml << 'EOF'
server_names = []
listen_addresses = ['127.0.0.1:53', '0.0.0.0:53', '[::1]:53']
max_clients = 250
ipv4_servers = true
ipv6_servers = false
dnscrypt_servers = true
doh_servers = false
odoh_servers = false
require_dnssec = true
require_nolog = true
require_nofilter = true
disabled_server_names = ['scaleway', 'scaleway-ams']
force_tcp = false
timeout = 5000
keepalive = 30
cert_refresh_delay = 240
bootstrap_resolvers = ['9.9.9.11:53', '8.8.8.8:53']
ignore_system_dns = true
log_files_max_size = 10
log_files_max_age = 7
log_files_max_backups = 1
block_ipv6 = false
block_unqualified = true
block_undelegated = true
reject_ttl = 10
cache = true
cache_size = 4096
cache_min_ttl = 2400
cache_max_ttl = 86400
cache_neg_min_ttl = 60
cache_neg_max_ttl = 600
lb_strategy = 'wp2'
lb_estimator = true
[sources.public-resolvers]
urls = [
'https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md',
'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md',
'https://cdn.jsdelivr.net/gh/DNSCrypt/dnscrypt-resolvers@master/v3/public-resolvers.md'
]
cache_file = 'public-resolvers.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 73
prefix = ''
[sources.relays]
urls = [
'https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md',
'https://download.dnscrypt.info/resolvers-list/v3/relays.md',
'https://cdn.jsdelivr.net/gh/DNSCrypt/dnscrypt-resolvers@master/v3/relays.md'
]
cache_file = 'relays.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 73
prefix = ''
[anonymized_dns]
routes = [
{ server_name='*', via=['anon-scaleway', 'anon-scaleway-ams', 'anon-kama', 'anon-tiarap'] }
]
skip_incompatible = true
EOF
Note: The
10.139.1.1and10.139.1.2addresses are the standard Qubes OS DNS IPs assigned to downstream VMs. Binding to0.0.0.0:53ensures dnscrypt-proxy can accept queries on these interfaces.
3. Create and configure sys-dns qube:
Create fedora-based AppVM sys-dns and add option Provides network in Qube Manager (advansed settings)
The next commands are executed in the sys-dns terminal.
4. Disable systemd-resolved in sys-dns:
sudo tee /rw/config/rc.local << 'EOF'
#!/bin/bash
# Disable systemd-resolved to free up port 53
systemctl stop systemd-resolved 2>/dev/null
systemctl disable systemd-resolved 2>/dev/null
systemctl mask systemd-resolved 2>/dev/null
systemctl stop systemd-resolved-varlink.socket 2>/dev/null
systemctl disable systemd-resolved-varlink.socket 2>/dev/null
systemctl mask systemd-resolved-varlink.socket 2>/dev/null
systemctl stop systemd-resolved-monitor.socket 2>/dev/null
systemctl disable systemd-resolved-monitor.socket 2>/dev/null
systemctl mask systemd-resolved-monitor.socket 2>/dev/null
# Assign Qubes standard DNS IPs to eth0 for downstream VMs
# These IPs are dynamically provided to downstream qubes via Qubes mechanisms
ip addr add 10.139.1.1/32 dev eth0 2>/dev/null
ip addr add 10.139.1.2/32 dev eth0 2>/dev/null
# start dnscrypt-proxy (let's add it just in case)
systemctl start dnscrypt-proxy
EOF
5. Configure nftables (Accept Rules for DNS) in sys-dns:
sudo tee /rw/config/qubes-firewall-user-script << 'EOF'
#!/bin/bash
# Allow DNS queries from downstream qubes to 10.139.1.1/2
# vif* are interfaces for downstream qubes, group 2
nft add rule ip qubes custom-input iifgroup 2 ip daddr { 10.139.1.1, 10.139.1.2 } udp dport 53 accept 2>/dev/null
nft add rule ip qubes custom-input iifgroup 2 ip daddr { 10.139.1.1, 10.139.1.2 } tcp dport 53 accept 2>/dev/null
EOF
Restart sys-dns to apply all changes.
6. Test with test-vm:
Create a test-vm, set Net qube to sys-dns and visit dnsleaktest.com to confirm you are using DNScrypt resolvers and not your ISP’s DNS. You should see this:
If you stop dnscrypt-proxy service in sys-dns, internet will stop working in test-vm:

