
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
dnssec-server
Advanced tools
π‘ Pure JavaScript authoritative DNS server for Node.js with built-in DNSSEC, dynamic zones, and modern record support.
π‘ Authoritative DNS server with real-time DNSSEC for Node.js
dnssec-server brings modern, flexible DNS to the Node.js ecosystem. Instead of managing static zone files or running heavyweight daemons like BIND, you can now compute DNS answers directly in JavaScript. Every response can be signed at runtime with DNSSEC, records can be generated dynamically (Geo-LB, canary, service discovery), and modern RR types are supported out of the box. All with a lightweight API, easy integration, and zero complex configuration.
Running DNS in production is usually associated with heavyweight software such as BIND, NSD or PowerDNS.
These are robust and widely used, but they come with trade-offs:
For modern stacks that are API-driven, containerized, and globally distributed, this is painful and slow.
dnssec-server takes a different approach: it is a pure JavaScript authoritative DNS server designed to live inside your Node.js application and give you DNS as code.
SVCB/HTTPS for service discovery, to TLSA for DANE, CAA for certificate policy, and URI for service endpoints. Aligned with the latest RFCs.res.send(). No need to learn zone file syntax.This makes dnssec-server especially useful when:
HTTPS/SVCB, ECH, DoT) and need a flexible testbed.In short: dnssec-server lets you treat DNS as part of your codebase β not as an external, opaque system.
npm i dnssec-server
Works with both ESM and CommonJS:
// ESM
import DNSServer from 'dnssec-server';
// CommonJS
const DNSServer = require('dnssec-server');
Below is a minimal authoritative server (UDP/TCP by default; enable DNSβoverβTLS via options.tls). It returns an A record for example.com. and NXDOMAIN otherwise.
import fs from 'node:fs';
import tls from 'node:tls';
import DNSServer from 'dnssec-server';
DNSServer.createServer({
tls: { // enable DNS over TLS (DoT) on port 853 by default
SNICallback: function (servername, cb) {
cb(null, tls.createSecureContext({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}));
}
}
}, function (req, res) {
console.log(`[${req.transport}] from ${req.remoteAddress}:${req.remotePort} q=${req.name} ${req.type} ${req.class} DO=${req.flag_do} ECS=${req.ecsAddress||'-'}/${req.ecsSourcePrefixLength||'-'}`);
if (req.name === 'example.com.' && req.type === 'A') {
res.answers.push({
name: 'example.com.',
type: 'A',
class: 'IN',
ttl: 300,
data: { address: '200.10.10.1' }
});
res.send();
} else {
res.header.rcode = 3; // NXDOMAIN
res.authority.push({
name: 'example.com.',
type: 'SOA',
class: 'IN',
ttl: 300,
data: {
mname: 'ns1.example.com.',
rname: 'hostmaster.example.com.',
serial: 2025081001,
refresh: 3600,
retry: 600,
expire: 604800,
minimum: 300
}
});
res.send();
}
});
createServer(options, handler)Creates and starts an authoritative DNS server.
Parameters
options
udp
(boolean | { port?: number, address?: string }) β enable/disable UDP transport (default: enabled, port 53 unless overridden).tcp
(boolean | { port?: number, address?: string }) β enable/disable TCP transport (default: enabled, port 53 unless overridden).tls
(false | { port?: number, address?: string, SNICallback?: (servername, cb) => void }) β enable DNSβoverβTLS (default: disabled). Supply an SNICallback that returns a SecureContext.dnssec (optional)
keyCallback: (name: string, cb: (err, material | null) => void) β called on every query with DO=1 to retrieve signing keys. See DNSSEC.cache (optional) β RRSIG signature cache. See RRSIG Caching. Can be:
true β use built-in in-memory cache.{ get, set } β provide your own cache backend (Redis, LRU, etc.).false β no caching, sign every response.cacheMax (number, default 10000) β max entries for the built-in cache (only when cache: true).onError (optional) β function(err, transport) called on socket errors. transport is 'udp4', 'udp6', 'tcp', or 'tls'. Without this, socket errors are silently ignored.handler(req, res)
your request handler. Populate res.answers / res.authority / res.additionals then call res.send().Returns an object with:
close(cb) β gracefully shut down all transports.clearRrsigCache() β clear the RRSIG signature cache (built-in mode).rrsigCacheSize() β return number of cached signatures (built-in mode).context β internal server context (for advanced use).Note: Defaults above align with common expectations; if you need precise behavior (binding interfaces/ports, concurrency, etc.) refer to the code and examples.
req) objectProperties commonly used in logic:
id (number) β message ID.transport ('udp4'|'udp6'|'tcp'|'tls'|'quic') β which transport delivered the query.remoteAddress (string), remotePort (number) β client socket tuple (for ECS-aware setups prefer ecsAddress).client ({ address, port, family }) β full client info including 'IPv4' or 'IPv6'.name (FQDN with trailing dot) β qname, e.g. example.com.type (string) β qtype, e.g. A, AAAA, TXT, ...class (string) β typically IN.flag_qr / flag_aa / flag_tc / flag_rd / flag_ra / flag_ad / flag_cd (booleans) β header flags.flag_do (boolean) β DO bit (EDNS(0) DNSSEC OK).edns (object | undefined) β full parsed EDNS data (udpSize, options, optionsStructured, etc.).edns_udp_size (number | undefined) β advertised UDP payload size.ednsOptions (array | undefined) β raw EDNS options list if you need to inspect.ecsAddress (string | undefined) β EDNS Client Subnet address if sent by resolver.ecsSourcePrefixLength (number | undefined) β ECS source prefix length.ecsScopePrefixLength (number | undefined) β ECS scope prefix length.tls (object | undefined) β TLS info when transport is 'tls' (includes authorized, alpn).raw (Uint8Array) β original wire bytes.message (object) β full decoded DNS message (header, questions, answers, etc.).res) objectheader β mutate flags/rcode:
res.header.aa = true (default authoritative)res.header.rcode = 0 (0=NOERROR, 3=NXDOMAIN, 2=SERVFAIL, ...)answers, authority, additionals β arrays of RRs. Each RR is { name, type, class:'IN', ttl, data } (see Supported Types).send() β finalize and transmit the response. You can call it once per query.When
flag_dois set and DNSSEC is configured, responses are signed automatically (RRSIG + DNSKEY/NSEC* as needed).
encodeMessage / decodeMessageFor advanced use cases, dnssec-server also exposes low-level primitives to
parse and serialize raw DNS messages.
This is useful if you want to:
import { encodeMessage, decodeMessage } from 'dnssec-server';
// Decode a raw DNS query from a Buffer/Uint8Array
const query = decodeMessage(rawBuffer);
// Manipulate the message object (add answers, change rcode, sign with DNSSEC, etc.)
query.answers.push({
name: 'example.com.',
type: 'A',
class: 'IN',
ttl: 60,
data: { address: '203.0.113.10' }
});
// Re-encode into wire format
const wire = encodeMessage(query);
This section explains how to generate DNSSEC material with the helper function and install it at both your registrar (e.g., Namecheap) and in your DNS zone.
buildDnssecMaterialYou can either provide your own private keys (Base64 string or Uint8Array), or leave them empty and the function will autoβgenerate random secure keys.
const DNSServer = require('dnssec-server');
// Auto-generate keys
const dnssec_material_obj = DNSServer.buildDnssecMaterial({ signersName: "example.com." });
// Or provide your own private keys
const dnssec_material_obj = DNSServer.buildDnssecMaterial({
signersName: "example.com.",
ksk: { privateKey: "BASE64-ENCODED-PRIVATE-KEY" },
zsk: { privateKey: "BASE64-ENCODED-PRIVATE-KEY" },
digestType: 2 // optional: 2=SHA-256 (default), 4=SHA-384
});
Example output:
{
"signersName": "example.com.",
"ksk": {
"keyTag": 14257,
"privateKey": "...",
"publicKey": "...",
"algorithm": 13,
"digestType": 2,
"digest": "A1B2C3D4E5F6..."
},
"zsk": {
"keyTag": 27179,
"privateKey": "...",
"publicKey": "..."
}
}
In Namecheap β Domain List β Manage β Advanced DNS β DNSSEC β Add DS Record, copy the following values from material.ksk:
ksk.keyTagksk.algorithm (usually 13)ksk.digestType (usually 2)ksk.digest (HEX uppercase)Example:
| Key Tag | Algorithm | Digest Type | Digest |
|---|---|---|---|
| 14257 | 13 | 2 | A1B2C3D4E5F6... |
β οΈ Only the KSK produces a DS record for the registrar. The ZSK is not used here.
The library handles DNSSEC automatically. Just pass the object returned by buildDnssecMaterial into the server via dnssec.keyCallback. No manual DNSKEY publishing is needed β dnssec-server will expose the DNSKEYs and serve signed responses.
import DNSServer from 'dnssec-server';
// 1) Generate (or load) DNSSEC material
const dnssec_material_obj = DNSServer.buildDnssecMaterial({
signersName: 'example.com.'
// optional:
// ksk: { privateKey: 'BASE64-P256-PRIVATE-KEY' },
// zsk: { privateKey: 'BASE64-P256-PRIVATE-KEY' },
});
// 2) Create your DNS/TLS server and wire DNSSEC
DNSServer.createServer({
dnssec: {
// Return DNSSEC material per signer name. You can support multiple zones.
keyCallback: function (name, cb) {
if (name.endsWith('example.com.')) cb(null, dnssec_material_obj);
else cb(null, null); // no DNSSEC for this name
}
}
}, function (req, res) {
// ... your request handling ...
});
Notes
buildDnssecMaterial output:
{
signersName: 'example.com.',
ksk: { keyTag, privateKey, publicKey, algorithm, digestType, digest },
zsk: { keyTag, privateKey, publicKey }
}
dnssec_material_obj to disk and load it on startup to keep keys stable across restarts.zsk and return it alongside the existing KSK; the server will continue serving a valid chain.After propagation, test with:
dig +dnssec example.com DNSKEY
dig +dnssec example.com A
Or use dnssec-debugger.verisignlabs.com to confirm everything validates.
ECDSA P-256 signing takes ~1β5ms per RRset. If many clients query the same records, the server re-signs identical responses unnecessarily. RRSIG caching avoids this by reusing previously computed signatures.
No cache (default) β every response is signed fresh:
DNSServer.createServer({
dnssec: { keyCallback: myKeyCallback }
}, handler);
Built-in in-memory cache β simple and zero-config:
DNSServer.createServer({
dnssec: {
keyCallback: myKeyCallback,
cache: true,
cacheMax: 5000 // optional, default 10000
}
}, handler);
Custom cache backend β full control over storage (Redis, LRU, database, etc.):
DNSServer.createServer({
dnssec: {
keyCallback: myKeyCallback,
cache: {
get: function (params, cb) {
// params: { name, type, class, keyTag, hash }
// Return cached value or null for cache miss
redis.get(params.hash + ':' + params.keyTag, function (err, raw) {
cb(err, raw ? JSON.parse(raw) : null);
});
},
set: function (params, value, cb) {
// value: { rrsig, expiration, inception }
var key = params.hash + ':' + params.keyTag;
var ttl = value.expiration - Math.floor(Date.now() / 1000);
redis.setex(key, ttl, JSON.stringify(value), cb);
}
}
}
}, handler);
get callbackCalled before signing. If it returns a valid cached entry, signing is skipped.
get(params, cb)
params contains:
name (string) β owner name of the RRset, e.g. 'example.com.'type (string) β RR type, e.g. 'A', 'AAAA', 'DNSKEY'class (string) β RR class, e.g. 'IN'keyTag (number) β key tag of the signing keyhash (string) β SHA-256 hex digest of the canonical RRset bytes (changes when record content changes)Call cb(null, cachedValue) on hit, cb(null, null) on miss, or cb(err) on error (will sign without caching).
set callbackCalled after signing a new RRSIG. Store the value however you like.
set(params, value, cb)
params is the same object as get. value contains:
rrsig (object) β the full RRSIG record, ready to push to answersexpiration (number) β UNIX timestamp when the signature expiresinception (number) β UNIX timestamp when the signature becomes validCall cb(null) when done. Errors don't affect the response (the signature was already computed).
The object returned by createServer provides:
const server = DNSServer.createServer({ ... }, handler);
server.clearRrsigCache(); // flush all cached signatures
server.rrsigCacheSize(); // number of cached entries (built-in mode)
These are useful after key rollover or zone updates to force re-signing.
If you are already operating an external DNS server (such as BIND) and maintaining your zones in standard zone files, you donβt need to rewrite your data.
dnssec-server provides first-class support for BIND-style zone files: you can load them directly, parse them with parseZone(), and use answerFromZone() to resolve queries.
This approach makes migration seamless:
$ORIGIN and $TTL directives are honored.priority, target, alpn, port, ipv4hint, ipv6hint, and glue generation.Supported today
$ORIGIN and $TTLdata:
priority, target, params (e.g. alpn, port, ipv4hint, ipv6hint, unknown keys preserved)ipv4hint/ipv6hint for the TargetName are added to additionalsβΉοΈ If your zone file doesnβt contain
$ORIGIN, passparseZone(text, { origin: 'example.com.' }).
import fs from 'node:fs';
import tls from 'node:tls';
import DNSServer from 'dnssec-server';
// 1) Load and parse multiple BIND-style zone files (sync for simplicity)
const zones = {
'example.com.': DNSServer.parseZone(fs.readFileSync('example.com.zone', 'utf8')),
'example.net.': DNSServer.parseZone(fs.readFileSync('example.net.zone', 'utf8')),
// add more apexes as needed...
};
// 2) Choose the zone by longest-suffix match
function pickZone(zs, qname) {
let bestOrigin = null, bestLen = -1;
for (const origin of Object.keys(zs)) {
const match = (qname === origin) || qname.endsWith('.' + origin.replace(/\.$/, '') + '.');
if (match && origin.length > bestLen) { bestOrigin = origin; bestLen = origin.length; }
}
return bestOrigin ? zs[bestOrigin] : null;
}
// 3) Create the server. Map each request to a pure answer object.
DNSServer.createServer({
tls: {
SNICallback: (servername, cb) =>
cb(null, tls.createSecureContext({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}))
}
}, (req, res) => {
const zone = pickZone(zones, req.name);
if(zone){
var { rcode, answers, authority, additionals } = DNSServer.answerFromZone(zone, req.name, req.type, req.class);
}else{
var rcode=3;
var answers=[];
var authority=[];
var additionals=[];
}
// π You have an opportunity to modify the response here (e.g., adjust TTLs,
// inject ECS-based answers, add/remove records, override rcode, etc.)
res.header.rcode = rcode; // 0 = NOERROR, 3 = NXDOMAIN
res.answers.push(...answers);
res.authority.push(...authority);
res.additionals.push(...additionals);
res.send();
}).listen(53, () => {
console.log('Authoritative DNS (UDP/TCP) on :53 + DoT on :853');
});
When your DNS server receives a query, the field req.remoteAddress does not usually show the real end-userβs IP.
Instead, it shows the resolver that made the query on the clientβs behalf β typically the ISPβs resolver, or a public one like Google (8.8.8.8) or Cloudflare (1.1.1.1).
To help authoritative servers make smarter decisions (e.g. geo-based routing), some resolvers add the EDNS Client Subnet (ECS) option.
ECS includes only a prefix of the userβs real IP address, never the full value, in order to preserve privacy.
For example: ECS=200.118.80.0/24
This means the real client was somewhere in the 200.118.80.xxx range, while the last octet was zeroed out.
The /24 indicates how many bits of the address are valid.
In this library, ECS data is exposed through convenient fields:
req.ecsAddress β the truncated client address (e.g. 200.118.80.0)req.ecsSourcePrefixLength β the prefix length (e.g. 24)Resolvers that do not send ECS will simply yield ECS=-/-.
ECS is useful if you want to implement geo-location logic, load balancing, or analytics that take into account where users are actually coming from, rather than just the resolverβs IP.
Return the nearest POP based on ECS if provided (preferred), or fall back to the socket IP.
function pickRegionByIp(ip) {
// toy example: map IP to region; replace with MaxMind-lite table or your own CIDR map
if (ip.startsWith('200.10.')) return 'sa-east';
if (ip.startsWith('192.0.2.')) return 'us-east';
return 'eu-west';
}
function handler(req, res){
var clientIp = req.ecsAddress || req.remoteAddress;
var region = pickRegionByIp(clientIp || '');
var targets = {
'sa-east': '200.10.10.1',
'us-east': '198.51.100.7',
'eu-west': '203.0.113.42'
};
res.answers.push({
name: req.name,
type: 'A',
class: 'IN',
ttl: 30,
data: {
address: targets[region]
}
});
res.send();
}
Tips
ecsAddress when available; many public resolvers (e.g., 8.8.8.8) include ECS.Serve two record sets with weighted selection.
function weightedPick(pct){ return Math.random()*100 < pct; }
function canaryA(req, res){
var canary = weightedPick(5); // 5% canary
res.answers.push({ name: req.name, type:'A', class:'IN', ttl:20, data:{ address: canary ? '203.0.113.77' : '203.0.113.10' }});
res.send();
}
SVCB/HTTPSAdvertise ALPNs, ports, IPv4/IPv6 hints, and ECH config as per RFC 9460/9461.
res.answers.push({
name: '_https.example.com.', type: 'HTTPS', class: 'IN', ttl: 300,
data: {
priority: 1,
targetName: 'svc.example.',
paramsStructured: {
alpn: ['h3','h2'],
port: 8443,
ipv4hint: ['192.0.2.10','192.0.2.11']
// ech, dohpath, tlsSupportedGroups ...
}
}
});
Synthesize answers depending on upstream health checks; switch NS/A sets when a region is down. Pair with short TTLs and RRSIG validity windows.
General RR structure in this library:
{
type: 'A' | 'AAAA' | 'MX' | ...,
name: 'example.com',
class: 'IN',
ttl: 300,
data: { ... } // record-specific fields
}
The following record types are supported. Each example shows the data object expected during encodeMessage (and returned during decodeMessage).
A{ data: { address: '127.0.0.1' } }
AAAA{ data: { address: Uint8Array(16) } }
NS, CNAME, PTR, DNAME{ data: { name: 'ns1.example.net.' } }
MD, MF, MB, MG, MR, NSAP_PTR, MAILA, MAILB{ data: { name: 'old-target.example.' } }
SOA{
data: {
mname: 'ns1.example.',
rname: 'hostmaster.example.',
serial: 2025082601,
refresh: 3600,
retry: 600,
expire: 1209600,
minimum: 300
}
}
MX{
data: {
preference: 10,
exchange: 'mail.example.'
}
}
TXT{ data: { texts: ['hello', 'world'] } }
HINFO{ data: { cpu: 'Intel',
os: 'Linux' } }
MINFO{
data: {
rmailbx: 'r.mail.example.',
emailbx: 'e.mail.example.'
}
}
RP{
data: {
mbox: 'admin.example.',
txt: 'info.example.'
}
}
AFSDB{
data: {
subtype: 1,
hostname: 'afsdb.example.'
}
}
X25{ data: { address: '311061700956' } }
ISDN{ data: { address: '150862028003217',
sa: '004' } }
RT{ data: {
preference: 10,
host: 'intermediate.example.'
} }
NSAP{ data: { address: Uint8Array([...]) } }
PX{ data: {
preference: 10,
MAP822: 'user.example.',
MAPX400: 'x400.example.'
} }
GPOS{
data: {
latitude: '48 51 29.0 N',
longitude: '2 17 40.0 E',
altitude: '0.00'
}
}
WKS{
data: {
address: '192.0.2.1',
protocol: 6,
bitmap: Uint8Array([...])
}
}
NULL{ data: { raw: Uint8Array([ ... ]) } }
SRV{
data: {
priority: 10,
weight: 5,
port: 443,
target: 'svc.example.'
}
}
NAPTR{
data: {
order: 100,
preference: 10,
flags: 'U',
services: 'SIP+D2T',
regexp: '!^.*$!sip:service@example.com!',
replacement: '.'
}
}
URI{
data: {
priority: 1,
weight: 10,
target: 'https://api.example/v1'
}
}
OPT{
type: 'OPT',
name: '.',
udpPayloadSize: 4096,
data: {
options: [
{
code: 12,
data: Uint8Array(31)
}
]
}
}
SVCB / HTTPS{
data: {
priority: 1,
targetName: 'svc.example.',
paramsStructured: {
alpn: ['h3','h2','http/1.1'],
noDefaultAlpn: true,
port: 8443,
ipv4hint: ['192.0.2.10','192.0.2.11'],
ech: Uint8Array([...]),
dohpath: '/dns-query{?dns}',
tlsSupportedGroups: [29,23]
}
}
}
DNSKEY / CDNSKEY{
data: {
flags: 257,
protocol: 3,
algorithm: 8,
key: Uint8Array([...])
}
}
KEY{
data: {
flags: 256,
protocol: 3,
algorithm: 5,
key: Uint8Array([...])
}
}
DS / CDS / DLV / TA{
data: {
keyTag: 12345,
algorithm: 8,
digestType: 2,
digest: Uint8Array([...])
}
}
RRSIG / SIG{
data: {
typeCovered: 1,
algorithm: 8,
labels: 1,
originalTTL: 300,
expiration: 1735689600,
inception: 1735603200,
keyTag: 12345,
signersName: 'example.',
signature: Uint8Array([...])
}
}
NSEC{
data: {
nextDomainName: 'b.example.',
types: ['A','TXT','RRSIG']
}
}
NSEC3{
data: {
hashAlgorithm: 1,
flags: 0,
iterations: 10,
salt: Uint8Array([0xde,0xad]),
nextHashedOwnerName: Uint8Array([...]),
types: ['A','AAAA','RRSIG']
}
}
NSEC3PARAM{
data: {
hashAlgorithm: 1,
flags: 0,
iterations: 10,
salt: Uint8Array([0xde,0xad])
}
}
DHCID{ data: { data: Uint8Array([...]) } }
TLSA / SMIMEA{
data: {
usage: 3,
selector: 1,
matchingType: 1,
certificate: Uint8Array([...])
}
}
CERT{ data: {
certType: 1,
keyTag: 12345,
algorithm: 8,
certificate: Uint8Array([...])
}
}
SSHFP{
data: {
algorithm: 1,
hash: 2,
fingerprint: Uint8Array([...])
}
}
IPSECKEY{
data: {
precedence: 10,
gatewayType: 3,
algorithm: 1,
gateway: 'gw.example.',
publicKey: Uint8Array([...])
}
}
LOC{
data: {
version: 0,
size: {
mant:0,
exp:0
},
horizontal:{
mant:0,
exp:0
},
vertical:{
mant:0,
exp:0
},
latitude: 0,
longitude: 0,
altitude: 0
}
}
APL{ data: { items: [ { family: 1,
prefix: 24,
neg: false,
address: Uint8Array([192,0,2]) } ] } }
HIP{
data: {
algorithm: 1,
hit: Uint8Array([...]),
publicKey: Uint8Array([...]),
servers: ['rendezvous1.example.']
}
}
NID{
data: {
preference: 10,
nodeIdHigh: 0x01234567,
nodeIdLow: 0x89abcdef
}
}
L32{
data: {
preference: 10,
locator32: '192.0.2.55'
}
}
L64{
data: {
preference: 10,
locator64High: 0x01234567,
locator64Low: 0x89abcdef
}
}
LP{
data: {
preference: 10,
fqdn: 'locator.example.'
}
}
EUI48 / EUI64{ data: { address: Uint8Array([0x00,0x1A,0x2B,0x3C,0x4D,0x5E]) } } // EUI48
{ data: { address: Uint8Array(8) } } // EUI64
OPENPGPKEY{ data: { key: Uint8Array([...]) } }
ZONEMD{
data: {
scheme: 1,
hashAlgorithm: 1,
digest: Uint8Array([...])
}
}
TKEY{
data: {
algorithm: 'hmac-sha256.',
inception: 1710000000,
expiration: 1710003600,
mode: 3,
error: 0,
key: Uint8Array([...]),
other: Uint8Array([])
}
}
TSIG{
data: {
algorithm: 'hmac-sha256.',
timeSignedHigh: 0,
timeSignedLow: 1710000123,
fudge: 300,
mac: Uint8Array([...]),
originalId: 0x1234,
error: 0,
otherData: Uint8Array([])
}
}
IXFR, AXFR, ANY{ data: {
raw: Uint8Array([])
}
}
Types preserved as raw (round-trip only): EID, NIMLOC, ATMA, SINK, NINFO, RKEY, TALINK.
{ data: { raw: Uint8Array([ ... ]) } }
The core API is stable, but several enhancements are planned to make dnssec-server even more production-ready:
π‘οΈ DNSSEC optimizations
π New transports
π¦ Operational features
π§ Developer experience
π Future ideas
π‘ Want something added? Open a discussion or file an issue with the tag roadmap. Contributions and proposals are welcome!
Q: I get `` when enabling DNSSEC.
A: Ensure your registrar DS matches your current KSK (keyTag, algorithm, digestType, digest). After rotation, DS must be updated.
Q: Geo routing seems off.
A: Many resolvers donβt send ECS. Fall back to req.remoteAddress or use anycast + regional NS.
Q: How do I serve both IPv4 and IPv6?
A: Push parallel A and AAAA answers.
If this library saves you time, consider supporting:
dnssec-server (helps guide roadmap).For commercial support or consulting, please open an issue titled [support] and weβll coordinate privately.
Apache License 2.0
Copyright Β© 2025 colocohen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
FAQs
π‘ Pure JavaScript authoritative DNS server for Node.js with built-in DNSSEC, dynamic zones, and modern record support.
We found that dnssec-server demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.