Skip to content

Security Features

Ferrous DNS includes several security mechanisms to protect your network from DNS-based attacks and data exposure.

This page covers the dashboard and API surface plus the per-feature reference. For the resolver-side hardening — upstream response validation, 0x20, source-port rotation, DNSSEC downgrade detection — and an explicit list of what is not covered yet, see Security Hardening.


Authentication

Ferrous DNS provides session-based authentication to protect the dashboard and REST API.

First-Run Setup

On first launch (when no password is configured), Ferrous DNS shows a setup wizard. Set the admin password via the web UI or CLI before the server accepts API requests.

Session-Based Login

Users authenticate with username and password via the login page. On success, a session cookie (ferrous_session) is issued.

POST /api/auth/login
Content-Type: application/json

{
  "username": "admin",
  "password": "your-password"
}
Option Description
Remember Me Extends session lifetime from session_ttl_hours (default 24h) to remember_me_days (default 30 days)
Rate Limiting After login_rate_limit_attempts failed attempts (default 5), login is locked for login_rate_limit_window_secs (default 900s / 15 min)

Auth Guard

All API endpoints are protected by the auth guard middleware, except:

  • GET /api/auth/status — check if auth is enabled
  • POST /api/auth/setup — first-run password setup
  • POST /api/auth/login — login
  • POST /api/auth/logout — logout
  • GET /api/health — health check

Session Management

View and revoke active sessions from Settings > Security or via the API:

GET /api/auth/sessions
DELETE /api/auth/sessions/{id}

Password Change

Change the admin password from Settings > Security or via:

POST /api/auth/change-password
Content-Type: application/json

{
  "current_password": "old-password",
  "new_password": "new-password"
}

Background Cleanup

A background task runs periodically to prune expired sessions from the database.


API Tokens

Named API tokens provide programmatic access to the Ferrous DNS API without requiring a session login. Tokens are ideal for automation scripts, monitoring integrations, and third-party tools.

Token Authentication

Include the token in the X-Api-Key header:

X-Api-Key: your-api-token

API tokens and session cookies are both valid authentication methods. The auth guard accepts either.

Token Management

GET    /api/api-tokens          # List all tokens (only prefix shown)
POST   /api/api-tokens          # Create a new token
PUT    /api/api-tokens/{id}     # Update token name or key
DELETE /api/api-tokens/{id}     # Delete a token

Token storage

Tokens are stored as SHA-256 hashes. The full token is only returned once at creation time — save it immediately.

Import Custom Keys

You can import existing API keys (e.g., from a Pi-hole migration) via PUT /api/api-tokens/{id} with a custom key value.


Auth Configuration

ferrous-dns.toml
[auth]
enabled = true                          # Enable authentication globally
session_ttl_hours = 24                  # Session lifetime without "Remember Me"
remember_me_days = 30                   # Session lifetime with "Remember Me"
login_rate_limit_attempts = 5           # Max failed attempts before lockout
login_rate_limit_window_secs = 900      # Lockout window (15 min)

[auth.admin]
username = "admin"                      # Admin username
password_hash = ""                      # Argon2id hash (set via setup wizard or CLI)
Field Type Default Description
enabled bool true Enable or disable authentication globally
session_ttl_hours int 24 Default session lifetime in hours
remember_me_days int 30 Extended session lifetime when "Remember Me" is checked
login_rate_limit_attempts int 5 Max failed login attempts before lockout
login_rate_limit_window_secs int 900 Duration of lockout window in seconds
username str admin Admin username
password_hash str "" Argon2id password hash (set via setup wizard or CLI)

Setting the password hash

Use the setup wizard on first run to set the password interactively. The Argon2id hash is written to the config file automatically.

See Auth Configuration for full details.


DNSSEC Validation

DNSSEC (DNS Security Extensions) validates that DNS responses are authentic and have not been tampered with in transit.

[dns]
dnssec_mode = "Strict"

dnssec_mode has three levels:

  • Off — no validation; the DNSSEC OK (DO) bit is not requested upstream.
  • Permissive (default) — every upstream response is validated and tagged (Secure/Insecure/Bogus/Indeterminate) in the query log, but the response is delivered unchanged.
  • Strict — a response that validates as Bogus is rejected with SERVFAIL (+ Extended DNS Error code 6), preventing forged responses from reaching clients.

