Security & privacy audit

How to report a vulnerability, and how DNSBunker handles data, checked against the running server and signed by the operator.

If you have found a security vulnerability in DNSBunker, please report it privately to security@dnsbunker.org, encrypted with the PGP key below if you can. Describe what you found, how to reproduce it and what impact you expect. Reports in English or German are welcome.

Safe harbor

Research and reports made in good faith under this policy will not lead to legal action by the operator, and the operator will not ask authorities to take any. Good faith means that you:

  • only test with your own devices, connections and data;
  • do not access, change or keep data of other users, and stop as soon as you come across any;
  • do not degrade the service: no denial-of-service, flooding or load tests;
  • do not use social engineering, phishing or physical attacks against the operator or the hosting provider;
  • give the operator reasonable time to fix the issue before you publish it.

If you are unsure whether something is covered, ask first. You will get a confirmation of receipt, updates on the fix and, if you wish, public credit. There is no bug bounty.

PGP key

Owner
Patrick Scholing, operator of DNSBunker
Fingerprint
C062 8376 18FD 65ED 2BED 1163 1E89 5311 330D 6DB7
Download
pgp-key.txt
Contact file
security.txt (RFC 9116, signed)
Date
14 October 2026
Service
dnsbunker.org (185.250.250.61, 2a0a:51c1:a:ea::)
Type
self-audit by the operator, Patrick Scholing
Revision
2, 14 October 2026 (DNSCrypt added, all configuration without comments)
Signature
OpenPGP, key C062 8376 18FD 65ED 2BED 1163 1E89 5311 330D 6DB7

1. Summary

DNSBunker answers DNS queries without writing them anywhere. This audit checked the running configuration of every service on the server, the content of all logs, the memory and swap settings, and the behaviour of the resolver from the outside. Where a setting kept more data than necessary, it was changed during the audit (section 5), and the privacy policy was corrected to match the actual operation (section 6).

  • No query logs: no component writes DNS queries or client IP addresses to disk.
  • Short memory: for abuse protection, the last 50,000 queries (about 70 seconds at current load) stay in a rolling in-memory buffer and are then overwritten. Per-IP rate-limit counters are dropped 60 seconds after the last query.
  • No swap: the memory of the DNS, web and time services cannot be paged out to disk.
  • No access logs: the website keeps none. Remaining warnings and errors are stripped of client addresses and live only in a volatile journal for at most 24 hours.
  • Four encrypted protocols: DoH, DoT, DoQ and DNSCrypt. Plain DNS is not answered.
  • Nothing leaves the server about users: no EDNS Client Subnet, QNAME minimisation on, no telemetry, no security polling, no third-party content on the website.
  • Remaining limits are listed openly in section 7.

2. Scope and method

The audit covers the server behind dnsbunker.org and every service running on it: dnsdist (DoT, DoH, DoQ and DNSCrypt frontend), PowerDNS Recursor, Caddy (website and DoH proxy), lighttpd (static website), chrony (public NTP), the statistics collector, the blocklist updater, logging, firewall and SSH.

Method:

  • Configuration review of all files listed in section 9, taken from the running server on 14 October 2026. They are published in full, with only secrets and abuse thresholds redacted.
  • Runtime inspection: listening sockets, cgroup memory and swap limits, core dump settings, journal storage, all files under /var/log, systemd timers.
  • Log content scan: every journal entry was searched for IP addresses other than the server's own. Results are in section 4.6.
  • External tests against the public endpoints, reproducible by anyone (section 8).

This is a self-audit. It shows how the service is configured and behaves; it cannot prove that the operator or the hosting provider will never change it. That residual trust is described in section 7.

3. Architecture and data flow

