This document describes the current public-exposure posture of leaf and the exact deployment checklist to run it on the Internet safely.
The server now includes the following protections and protocol behavior:
- Authoritative-only behavior for configured zone(s) (
AAset only for in-zone answers). - Strict DNS query validation:
- non-
QUERYopcodes returnNOTIMP - multi-question requests return
FORMERR - out-of-zone names return
REFUSED
- non-
- Explicit
ANYquery policy: in-zoneANYqueries returnREFUSED. - Apex authoritative records:
SOAresponses at zone apexNSresponses at zone apex
- Negative caching behavior:
NXDOMAINresponses include zoneSOAin authority sectionNODATAresponses include zoneSOAin authority section
- UDP/TCP abuse controls:
- global QPS limiter
- per-IP QPS limiter
- per-IP + qname invalid-query limiter (
NXDOMAIN/REFUSED/FORMERR) - bounded tracked-IP table for rate limiting
- bounded tracked key table for invalid-query throttling
- global TCP connection cap
- per-IP TCP connection cap
- TCP hardening:
- idle/read/write timeouts
- max framed request size bound
- UDP hardening:
- max datagram request size bound
- Structured operational logs for startup and drop reasons (
rate_limited,invalid_query_rate_limited,request_too_large,parse_error,connection_limit_reached). - Optional per-query success logs (
udp_query,tcp_query) controlled by config and minimized to avoid client identifiers.
All values are configurable via CLI flags or environment variables.
You can also load values from a TOML file via --config or LEAF_CONFIG (or ./leaf.toml if present).
Core DNS:
LEAF_ZONES(required unlessLEAF_ZONEis set): comma-separated served zones, e.g.dev.example.com,prod.example.comLEAF_ZONE(optional): backward-compatible single-zone shortcutLEAF_CONFIG(optional): path to TOML config fileLEAF_LISTEN(default0.0.0.0:5300)LEAF_TTL(default60)LEAF_ZONE_NS(defaultns1.<zone>)LEAF_ZONE_HOSTMASTER(defaulthostmaster.<zone>)
SOA:
LEAF_SOA_SERIAL(default1)LEAF_SOA_REFRESH(default300)LEAF_SOA_RETRY(default60)LEAF_SOA_EXPIRE(default86400)LEAF_SOA_MINIMUM(default60)
Traffic and connection controls:
LEAF_GLOBAL_QPS_LIMIT(default5000)LEAF_PER_IP_QPS_LIMIT(default200)LEAF_PER_IP_INVALID_QNAME_QPS_LIMIT(default20)LEAF_LIMITER_MAX_TRACKED_IPS(default10000)LEAF_INVALID_QNAME_LIMITER_MAX_TRACKED_KEYS(default50000)LEAF_TCP_MAX_CONNECTIONS(default1024)LEAF_TCP_MAX_CONNECTIONS_PER_IP(default64)LEAF_TCP_IDLE_TIMEOUT_MS(default10000)LEAF_TCP_READ_TIMEOUT_MS(default3000)LEAF_TCP_WRITE_TIMEOUT_MS(default3000)LEAF_MAX_TCP_FRAME_BYTES(default4096)LEAF_MAX_UDP_REQUEST_BYTES(default1232)LEAF_LOG_QUERIES(defaultfalse)
Build binary:
cargo build --releaseAllow binding low ports without root:
sudo setcap 'cap_net_bind_service=+ep' ./target/release/leafRun as dedicated user (example):
sudo useradd --system --no-create-home --shell /usr/sbin/nologin leaf
sudo -u leaf \
LEAF_ZONES=dev.example.com,prod.example.com \
LEAF_LISTEN=0.0.0.0:53 \
./target/release/leafBuild container image:
podman build -t leaf:latest -f Containerfile .Run in container with minimum capabilities:
PUBLIC_IP=<your-public-ip>
sudo podman run -d --name leaf --restart=always \
--read-only \
--cap-drop=all \
--cap-add=NET_BIND_SERVICE \
-e LEAF_ZONES=dev.example.com,prod.example.com \
-e LEAF_LISTEN=0.0.0.0:53 \
-e LEAF_CONFIG=/etc/leaf/leaf.toml \
-v ./leaf.toml:/etc/leaf/leaf.toml:ro \
-p ${PUBLIC_IP}:53:53/udp \
-p ${PUBLIC_IP}:53:53/tcp \
leaf:latestNotes:
- Rootful Podman is recommended for direct binding on host port
53. - Prefer binding published ports to the server public IP (not wildcard) to avoid host resolver/listener conflicts.
- Keep filesystem read-only except explicit mounted config/log paths.
- Validate startup logs after restart to confirm bind and config load.
Expose only DNS:
- allow
53/udp - allow
53/tcp - deny other inbound ports to this host
Delegate a limited subzone (for example canary.dev.example.com) to this service before full-zone cutover.
From an external network, verify:
dig @<server-ip> 1-2-3-4.dev.example.com A +norecurse
dig @<server-ip> dev.example.com SOA +norecurse
dig @<server-ip> dev.example.com NS +norecurse
dig @<server-ip> nope.dev.example.com A +norecurseExpected outcomes:
Aquery for encoded name returnsNOERRORwith oneAanswer.- Apex
SOAandNSreturnNOERRORwith authoritative answers. - Unknown in-zone name returns
NXDOMAINwithSOAin authority section.
- Burst per-IP traffic above configured limit and confirm throttling in logs.
- Open many TCP connections from one source and confirm cap enforcement.
- Send oversized TCP frames and confirm immediate drop.
- Confirm process remains stable under sustained mixed load.
At minimum, alert on:
- process restarts
- sustained
rate_limitedevents above baseline - sustained
invalid_query_rate_limitedevents above baseline - sustained parse-error spikes
- socket bind/startup failures
The binary-level controls are now in place, but public DNS operation still requires external safeguards:
- upstream anti-DDoS capacity (provider or edge filtering)
- multi-instance deployment for redundancy
- centralized log shipping and retention
- documented incident response playbooks for abuse spikes
Do not perform full-zone public cutover until the deployment checklist is complete and validated in staging.