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 enabledPOST /api/auth/setup— first-run password setupPOST /api/auth/login— loginPOST /api/auth/logout— logoutGET /api/health— health check
Session Management¶
View and revoke active sessions from Settings > Security or via the API:
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:
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¶
[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.
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 asBogusis rejected withSERVFAIL(+ 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:
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):
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:
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 Permanentlyredirect tohttps://
This means you never need to configure separate HTTP and HTTPS ports.
Configuration¶
[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¶
[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:
[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.15forBLOCKED,6forDNSSEC_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_requesthandler that processes TCP, DoT, DoH, DoQ, and H3 queries - Raw UDP fallback path — the
handle_raw_udp_fallbackhandler 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
REFUSEDin 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
25— Bad 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 | permissive by default, strict to SERVFAIL on Bogus |
| DNSSEC downgrade (DS denial) | /api/dnssec/stats |
| Upstream response validation (txid + question + source) | |
| Upstream source-port rotation | |
| 0x20 QNAME case randomization | qname_case_randomization, off by default |
| DNS tunneling detection | |
| DNS rebinding protection | |
| NXDomain hijack detection | |
| DNS Cookies (RFC 7873) | |
| Extended DNS Errors (RFC 8914) | |
| Encrypted upstream (DoH/DoT/DoQ) | |
| Server-side DoT/DoH | |
| PROXY Protocol v2 | |
| Dashboard authentication | |
| API token authentication | |
| HTTPS dashboard | |
| DNS rate limiting | |
| TCP/DoT connection limiting | |
| TOTP / 2FA (authenticator app) | |
| Passkeys / WebAuthn (second factor + passwordless) | |
| API request rate limiting | |
| Login attempt throttling / lockout |