Identification Failures in Django with Mutual Tls
Identification Failures in Django with Mutual Tls
Identification failures occur when an API or application cannot reliably distinguish one actor from another, leading to incorrect access decisions. In Django, enabling mutual TLS (mTLS) adds a layer of client authentication via client certificates, but misconfiguration can weaken identification rather than strengthen it. When mTLS is implemented inconsistently across Django components, an attacker may present a valid but low-assurance certificate and be incorrectly treated as a high-assurity client, or a missing verification step may allow unauthenticated requests to reach views that rely on certificate-derived user information.
Django does not terminate TLS itself; it typically runs behind a reverse proxy (e.g., Nginx, HAProxy, or an API gateway) that handles mTLS and injects headers such as SSL_CLIENT_VERIFY and SSL_CLIENT_CERT. An identification failure arises when Django trusts these headers without strict validation of the proxy configuration or certificate status. For example, if the proxy is configured to allow optional client certificates and Django assumes a certificate is always present, requests without a cert may be mapped to an authenticated user via REMOTE_USER, creating a spoofing path. Similarly, if the proxy validates client certificates but does not enforce revocation checks (CRL/OCSP), a revoked certificate might still be treated as valid by downstream Django code that reads the certificate subject and maps it to a user.
Another common pattern is mapping the certificate’s Distinguished Name (DN) or Subject Alternative Name (SAN) to a Django user via a custom authentication backend. If this mapping is too permissive—such as using a substring match on the Common Name or failing to validate the certificate fingerprint against a known store—an attacker who can obtain or forge a certificate with a similar DN could be identified as a legitimate user. This is an identification failure because the system incorrectly attributes request context to the wrong principal. Additionally, if session tokens or API keys are issued after the initial mTLS-authenticated request without re-verifying the certificate on subsequent calls, the ongoing identification of the client depends on a weaker mechanism (the token), which may be hijacked.
The interplay between Django’s authentication middleware, proxy-level mTLS, and custom user mapping logic creates a chain where any weak link can lead to incorrect identification. For instance, a misconfigured HTTPSRedirectMiddleware or improper USE_X_FORWARDED_HOST/PROXY_SSL settings can cause mixed HTTP/HTTPS routing, leading to situations where mTLS verification is bypassed. The OWASP API Security Top 10’s Broken Object Level Authorization (BOLA) and Identification/Authentication Failures categories are relevant here: if the API relies on certificate-derived identifiers without enforcing strict verification and revocation, attackers can escalate privileges by exploiting these gaps.
Mutual Tls-Specific Remediation in Django
Remediation centers on strict validation at the proxy and application layers, explicit certificate checks in Django, and robust mapping logic. Below are concrete configurations and code examples for a typical setup where Nginx terminates TLS and performs client certificate verification, then forwards verified identity to Django via headers.
Nginx mTLS configuration
Configure the reverse proxy to require and validate client certificates, and to set trusted headers only when verification succeeds.
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
ssl_client_certificate /etc/nginx/ca.pem;
# Require client certificates and verify against the CA
ssl_verify_client on;
location / {
# Only allow requests with a verified client certificate
proxy_set_header SSL_CLIENT_VERIFY $ssl_client_verify;
proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;
proxy_pass http://django_app;
}
}
Django settings and custom authentication backend
Ensure Django does not rely on insecure headers and implements explicit verification. Define a custom authentication backend that validates the proxy headers and maps certificates to users securely.
# settings.py
import ssl
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'yourapp.middleware.ClientCertMiddleware', # Custom verification
'django.contrib.auth.middleware.RemoteUserMiddleware',
]
# Require proxy to confirm verification; deny if missing
PROXY_SSL_REQUIRED = True
REMOTE_USER_HEADER = 'SSL_CLIENT_VERIFY'
# Optional: restrict accepted certificate fields
REQUIRED_CERT_ISSUER = 'CN=Your CA,O=Your Org,C=US'
REQUIRED_CERT_O = 'Your Org'
Custom middleware to validate headers and map certificates
The middleware checks the proxy’s verification status, ensures the certificate is present, performs revocation-safe checks where possible, and maps the certificate to a Django user without relying on substring matches.
# yourapp/middleware.py
import ssl
from django.conf import settings
from django.contrib.auth import get_user_model
from django.http import HttpResponseForbidden
import re
User = get_user_model()
CERT_HEADER = 'SSL_CLIENT_CERT'
VERIFY_HEADER = 'SSL_CLIENT_VERIFY'
def parse_cert_subject(cert_pem: str) -> dict:
# Very basic PEM extraction; in production use cryptography or OpenSSL bindings
lines = [l.strip() for l in cert_pem.split('\\n') if l.strip() and not l.startswith('-----')]
# This is a placeholder: real parsing should use cryptography.x509
# Example extraction for subject fields (not robust for all DNs)
subject = {}
for line in lines:
if '=' in line:
k, v = line.split('=', 1)
subject[k.strip()] = v.strip()
return subject
class ClientCertMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 1) Ensure the proxy verified the client cert
verify = request.META.get(VERIFY_HEADER, '')
if settings.PROXY_SSL_REQUIRED and verify != 'SUCCESS':
return HttpResponseForbidden('Client certificate not verified')
# 2) Ensure a client certificate is present
cert_pem = request.META.get(CERT_HEADER)
if not cert_pem:
return HttpResponseForbidden('Client certificate missing')
# 3) Parse subject and enforce constraints (example constraints)
subject = parse_cert_subject(cert_pem)
common_name = subject.get('CN', '')
org = subject.get('O', '')
issuer = subject.get('issuer', '') # Parsed issuer would require a proper library
# Example strict checks (adjust to your policy)
if not re.match(r'^[a-zA-Z0-9._-]+$', common_name):
return HttpResponseForbidden('Invalid certificate subject')
if org != settings.REQUIRED_CERT_O:
return HttpResponseForbidden('Organization mismatch')
# 4) Map to a Django user securely; avoid substring matches
# Prefer mapping by certificate fingerprint or a SAN entry (e.g., email)
# Here we use a deterministic mapping via a custom user attribute
try:
user = User.objects.get(cert_common_name=common_name)
except User.DoesNotExist:
return HttpResponseForbidden('User not registered for this certificate')
# 5) Attach user if valid
request.user = user
return self.get_response(request)
Additional hardening
- Enable revocation checks at the proxy (e.g., ssl_crl in Nginx) and consider OCSP stapling to reduce reliance on stale CRLs.
- Do not use the certificate subject alone for authorization; treat mTLS as authentication and enforce per-view or per-action authorization using Django’s permission system.
- Rotate CA and certificate material regularly, and automate renewal to avoid long-lived certificates.
- Log verification failures and certificate metadata for audit, but avoid logging full certificate bodies in plain text.