A query travels through the following components. Only the first two ever see a client IP address.

  • Caddy (port 443) terminates HTTPS for DoH and the website. It forwards DoH to dnsdist over HTTP/2 on localhost and passes the client address in X-Forwarded-For, which dnsdist needs for rate limiting.
  • dnsdist (port 853 for DoT and DoQ, port 8443 for DNSCrypt, localhost for DoH) applies rate limits, answers from its packet cache or forwards the query to the recursor.
  • PowerDNS Recursor (localhost only) validates DNSSEC, applies the blocklists and asks authoritative nameservers on the internet. It only ever receives queries from dnsdist on 127.0.0.1, so it never learns a client address.
  • lighttpd (localhost only) serves the static website behind Caddy and receives no client address at all.
  • chrony (UDP port 123) is a separate public time service on the same server.

Plain DNS on port 53 is not offered; the firewall drops it.

4. Findings

4.1 DNS frontend (dnsdist 2.1.3)

  • No logging actions, no dnstap, no protobuf export, no remote logger. Nothing about queries is written to the journal; the scan in 4.6 found no dnsdist entries at all.
  • Ring buffer: 50,000 queries and 50,000 responses (client address, name, type, time, response code and size) are kept in memory for the dynamic blocking rules, which look at the last 10 to 20 seconds. At about 730 queries per second this covers about 70 seconds; older entries are overwritten. The buffer can only be read through the local console (127.0.0.1) and is not exported.
  • Per-IP rate limiting: token buckets per IPv4 address or IPv6 /64, discarded 60 seconds after the last query and fully swept every 30 seconds. Dynamic blocks of abusive addresses last 60 seconds.
  • Packet cache: stores answers keyed by the question only. It contains no client data; EDNS padding and keepalive options are excluded from the key.
  • No EDNS Client Subnet is added, and incoming ECS options are not forwarded (tested in 8.1).
  • Security polling is disabled (setSecurityPollSuffix("")), so the server does not announce its version to anyone.
  • The statistics web server listens on 127.0.0.1 only and requires authentication for everything. Its dashboard shows totals, rule and server tables and, at most, currently blocked address ranges; it has no view of individual queries or clients.
  • TLS for DoT: TLS 1.2 and 1.3 only; session ticket keys rotate every hour, at most two are valid, tickets are issued with a lifetime of one hour.
  • DNSCrypt: the long-term provider key (Ed25519) is stored on disk, readable only by root and the dnsdist service, and is used only to sign certificates. The short-term resolver keys (X25519) are generated by dnsdist every 12 hours and are valid for 24 hours. They are kept in /run/dnsdist, which is in RAM, survives a service restart but not a reboot, and is excluded from swap. Expired certificates are removed automatically; at most three exist at any time. The kernel-level eBPF filter that drops blocked addresses is detached from the DNSCrypt ports, because it cannot parse encrypted packets and dropped about half of them in testing; abusive DNSCrypt clients are still blocked by dnsdist itself.

4.2 Recursive resolver (PowerDNS Recursor 5.4.7)

  • Listens on 127.0.0.1 only and accepts queries only from 127.0.0.1.
  • edns_subnet_allow_list is empty: no part of any client address is ever sent to authoritative servers.
  • QNAME minimisation (RFC 9156) is enabled. Aggressive use of DNSSEC-validated cache (RFC 8198) reduces the number of upstream queries further.
  • Local copies of the root, arpa, in-addr.arpa and ip6.arpa zones (RFC 8806), downloaded from internic.net and verified with DNSSEC (and ZONEMD for the root zone), mean that names in these zones are answered without asking their nameservers at all.
  • Private and special-use reverse zones (RFC 1918, RFC 6303) are answered locally and never leak upstream.
  • DNSSEC validation is enforced; failures return SERVFAIL with an extended error code.
  • No query logging, no carbon/metrics export, no web API, security polling disabled.
  • The recursor keeps internal statistics of the most recently queried names for its diagnostic top lists. Since every query comes from 127.0.0.1, these contain no client addresses.
  • Blocklists (RPZ) are evaluated in memory; only per-list totals are counted for the public statistics.