The AD (Authenticated Data) bit is set only when a response validates as Secure and the client did not set the CD (Checking Disabled) bit. A client that sets CD opts out of enforcement — Strict mode will not SERVFAIL its queries, so it can do its own validation. Enforcement is fail-open: only a proven Bogus result is rejected; validation errors and timeouts are served.

The queries_dnssec_bogus counter (dashboard + Prometheus ferrousdns_queries_dnssec_bogus) tracks how many responses failed validation.

Standards: RFC 4035, RFC 6840, RFC 8914 (EDE)

Performance impact

DNSSEC validation adds a small overhead on cache misses (signature verification + per-zone DNSKEY/DS lookups, cached). Cache hits have zero DNSSEC overhead. Disable with dnssec_mode = "Off" only for maximum-throughput benchmarking.


Malware Detection

Ferrous DNS includes built-in DNS tunneling detection, DNS rebinding protection, and NXDomain hijack detection. See the dedicated Malware Detection page for full details, real-world attack examples, configuration reference, and comparison with other DNS servers.


PTR Block for Private Ranges

Prevent information leakage via reverse DNS lookups on private IP ranges:

[dns]
block_private_ptr = true

When enabled, PTR queries for RFC-1918 addresses that are not in the local records are blocked. This prevents external DNS leakage of your internal network topology.


Non-FQDN Query Blocking

Block DNS queries for names that are not fully qualified domain names (FQDNs):

[dns]
block_non_fqdn = true

Non-FQDN queries (e.g. myserver without a domain suffix) can expose internal network information when forwarded to external resolvers. Blocking them keeps internal names local.


PROXY Protocol v2

When Ferrous DNS is deployed behind a load balancer, PROXY Protocol v2 restores accurate client IPs for logging, client detection, and per-group policies:

[server]
proxy_protocol_enabled = true

Supported load balancers: HAProxy, AWS NLB, nginx (stream module), Traefik

Danger

Only enable when a trusted load balancer always injects the PROXY header. Without a load balancer in front, all TCP connections will fail.


HTTPS for Web UI

Ferrous DNS can serve the dashboard and REST API over HTTPS, encrypting all traffic between your browser and the server.

How It Works

