Container Escape in Django with Mutual Tls
Container Escape in Django with Mutual Tls — how this specific combination creates or exposes the vulnerability
A container escape in a Django application that uses mutual TLS (mTLS) typically arises when transport-layer protections are mistakenly treated as sufficient for full request validation. mTLS ensures that both the client and the server present valid certificates, which strongly authenticates the peer. However, authentication is not authorization, and it does not constrain what a verified client can do once a connection is accepted.
In a containerized deployment, the runtime boundary is enforced by the container runtime and orchestrator rather than by the application or its TLS settings. If the Django app runs with elevated privileges or mounts sensitive host paths, an authenticated client can exploit business logic or integration points to break out of the container. For example, an endpoint that dynamically generates configuration files and writes them to a volume mounted from the host may allow path traversal or command injection when fed by a compromised but mTLS-authenticated client.
Another scenario involves service-to-service communication where mTLS is enforced at the sidecar or ingress, but the Django app receives requests over plain HTTP internally. If the app trusts internal network boundaries implicitly, an attacker who compromises a sidecar or another container on the same network can relay or modify requests to the Django process, leveraging container networking to reach host-level services or the filesystem. The mTLS layer may still validate the sidecar’s identity, but the application layer does not revalidate context, leading to confused deputy–style issues where trusted internal channels are abused.
SSRF-like patterns also apply: if the Django app makes outbound requests to URLs provided by the client, even when mTLS is used inbound, the app can be tricked into reaching internal services that are only reachable from within the container network or host. Combined with container escape primitives such as procfs mounts or Docker socket exposure, this can allow an authenticated request to read host files or execute commands on the host. The key takeaway is that mTLS secures the channel, not the semantics of file paths, process execution, or host resource access; those must be enforced separately in the application and runtime configuration.
Mutual Tls-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring that mTLS is treated as a strong identity signal rather than a complete security boundary. In Django, you should validate the client certificate subject or SAN to enforce per-client permissions, avoid implicit trust in internal traffic, and ensure that any file or subprocess operations use strict allowlists.
First, configure Django to require and inspect client certificates. When terminating TLS at a reverse proxy, pass the client certificate information to Django via headers, and validate them explicitly. Below is an example using a custom middleware that checks the presented certificate’s Common Name and denies access if it is not in an allowlist:
import ssl
from django.http import HttpResponseForbidden
class MutualTlsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
cert_subject = request.META.get('SSL_CLIENT_SDN')
if not cert_subject:
return HttpResponseForbidden('Client certificate required')
allowed_cns = {'api-service', 'admin-client'}
cn = self._extract_cn(cert_subject)
if cn not in allowed_cns:
return HttpResponseForbidden('Unauthorized client')
return self.get_response(request)
def _extract_cn(self, subject):
# Example subject: '/CN=api-service/O=Example'
for part in subject.split('/'):
if part.startswith('CN='):
return part[3:]
return ''
Second, avoid relying on network-level segmentation alone. Even when mTLS is enforced, apply strict host and port allowlists for any outbound HTTP calls the Django app makes. Use a requests session with certificate verification and pin server certificates where appropriate:
import requests
from requests.exceptions import SSLError
session = requests.Session()
session.verify = '/path/to/ca-bundle.pem'
session.cert = ('/path/client.pem', '/path/client-key.pem')
def safe_post(url, data):
if not url.startswith('https://api.trusted.example.com/'):
raise ValueError('Unexpected host')
try:
resp = session.post(url, json=data, timeout=5)
resp.raise_for_status()
return resp.json()
except SSLError:
raise
Third, restrict filesystem and subprocess access in container runtime policies. Ensure that the Django container does not mount the Docker socket or host procfs unless strictly necessary, and use read-only filesystems where possible. Combine this with code-level validation for any file paths derived from client-supplied data to prevent path traversal, even when the request is mTLS-authenticated:
import os
from django.core.exceptions import SuspiciousOperation
def write_user_file(base_dir, filename, content):
safe_dir = os.path.realpath(base_dir)
target = os.path.realpath(os.path.join(safe_dir, filename))
if os.path.commonpath([safe_dir, target]) != safe_dir:
raise SuspiciousOperation('Invalid path')
with open(target, 'w') as f:
f.write(content)