4.3 Transport encryption

  • DoT and DoQ on port 853, DNSCrypt version 2 on port 8443 (UDP and TCP, X25519 with XChaCha20-Poly1305), DoH over HTTP/2 and HTTP/3 on port 443. Plain DNS is not accepted.
  • TLS 1.0 and 1.1 are refused (tested in 8.6).
  • DNSCrypt over TCP works for queries, including the fallback when an answer is too large for UDP. dnsdist does not answer the unencrypted certificate request over TCP, so clients must fetch the certificate over UDP, which is their default; clients forced to TCP only, for example through Tor, should use DoT or DoH instead.
  • Every response to a query with EDNS is padded to a multiple of 468 bytes (RFC 8467), whether or not the client asked for it, so its size reveals less about the name (tested in 8.5).
  • The website and DoH endpoint publish Encrypted Client Hello (ECH) keys in DNS, hiding the server name from on-path observers for clients that support ECH.
  • Session resumption: for DoT and DoH, ticket keys exist only in memory, rotate every hour, and at most two are valid. A resumed connection can therefore only be linked to an earlier one within about two hours; this link is not recorded anywhere. DoQ uses the QUIC library's in-memory keys.

4.4 Website and DoH proxy (Caddy 2.11.7, lighttpd 1.4.79)

  • No access logs in Caddy or lighttpd.
  • Caddy logs only warnings and errors. Client IP, client port, request headers and the DoH query parameter dns are deleted from every entry, and IP:port pairs inside error details are replaced with [redacted]. Per-request error messages, whose message text could contain an address, are excluded from logging entirely.
  • lighttpd only writes start, stop and log-rotation notices. Since this audit, Caddy no longer forwards the client address to lighttpd at all.
  • Per-IP rate limiting in Caddy uses one-second windows in memory.
  • The website sets no cookies and loads nothing from third parties; all fonts and scripts are self-hosted (tested in 8.8). The only external hosts it contacts are the setup-check names under check.dnsbunker.org, which run on the same server.
  • Strict Content-Security-Policy (self only), HSTS with preload, Referrer-Policy no-referrer, a restrictive Permissions-Policy and frame protection are set on every page.
  • The language choice is stored in the visitor's own browser (localStorage) and never sent anywhere.

4.5 Operating system

  • systemd-journald runs with Storage=volatile: the journal lives in RAM, is limited to 32 MB and entries older than one day are deleted.
  • Core dumps are disabled (kernel.core_pattern = |/bin/false), so no process memory can be written to disk on a crash.
  • Swap: the server has a 5.1 GB swap partition. Since this audit, dnsdist, the recursor, Caddy, journald and chrony run with MemorySwapMax=0, so their memory, including query data and the volatile journal, can never be paged out.
  • Firewall (nftables): only ports 80, 443, 853, 8443 and 123 are open. Service traffic is exempt from connection tracking (notrack), so the kernel does not keep a table of client connections either.
  • SSH is stopped and masked in normal operation and the firewall has no rule for it. The operator starts it only for maintenance windows through the hosting provider's out-of-band console. Since this audit, it accepts public keys only.
  • /var/log contains only package-manager logs, lighttpd start/stop notices and the system login records of the operator. No file contains DNS queries or addresses of DNS or website users.

4.6 Log content scan

The volatile journal was searched for IP addresses other than the server's own, private ranges and localhost.

  • Before the changes: 5,277 entries over 23 hours. Four Caddy warnings contained a client address inside an error detail ("write tcp server->client: connection reset by peer"). The remaining hits were SSH login attempts during a maintenance window and the operator's own login.
  • After the changes: the journal was cleared once so that no entry from before the fix remains. Afterwards, aborted website downloads were provoked on purpose: Caddy logged them with both addresses replaced by [redacted], and the journal contained no address of a DNS or website user. The only addresses left in it belong to SSH sessions of the current maintenance window.

