RSA vs ECDSA in CoreDNS DNSSEC plugin: The CPU Cost of Signing NXDOMAINs
TL;DR#
The CoreDNS dnssec plugin guidance to prefer ECDSA is there for a reason.
In this post we see that with the same NXDOMAIN-signing workload, RSA (RSASHA256/3072) used 30x the amount of CPU compute compared to ECDSA (P‑256).
Introduction#
When you enable DNSSEC in CoreDNS, negative answers must be provably negative. CoreDNS implements authenticated denial with NSEC “black lies”. It forges per-query NSEC owner names to prevent zone walking, which means responses can’t be broadly reused by resolvers and each miss requires fresh signing. That pushes cryptographic signing onto the hot path, so the key algorithm directly determines CPU and packet size.
Dry information for those who are initiated:
- Draft: “Compact DNSSEC Denial of Existence or Black Lies”
- RFC 6605: “Elliptic Curve Digital Signature Algorithm (DSA) for DNSSEC”
- RFC 5702: “Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource Records for DNSSEC”
- RFC 8198: “Aggressive Use of DNSSEC-Validated Cache”
Note that ECDSA P-256 RRSIGs are 64 bytes, while RSASHA256-3072 RRSIGs are 256 bytes (size equals modulus). ECDSA signatures also save you bandwidth.
Test design#
I ran two otherwise identical configs that differed only in the DNSSEC key algorithm: RSA (RSASHA256, 3072; algorithm 8) and ECDSA (P‑256; algorithm 13). As mentioned in RFC 6605:
“Current estimates are that ECDSA with curve P-256 has an approximate equivalent strength to RSA with 3072-bit keys”
To force signing work, I generated many unique, random non‑existent names, set the DO bit and EDNS to trigger signing. I measured throughput and latency with dnsperf, and captured CPU profiles with pprof to summarize and visualize.
Environment#
CoreDNS was built as v1.13.1-dirty from commit coredns/coredns@60e2d455f9f2fd0f12b1ce3dcb64125913a26743 with Go 1.25.3 on macOS.
Supporting tools included dnsperf, bind or ldns key tools, and optionally graphviz for SVG outputs.
Configuration#
Zone file#
Save as db.example.test:
$ORIGIN example.test.
@ 3600 IN SOA ns1.example.test. hostmaster.example.test. (1 7200 3600 1209600 3600)
3600 IN NS ns1.example.test.
ns1 3600 IN A 127.0.0.1
Keys#
Generate one key for each algorithm:
- ECDSA P-256:
mkdir -p keys/ecdsa
( cd keys/ecdsa && dnssec-keygen -a ECDSAP256SHA256 -n ZONE example.test )
- RSA 3072:
mkdir -p keys/rsa
( cd keys/rsa && dnssec-keygen -a RSASHA256 -b 3072 -n ZONE example.test )
Note the basenames printed (e.g., Kexample.test.+013+15419 or Kexample.test.+008+09030). Use the exact basename in the Corefiles below.
Corefiles#
Corefile.ecdsa:
.:1053 {
pprof 127.0.0.1:6060
file db.example.test example.test
dnssec example.test {
key file keys/ecdsa/Kexample.test.+013+15419
}
bufsize 1232
}
Corefile.rsa:
.:1053 {
pprof 127.0.0.1:6060
file db.example.test example.test
dnssec example.test {
key file keys/rsa/Kexample.test.+008+09030
}
bufsize 1232
}
Buffer size is set to 1232 to prevent IP fragmentation with EDNS0. See bufsize plugin docs for more information if curious.
log and errors are omitted due to profiling. They add syscall noise.
Load generation#
Create a large set of unique NXDOMAIN queries:
jot -w "nonexist-%08d.example.test A" 200000 1 > queries.txt
Run CoreDNS for the chosen Corefile:
./coredns -conf Corefile.ecdsa
# or
./coredns -conf Corefile.rsa
Drive the load (DO bit + EDNS, match bufsize):
dnsperf -s 127.0.0.1 -p 1053 -d queries.txt -l 60 -Q 2000 -c 50 -e -D -b 1232
Profiling#
Capture a 30‑second CPU profile while the load is running:
go tool pprof -proto http://127.0.0.1:6060/debug/pprof/profile?seconds=30 > rsa.cpu.pb.gz
# or
go tool pprof -proto http://127.0.0.1:6060/debug/pprof/profile?seconds=30 > ecdsa.cpu.pb.gz
Generate clean visuals from call graphs in SVG format:
go tool pprof -svg -hide='^(syscall|runtime\.)' -focus='^crypto|dnssec' rsa.cpu.pb.gz > rsa.user.svg
# or
go tool pprof -svg -hide='^(syscall|runtime\.)' -focus='^crypto|dnssec' ecdsa.cpu.pb.gz > ecdsa.user.svg
Interactive exploration:
go tool pprof ./coredns rsa.cpu.pb.gz
# inside pprof:
hide=^(syscall|runtime\.)
top
top -cum
list sign
Results#
From pprof we can see that RSA used about 176 seconds of samples over 30 seconds:
(pprof) top
Showing nodes accounting for 168.37s, 95.49% of 176.32s total
ECDSA used about 5.6 seconds of samples over 30 seconds:
(pprof) top
Showing nodes accounting for 5.33s, 95.35% of 5.59s total
For this workload, RSA required approximately 30× more CPU than ECDSA.
Latency#
See table below.
| Algorithm | QPS | Avg Lat (ms) | Min (ms) | Max (ms) | StdDev (ms) | Avg resp bytes |
|---|---|---|---|---|---|---|
| ECDSA P-256 | 1999.65 | 0.298 | 0.083 | 40.003 | 1.333 | 495 |
| RSA-3072 | 1342.79 | 73.412 | 4.608 | 1055.376 | 69.898 | 1135 |
Detailed pprof output from RSA#
RSA top#
RSA call graph#
RSA flame graph#
Detailed pprof output from ECDSA#
ECDSA top#
ECDSA call graph#
ECDSA flame graph#
Conclusion#
Prefer ECDSA (P‑256) keys for online DNSSEC signing in CoreDNS, unless you have a compelling reason not to. Monitor coredns_dnssec_cache_hits_total and coredns_dnssec_cache_misses_total metrics series, and naturally overall CPU saturation from the process or container runtime.
If you end up debugging this further, use the pprof plugin in CoreDNS.