When HTTPS is enabled, the web server uses a single port (default 8080) that automatically detects the protocol:

  • TLS connections (browsers accessing https://) are served normally over HTTPS
  • Plain HTTP connections receive a 301 Moved Permanently redirect to https://

This means you never need to configure separate HTTP and HTTPS ports.

Configuration

ferrous-dns.toml
[server.web_tls]
enabled       = false               # Enable HTTPS for the dashboard and API
tls_cert_path = "/data/cert.pem"    # Path to PEM certificate
tls_key_path  = "/data/key.pem"     # Path to PEM private key
Option Type Default Description
enabled bool false Enable HTTPS for the web server
tls_cert_path str /data/cert.pem Path to the PEM-encoded TLS certificate
tls_key_path str /data/key.pem Path to the PEM-encoded TLS private key

Graceful fallback

If enabled = true but the certificate files are missing at startup, the server logs a warning and falls back to plain HTTP.

Managing Certificates via the UI

Navigate to Settings > Security > HTTPS / TLS to:

  • Enable/disable HTTPS with a toggle
  • View certificate status — subject, expiration date, and validity
  • Upload certificates — drag and drop PEM certificate and key files
  • Generate a self-signed certificate — instant HTTPS with one click (browsers will show a security warning, but the connection is encrypted)

Quick setup

Click Generate Self-Signed Certificate for immediate HTTPS without needing external certificates. For production, use Let's Encrypt or your CA.

TLS API Endpoints

GET  /api/tls/status              # Certificate status (exists, valid, subject, expiration)
POST /api/tls/upload              # Upload cert + key (multipart/form-data)
POST /api/tls/generate?force=true # Generate self-signed certificate

Restart required

Changing HTTPS settings requires a server restart to take effect. The UI shows a "Restart Required" banner after saving.


Encrypted DNS Transports

Encrypting DNS traffic prevents:

  • ISP surveillance — your DNS queries are not visible to your ISP
  • Man-in-the-middle attacks — responses cannot be forged in transit
  • DNS poisoning — combined with DNSSEC for end-to-end verification

See Encrypted DNS for setup.


DNS Rate Limiting

Ferrous DNS includes a token-bucket rate limiter that throttles abusive clients per subnet, protecting the server from query floods without affecting legitimate traffic.

How It Works

Each client subnet (default /24 for IPv4, /48 for IPv6) gets an independent token bucket. Tokens refill at the configured queries_per_second rate, up to the burst_size capacity. When a subnet exhausts its tokens, queries are either refused (REFUSED response code) or slipped (TC=1 truncated response forcing a TCP retry).

Configuration

ferrous-dns.toml
[dns.rate_limit]
enabled                    = true
queries_per_second         = 1000     # sustained QPS per subnet
burst_size                 = 500      # token bucket capacity
ipv4_prefix_len            = 24       # /24 groups the home LAN
ipv6_prefix_len            = 48       # /48 standard home delegation
whitelist                  = ["127.0.0.0/8", "::1/128", "10.0.0.0/8"]
nxdomain_per_second        = 50       # separate stricter budget for NXDOMAIN
slip_ratio                 = 2        # every 2nd rate-limited response is TC=1
dry_run                    = false    # true = log only, don't refuse
stale_entry_ttl_secs       = 300      # evict idle subnet buckets after 5 min
Option Default Description
enabled false Master switch for rate limiting
queries_per_second 1000 Sustained token refill rate per subnet
burst_size 500 Maximum tokens (allows short bursts above QPS)
ipv4_prefix_len 24 IPv4 subnet grouping prefix length
ipv6_prefix_len 48 IPv6 subnet grouping prefix length
whitelist [] CIDRs that bypass rate limiting entirely
nxdomain_per_second 50 Separate, stricter budget for NXDOMAIN responses
slip_ratio 0 Every Nth rate-limited response sends TC=1 instead of REFUSED. 0 = disabled
dry_run false Log rate-limit events without refusing queries
stale_entry_ttl_secs 300 Seconds before an idle subnet bucket is evicted

TC=1 Slip Mechanism

When slip_ratio is set (e.g. 2), every Nth rate-limited UDP response is sent as a truncated response (TC=1 flag set) instead of REFUSED. This forces the client to retry over TCP, which:

  • Verifies the client is a legitimate resolver (not a spoofed-source flood)
  • Allows real clients to still get answers via TCP even when rate-limited on UDP
  • Follows the same approach used by NSD and BIND

NXDOMAIN Budget

The nxdomain_per_second setting provides a separate, stricter budget for NXDOMAIN responses. This catches malware and IoT devices that probe many random subdomains while leaving the general query budget unaffected.

Dry-Run Mode

Set dry_run = true to log rate-limit events without actually refusing queries. This is useful for calibrating thresholds before enforcing limits in production. Rate-limited queries appear in the query log with status RATE_LIMITED and in the dashboard stats.

Recommended first deployment

Enable rate limiting with dry_run = true for 24-48 hours. Check the dashboard for false positives, then switch to dry_run = false once thresholds are validated.


TCP/DoT/DoQ Connection Limiting

Per-IP connection limits protect against TCP, DoT, and DoQ connection exhaustion:

ferrous-dns.toml
[dns.rate_limit]
tcp_max_connections_per_ip = 30    # max concurrent TCP DNS connections per IP
dot_max_connections_per_ip = 15    # max concurrent DoT connections per IP
doq_max_connections_per_ip = 15    # max concurrent DoQ connections per IP
Option Default Description
tcp_max_connections_per_ip 30 Max concurrent TCP connections per IP. 0 = unlimited
dot_max_connections_per_ip 15 Max concurrent DoT connections per IP. 0 = unlimited
doq_max_connections_per_ip 15 Max concurrent DoQ connections per IP. 0 = unlimited

Connections that exceed the limit are immediately closed. The connection counter is automatically decremented when a connection closes, preventing resource leaks.

Because a single QUIC connection multiplexes many streams, DoQ adds two further per-connection safeguards on top of the per-IP limit above (both fixed, not configurable):

  • Concurrent stream cap — each DoQ connection may have at most 100 in-flight query streams at once, so one admitted connection cannot open unbounded streams.
  • Stream read timeout — a stream that opens but does not deliver its complete length-prefixed query within 5 seconds is dropped, closing the slow-loris vector that QUIC keep-alive would otherwise keep alive.

Extended DNS Errors (RFC 8914)

Extended DNS Errors (EDE) is a DNS protocol extension defined in RFC 8914 that attaches a structured error code and an optional human-readable description to DNS error responses. This gives DNS clients and resolvers precise, machine-readable information about why a query failed — instead of a bare SERVFAIL, REFUSED, or NXDOMAIN.

How It Works

When Ferrous DNS rejects or fails to resolve a query, it appends an EDE option to the OPT record of the DNS response. The option contains two fields:

  • info_code — a standardised 16-bit code defined by the RFC (e.g. 15 for BLOCKED, 6 for DNSSEC_BOGUS)
  • extra_text — a short human-readable string describing the specific reason (e.g. "DGA domain detected", "upstream connection timed out")

EDE is encoded as EDNS option code 15 inside the OPT record and is fully transparent to clients that do not understand it — they simply ignore the unknown option.

EDNS requirement

Ferrous DNS only includes an EDE option when the client itself advertised EDNS support by including an OPT record in its query. Clients that do not send EDNS queries receive the same error response as before, without the EDE option.

No configuration is required. EDE is always active.

Error Code Reference

EDE Name Code Triggered by
DNSSEC_BOGUS 6 DnssecValidationFailed — DNSSEC signature validation failed
DNSKEY_MISSING 9 InsecureDelegation — insecure DNSSEC delegation
BLOCKED 15 Blocked — domain matched the blocklist
BLOCKED 15 DgaDomainDetected — DGA domain identified by statistical analysis
BLOCKED 15 FilteredQuery — query filtered by group policy or schedule
PROHIBITED 18 DnsTunnelingDetected — DNS tunneling activity detected
PROHIBITED 18 DnsRateLimited — client subnet exceeded the rate limit budget
NO_REACHABLE_AUTHORITY 22 QueryTimeout — upstream query timed out before responding
NO_REACHABLE_AUTHORITY 22 TransportNoHealthyServers — no upstream server is currently healthy
NO_REACHABLE_AUTHORITY 22 TransportAllServersUnreachable — all configured upstream servers are unreachable
NETWORK_ERROR 23 TransportTimeout — upstream TCP/TLS connection timed out
NETWORK_ERROR 23 TransportConnectionRefused — upstream server actively refused the connection
NETWORK_ERROR 23 TransportConnectionReset — upstream server reset the TCP/TLS connection

Debugging with EDE

DNS clients like dig display EDE data automatically. Use dig +edns=0 example.com @your-server to confirm EDNS support, then inspect the OPT PSEUDOSECTION in the response for the EDE info code and extra text.

Scope

EDE is applied in both DNS code paths:

  • Standard path — the Hickory handle_request handler that processes TCP, DoT, DoH, DoQ, and H3 queries
  • Raw UDP fallback path — the handle_raw_udp_fallback handler for high-throughput UDP resolution

This ensures consistent error reporting regardless of the transport protocol used by the client.


DNS Cookies (RFC 7873)

DNS Cookies (RFC 7873) are a lightweight, stateless anti-spoofing mechanism for UDP DNS. Because UDP has no handshake, a resolver cannot normally verify that a query's source IP is genuine. An attacker can forge queries from a victim's address, causing the server to flood the victim with large responses (amplification attack) or inject a forged answer before the real upstream replies (cache poisoning via spoofing). DNS Cookies solve this by binding each client–server pair with a cryptographic token that cannot be forged without knowledge of the server's secret key.

How It Works

The handshake proceeds in three stages:

Stage 1 — Bootstrap (first query from this client)

  Client → Server:  query  [EDNS OPT: client_cookie=<8-byte random>]
  Server → Client:  answer [EDNS OPT: client_cookie=<echo> | server_cookie=<HMAC>]

  The server computes:
      server_cookie = HMAC-SHA256(secret, client_ip ‖ client_cookie)[0..8]
  The client stores this server_cookie for future queries.

Stage 2 — Valid cookie (subsequent queries)

  Client → Server:  query  [EDNS OPT: client_cookie=<same> | server_cookie=<cached>]
  Server validates:  recomputes HMAC and compares → match → trusted client
  Server → Client:  answer [EDNS OPT: refreshed server_cookie]

Stage 3 — Secret rotation

  After secret_rotation_secs, the server starts signing with a new secret.
  The previous secret remains accepted for one full rotation window so that
  in-flight clients are not abruptly rejected — they receive a new cookie in
  the response and update their cache silently.

Protection Against UDP Spoofing

When a client has negotiated a valid server cookie, the server can confirm with high confidence that subsequent queries originate from the same IP. An attacker spoofing the victim's source address does not know the server's HMAC secret and cannot reproduce the correct server cookie, so:

  • Amplification attacks — spoofed queries without a valid cookie are refused in permissive mode (no resolution, minimal response) or rejected with REFUSED in strict mode, preventing the server from being used as a reflector.
  • Cache poisoning — an attacker cannot inject a forged response without matching the cookie the client presented, dramatically narrowing the race window.

Integration with EDE Code 25

In strict mode (require_valid_cookie = true), queries with an absent or invalid server cookie are rejected with:

  • RCODE REFUSED
  • EDE info_code 25Bad or Missing EDNS Cookie (RFC 8914 § 4.25)

This gives RFC-aware clients a precise machine-readable reason for the rejection, enabling automatic retry with a freshly bootstrapped cookie.

Fast Path Behaviour

Cache hits bypass the DNS Cookie guard entirely — a query served from L1 or L2 cache does not incur any cookie verification overhead. Cookie validation runs only on cache misses, keeping the hot path at zero additional cost.

Configuration

[dns.dns_cookies]
enabled               = true    # on by default
server_secret         = ""      # empty = ephemeral secret (not for production)
secret_rotation_secs  = 3600    # rotate every hour
require_valid_cookie  = false   # permissive mode (recommended default)

The section is nested under [dns]

A top-level [dns_cookies] table is silently ignored — the settings look applied but the defaults stay in force. If server_secret seems not to take effect, check the nesting first.

See DNS Cookies configuration for the full option reference and strict-mode setup.


Upcoming Security Features

The following are planned for future releases:

Feature Description
Read-Only Mode Disable config changes via a flag
API / login throttling login_rate_limit_attempts and login_rate_limit_window_secs are accepted and persisted today, but nothing enforces them — see Security Hardening
EDNS Client Subnet handling Strip client ECS from upstream queries by default, with optional injection (RFC 7871)
Enforcing DS-denial checks Turn the current downgrade detection into an opt-in strict mode
RFC 5011 trust anchor rollover Track root key rolls without a new release

Current Security Posture

Mechanism Status
DNSSEC validation ✅ Active — permissive by default, strict to SERVFAIL on Bogus
DNSSEC downgrade (DS denial) ⚠ Detection only — fail-opens counted at /api/dnssec/stats
Upstream response validation (txid + question + source) ✅ Active on every transport
Upstream source-port rotation ✅ Active
0x20 QNAME case randomization ❔ Opt-in — qname_case_randomization, off by default
DNS tunneling detection ✅ Active
DNS rebinding protection ✅ Active
NXDomain hijack detection ✅ Active
DNS Cookies (RFC 7873) ✅ Active
Extended DNS Errors (RFC 8914) ✅ Active
Encrypted upstream (DoH/DoT/DoQ) ✅ Active
Server-side DoT/DoH ✅ Active
PROXY Protocol v2 ✅ Active
Dashboard authentication ✅ Active
API token authentication ✅ Active
HTTPS dashboard ✅ Active
DNS rate limiting ✅ Active
TCP/DoT connection limiting ✅ Active
TOTP / 2FA (authenticator app) ✅ Active
Passkeys / WebAuthn (second factor + passwordless) ✅ Active
API request rate limiting ❌ Not implemented
Login attempt throttling / lockout ❌ Not implemented (config keys accepted but inert)