Dangling Dns in Django with Mutual Tls
Dangling Dns in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
A dangling DNS record occurs when a hostname (e.g., internal.service.example.com) still points to an IP that no longer hosts the intended service. In Django, this can surface when the application performs hostname-based routing or conditional logic, or when a dependency uses DNS to locate a backend. When mutual TLS (mTLS) is enforced, the client presents a certificate, and the server validates it. The server may also perform hostname verification against the certificate’s Subject Alternative Name (SAN) or Common Name (CN). If a dangling DNS entry resolves to a server that does not present a matching certificate, or if the dangling host is replaced by a rogue server that presents a valid but unexpected certificate, Django’s behavior can diverge from policy:
- Hostname verification may pass if the dangling DNS name resolves to a server controlled by an attacker and that server presents a certificate with a matching SAN/CN, enabling TLS-terminated traffic to be forwarded to a malicious endpoint.
- Django’s ALLOWED_HOSTS may not include the dangling name, but if the application uses request.get_host() or relies on HTTP Host header routing, an attacker can supply the dangling name to bypass host-based checks.
- If mTLS is enforced at a reverse proxy or load balancer and Django trusts the proxy, the proxy may route requests to a backend inferred from a dangling DNS name, exposing internal service boundaries or causing the application to interact with an unintended service.
Django’s security posture under mTLS depends on strict hostname alignment, certificate validation, and network topology awareness. A dangling DNS record violates the assumption that DNS names are stable and authoritative. An attacker who can register or inherit the dangling IP can intercept or manipulate traffic, especially if Django’s hostname checks are incomplete or if the mTLS configuration does not tightly couple certificate validation with DNS expectations.
Mutual Tls-Specific Remediation in Django — concrete code fixes
To mitigate dangling DNS in a Django application with mTLS, enforce strict hostname verification, validate certificates before establishing trust, and avoid relying on DNS for security-critical routing. Below are concrete, secure configurations and code examples.
1. Enforce Hostname Verification in mTLS
Configure your mTLS setup so that the server validates the client certificate and verifies that the certificate matches the expected hostname. Use django-allowed-hosts and ssl_match_hostname where applicable, and ensure your reverse proxy or ASGI server does not skip hostname checks.
# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
# Use a custom middleware to validate client certificate subject/SAN against allowed patterns
import ssl
from django.utils.deprecation import MiddlewareMixin
class MutualTlsHostnameVerificationMiddleware(MiddlewareMixin):
ALLOWED_HOST_PATTERNS = ['api.example.com', 'service.example.com']
def process_request(self, request):
# Assuming the client cert is stored in request.META by the web server (e.g., SSL_CLIENT_VERIFY)
cert_hostname = request.META.get('SSL_CLIENT_VERIFY_HOST')
if cert_hostname:
if cert_hostname not in self.ALLOWED_HOST_PATTERNS:
raise SuspiciousOperation(f'Hostname mismatch: {cert_hostname}')
else:
# Fail closed if no hostname is provided
raise SuspiciousOperation('Missing client certificate hostname')
2. Validate Server Certificates Against Known DNS
When Django acts as an mTLS client (e.g., calling downstream services), pin the expected hostname and validate the server certificate against it. Use the requests library with a custom adapter or urllib3’s match_hostname.
# utils/mtls_client.py
import ssl
import socket
from urllib3.util.ssl_match_hostname import match_hostname, CertificateError
def verified_session():
ctx = ssl.create_default_context(cafile='/path/to/ca-bundle.pem')
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
return ctx
def call_protected_service(url, cert_path, key_path):
ctx = verified_session()
with socket.create_connection((url, 443)) as sock:
with ctx.wrap_socket(sock, server_hostname=url) as ssock:
# match_hostname is called inside wrap_socket when check_hostname=True
pass
# Proceed with requests or urllib using the SSL context
3. Remove DNS-Dependent Routing in Django
Avoid using dynamic hostnames for routing decisions. Instead, use explicit paths, feature flags, or service discovery with static, validated endpoints. If you must use DNS, cache and validate the resolved IP against an allowlist and re-validate on each request. Example of unsafe pattern to avoid:
# settings.py — do not do this
BACKEND_HOST = os.getenv('BACKEND_DNS', 'internal.service.example.com')
# views.py — unsafe dynamic resolution
from django.http import HttpResponse
import socket
def unsafe_view(request):
ip = socket.gethostbyname(BACKEND_HOST)
# forward request to ip — vulnerable to dangling DNS
...
Prefer a static configuration with periodic, controlled re-validation:
# settings.py
ALLOWED_BACKEND_IPS = {'192.0.2.10', '198.51.100.20'}
# health check job that updates a local allowlist after DNS validation
4. Secure Reverse Proxy and Upstream Communication
If a reverse proxy handles mTLS and forwards to Django, ensure the proxy validates client certificates and does not route based on untrusted DNS. Configure the proxy to use strict SNI and hostname checks, and set Django’s ALLOWED_HOSTS to only the names the proxy uses when forwarding.
# Example Nginx mTLS configuration
ssl_verify_client on;
ssl_client_certificate /etc/ssl/ca.pem;
proxy_set_header X-SSL-Verify $ssl_client_verify;
location /api/ {
# Use a fixed upstream; avoid DNS-based round-robin for security-critical paths
proxy_pass https://fixed-upstream.example.com;
proxy_ssl_server_name on;
proxy_ssl_name fixed-upstream.example.com;
}