4.7 Public time service (chrony 4.6.1)

  • The server answers NTP queries from anyone on port 123. To rate-limit abusive clients, chrony keeps a table of recently seen addresses with packet counters in memory. Since this audit it is limited to 8 MB (before: 100 MB); older entries are overwritten continuously. Measured after the change: the table holds 65,536 addresses, the oldest entry was 9.5 minutes old and half of all entries were less than 2 minutes old (before: about 524,000 addresses, the oldest 3.5 hours old).
  • chrony writes no logs, no client data to disk, and its own time sources are authenticated with NTS.

4.8 Statistics collector

  • A sandboxed service reads totals from the dnsdist API once a minute and writes them to a file in RAM. It stores sums only: total and blocked queries (per blocklist), protocol shares, IPv4/IPv6 share, cache hits and average latency. Per-minute values are kept for 24 hours. No addresses and no names are involved.

4.9 Connections the server makes on its own

None of these carry information about users.

  • Authoritative nameservers worldwide, for recursive resolution (only the server's address and the minimised name).
  • hagezi-mirror.dnsbunker.org (served through Cloudflare), every four hours, to download the blocklists.
  • www.internic.net, every four hours, for the root and arpa zone copies.
  • NTP sources with NTS: PTB (Germany), BEV (Austria), time.nl (Netherlands).
  • Debian package mirrors; api.github.com and caddyserver.com for weekly Caddy update checks.
  • Cloudflare's DNS API and the ACME certificate authority, to publish ECH keys and to obtain the certificate for check.dnsbunker.org.

4.10 Third parties

  • IP-Projects GmbH & Co. KG (Germany) rents the virtual server in Frankfurt. It has no access to query content, but as with any hosted server it controls the physical hardware.
  • Cloudflare, Inc. (USA) hosts the DNS zone of dnsbunker.org and serves the blocklist mirror. DNS queries, website visits and the setup check do not pass through Cloudflare. The operator's password-protected statistics dashboard is reached through Cloudflare; it shows totals and, at most, currently blocked address ranges.
  • No analytics, advertising, CDN, font or tracking providers are used.
  • Besides the HaGeZi lists, the local list blocks exactly two names, both technical: use-application-dns.net (the canary that tells Firefox not to switch to its own DoH provider) and dns-tunnel-check.googlezip.net.
  • No domain is blocked for legal reasons. As of this audit there have been no authority requests for user data and no blocking orders.

5. Changes made during this audit

All changes were applied on 14 October 2026 and are reflected in the published configuration.

  • Caddy: client addresses are now also removed from error details; per-request error messages are no longer logged; lighttpd no longer receives the client address (X-Forwarded-For and X-Real-IP removed).
  • Caddy: TLS session ticket keys now rotate every hour with at most two valid keys (before: every 12 hours, four keys).
  • dnsdist: ring buffer reduced from 200,000 to 50,000 entries (from about 4.6 minutes to about 70 seconds of history).
  • dnsdist: per-IP rate-limit counters are dropped after 60 seconds instead of 300 seconds.
  • dnsdist: DoT session ticket keys now rotate every hour, at most two valid, ticket lifetime one hour (before: every 12 hours, five keys).
  • dnsdist, recursor, Caddy, journald, chrony: excluded from swap (MemorySwapMax=0).
  • chrony: client table reduced from 100 MB to 8 MB (before: about 524,000 addresses, oldest 3.5 hours).
  • SSH: password and keyboard-interactive login disabled; public keys only.
  • Journal: cleared once after the changes.

Revision 2, also on 14 October 2026:

  • dnsdist: DNSCrypt version 2 added on port 8443, with short-term keys in RAM as described in 4.1. The setup check on the website recognises it, and the public statistics count it as a fourth protocol.
  • All configuration files and scripts of the service had their comments removed on the server itself, so the published copies are the running files, changed only where section 9 says so. Comments were removed with a tool that only deletes comment text and verifies that no other line changes; for Python scripts it also proves that the syntax tree stays identical.
  • The blocklist updater no longer writes comments into the zone files; the checksum it uses to detect changes now lives in a separate file.

6. Privacy policy corrections

The privacy policy was compared sentence by sentence with the findings above and updated on 14 October 2026 in all five languages:

  • The summary said that lookups and IP addresses are held in memory only "for the milliseconds it takes to answer". It now also describes the abuse-protection ring buffer of about one minute.
  • The website section claimed access logs of up to 7 days. In reality there are none; it now describes the filtered error logging and the one-second rate limit.
  • TLS session tickets were described as rotating only on restart; they now rotate hourly.
  • The public statistics section listed two counters; it now lists all aggregate figures.
  • The blocklist source now names the project's own mirror behind Cloudflare.
  • New sections describe the public time service (NTP) and the role of Cloudflare.
  • Revision 2: DNSCrypt was added to the transport section and to the statistics description.

7. Residual risks and limits

  • Trust in the operator: the operator has root access and could change the configuration. This audit describes the state on 14 October 2026; later changes will be documented on the same page.
  • Trust in the hosting provider: the provider controls the hypervisor and could in theory read the server's memory.
  • Ring buffer: for about 70 seconds, the operator could look up recent queries with client addresses through the local console. This data is required for automatic abuse protection.
  • Session resumption: within up to two hours, a resumed TLS connection can be recognised as coming from the same client. Clients can disable resumption.
  • Authoritative nameservers necessarily see the names being resolved, minimised and from the server's address, never with a client address.
  • Public NTP: the time service keeps recently seen addresses for rate limiting, as described in 4.7.
  • The swap partition itself is not encrypted; the services that handle user data are excluded from it.

8. Verify it yourself

All tests were run on 14 October 2026 against the public DoT endpoint. Equivalent commands with kdig (knot-dnsutils):

8.1 No EDNS Client Subnet

kdig +tls @dnsbunker.org o-o.myaddr.l.google.com TXT
kdig +tls +subnet=203.0.113.0/24 @dnsbunker.org o-o.myaddr.l.google.com TXT

Result: Google's nameserver sees only 2a0a:51c1:a:ea:: or 185.250.250.61 and reports no client subnet, also when the client sends one.

8.2 QNAME minimisation

kdig +tls @dnsbunker.org qnamemintest.internet.nl TXT

Result: "HOORAY - QNAME minimisation is enabled on your resolver :)!"

8.3 No forwarding to another resolver

kdig +tls @dnsbunker.org whoami.akamai.net A

Result: 185.250.250.61, the server itself.

8.4 DNSSEC validation

kdig +tls @dnsbunker.org dnssec-failed.org A

Result: SERVFAIL with extended error 7 (Signature Expired). For correctly signed domains, kdig +tls +adflag @dnsbunker.org internetsociety.org shows the ad flag.

8.5 Response padding

kdig +tls +padding @dnsbunker.org example.com A

Result: the response is padded to exactly 468 bytes. Without +padding, responses to EDNS queries are padded as well.

8.6 TLS versions

openssl s_client -connect dnsbunker.org:853 -tls1_2
openssl s_client -connect dnsbunker.org:853 -tls1_1 -cipher 'DEFAULT@SECLEVEL=0'

Result: TLS 1.2 and 1.3 are accepted; TLS 1.0 and 1.1 are refused by the server with a protocol-version alert. (Without the SECLEVEL option, current OpenSSL versions already refuse TLS 1.1 on the client side.)

8.7 DNSCrypt

kdig @185.250.250.61 -p 8443 2.dnscrypt-cert.dnsbunker.org TXT
dnslookup example.com sdns://AQMAAAAAAAAAEzE4NS4yNTAuMjUwLjYxOjg0NDMgNw_pM00juOiBe9ZNN0x42xQvsI5EWe3DjleywHCz0eUdMi5kbnNjcnlwdC1jZXJ0LmRuc2J1bmtlci5vcmc

Result: the first command shows the currently valid certificates (at most three, each valid for 24 hours); the second resolves through DNSCrypt with the AdGuard dnslookup tool. The provider public key is 370F:E933:4D23:B8E8:817B:D64D:374C:78DB:142F:B08E:4459:EDC3:8E57:B2C0:70B3:D1E5.

8.8 Website requests and cookies

Loading all four pages in a browser with developer tools open shows requests only to dnsbunker.org and the check.dnsbunker.org setup-check names, and no cookies.

8.9 This document

gpg --fetch-keys https://dnsbunker.org/.well-known/pgp-key.txt
gpg --verify privacy-audit-2026-10.txt
grep -oE '[0-9a-f]{64}  [A-Za-z0-9._-]+' privacy-audit-2026-10.txt | sha256sum -c

Run the last command in a directory that contains the downloaded configuration files.

The fingerprint of the signing key must be C062 8376 18FD 65ED 2BED 1163 1E89 5311 330D 6DB7.

9. Published configuration

The files below are copies of the configuration running on the server, published at https://dnsbunker.org/security/config-2026-10/. Passwords, API keys and abuse thresholds are replaced with [REDACTED]; the operator's administration host is removed. Everything else is unchanged. SHA-256 checksums:

9df92b32774ce28eb30678ca469e81cc13fb660d07db8c97bd70564455f9a965  dnsdist.conf.txt
0fb7e64d3c92a389040cdbafe0b27637b2fa9ff428d8fdd1b643cf5346548bf4  recursor.conf.txt
8b51c957a25524632d3a4c98df102d61975c0e992fe7773b09e21afd9b031a1d  rpz-00-local.rpz.txt
580fe6ab247d6dd9e337ba7fec72899c02d761eeaa57a67ffcc472438737fda3  rpz-10-allow.rpz.txt
08aa0d9ebe376bca995428c9f46f3da0a22bd40984f3da0b358a4ecf4496719b  rpz-20-deny.rpz.txt
6c115ef27035c8a62124857b9f2b46455873e584a47498e984e681f9a550ae60  rpz.dnsbunker.org.zone.txt
ff494cea4907f6982d334ffe6c985370db69742d48d56d965ab8e089da610297  rpz-update.txt
772ebb6f970dc69d9614d52e37d3bd9491db03499639a1fd2770ab9442503149  Caddyfile.txt
996087171c1bcf43ce46b00fcc736d71a880a6c33f554ceebf7acdc580cd3d20  caddy-config-build.txt
5559e03c4523314454c784a0a0de0aac78507529258316d1dcf3aa71a546651f  lighttpd.conf.txt
d3571e2ba919ad6fd269c657481a95064e31ea2c236db78d32ba6b941dbad798  chrony.conf.txt
a33415300b1111d732caf060cee7b18996a487784ca11022f69bf6dde3f5b4db  journald.conf.txt
a4183b44be2831d4542b47fec52d69e8aa1281f178417eb919b529b107250b50  nftables.conf.txt
13f9ae8c96d385aa4521ec4ce26d2a081e56f656b1ea551b73d47d7a6f95746d  sshd-10-dnsbunker.conf.txt
b7051f3351b8655b76b60ec73bd7eb4ba55823fdd09992c2747961e239810534  systemd-swap-dropins.txt
017fc052dfe6f52923664e59b8c6f3a0ff6cc1f35cd628a2ba90b9d2799c5c5a  dnsbunker-stats.txt
46a2aac9e15e76169565d530b8c42b3662982de981a227066e9e47bf7e3fd31e  dnsbunker-stats.service.txt
f6bda160393127f8fe80665516fb8dc0b89f99bb4fcfdf7865065f93b9fffa3a  dnsdist-dnscrypt-dropin.conf.txt
0b7370d0bbf0c1ff42a29665d219b6c5c4994acc74116fd8bcaffd3fe25896fd  sysctl-99-dnsbunker.conf.txt
03e643dd80e30c89168cff09ed67f26865384162cf14fa1723fee86217281f73  runtime-state.txt