DEV Community

Cover image for The CPU Cost of Signing NXDOMAINs
Ville Vesilehto
Ville Vesilehto

Posted on • Originally published at ville.dev

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:

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
Enter fullscreen mode Exit fullscreen mode

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 )
Enter fullscreen mode Exit fullscreen mode
  • RSA 3072:
mkdir -p keys/rsa
( cd keys/rsa && dnssec-keygen -a RSASHA256 -b 3072 -n ZONE example.test )
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run CoreDNS for the chosen Corefile:

./coredns -conf Corefile.ecdsa
# or
./coredns -conf Corefile.rsa
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Interactive exploration:

go tool pprof ./coredns rsa.cpu.pb.gz
# inside pprof:
hide=^(syscall|runtime\.)
top
top -cum
list sign
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 top

RSA call graph

RSA flame graph

Detailed pprof output from ECDSA

ECDSA top

ECDSA top

ECDSA call graph

ECDSA call graph

ECDSA flame 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.

Comments and feedback welcome - and thanks for reading!

Top comments (0)