Container Escape in Phoenix with Mutual Tls
Container Escape in Phoenix with Mutual Tls — how this specific combination creates or exposes the vulnerability
A container escape in Phoenix involving mutual TLS (mTLS) typically arises when an API endpoint that enforces client certificate authentication is reachable from within a container and is also misconfigured in a way that allows an attacker to pivot from the compromised container to the host or to other services. Even though mTLS provides strong service-to-service authentication, the vulnerability is not in the TLS handshake itself but in how the application uses the authenticated identity and how the container networking is set up.
In a typical Phoenix deployment with mTLS, the endpoint verifies the client certificate, extracts attributes (such as the Common Name or SANs), and uses those attributes to make authorization decisions. If the application treats the mTLS identity as trusted input without additional validation, an attacker who can run code inside a container might be able to spoof a valid client certificate or manipulate the application into trusting a specially crafted certificate chain. Because the container shares the host network stack (or uses overlay networking), a compromised process can attempt connections to localhost or to other containers on the same network, leveraging the mTLS-secured endpoints to move laterally or escalate privileges.
Another angle is the use of unauthenticated or weakly protected endpoints in the same service. Even when most routes require mTLS, an overlooked route in Phoenix (perhaps a health check or a debug endpoint) might skip certificate verification or accept requests without a client certificate. An attacker who escapes the container can probe the local service and interact with these unprotected paths, bypassing the intended mTLS boundary. Additionally, if the Phoenix application resolves service names via DNS inside the cluster and trusts the hostname presented by a service, an attacker could perform service name spoofing or man-in-the-middle within the cluster to trigger insecure fallback behavior.
The interplay between container networking and mTLS configuration also matters. If the Phoenix application binds to 0.0.0.0 inside the container and the network policies are permissive, a compromised container can reach other containers that expose mTLS-protected APIs. The application might log certificate details for observability; if those logs are not properly protected, sensitive identity information can be exfiltrated, aiding further attacks. In short, a container escape in this context leverages weak service boundaries, overly permissive network policies, and insufficient validation of mTLS-derived attributes to move across security layers.
Mutual Tls-Specific Remediation in Phoenix — concrete code fixes
Remediation focuses on tightening how Phoenix consumes client certificates, validating identities, and limiting what the application trusts. Below are concrete code examples using the Plug.SSL and Phoenix.Endpoint configuration, followed by runtime identity validation patterns.
1. Enforce mTLS at the endpoint and require a client certificate
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, http: [transport: :cowboy]
# Enable mTLS by requiring a client certificate
plug Plug.SSL,
enable: true,
cert: "priv/ssl/server.crt",
key: "priv/ssl/server.key",
cacertfile: "priv/ssl/ca_bundle.crt",
verify: :verify_peer,
fail_if_no_peer_cert: true
# Other plugs and pipeline configuration...
plug Plug.Parsers, parsers: [:json]
plug Plug.MethodOverride
plug Plug.Head
plug MyAppWeb.Router
end
2. Validate certificate attributes and avoid trusting raw identity claims
After mTLS verification, explicitly validate the certificate fields and map them to application roles instead of using the certificate subject directly for authorization.
defmodule MyAppWeb.Auth do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case get_peer_cert_subject(conn) do
{:ok, %{common_name: "svc-order", san: ["order.internal"]}} ->
# Map to an internal role, do not trust the CN alone
assign(conn, :service_role, :order_service)
{:ok, %{common_name: "svc-payment", san: ["payment.internal"]}} ->
assign(conn, :service_role, :payment_service)
_ ->
# Reject unknown or malformed identities
halt_unauthorized(conn, "invalid client certificate")
end
end
defp get_peer_cert_subject(conn) do
case conn.status do
# Plug.SSL sets peer_cert_subject when verify is active
_ when conn.private[:ssl_peer_cert] ->
cert = conn.private[:ssl_peer_cert]
# Parse the certificate DN and extract CN/SANs using :public_key
# This is a simplified representation; in practice use :public_key.pkix_decode_cert/2
subject = :public_key.pkix_decode_cert(cert, :otp) |> extract_subject()
{:ok, subject}
_ ->
{:error, :no_cert}
end
end
defp halt_unauthorized(conn, message) do
conn
|> put_status(:forbidden)
|> Phoenix.Controller.json_render(%{error: message})
|> Plug.Conn.halt()
end
end
3. Apply network policies and restrict localhost exposure
Ensure that the Phoenix service does not bind to 0.0.0.0 unnecessarily and that Kubernetes or container network policies restrict traffic to only required peers. Combine this with readiness probes that do not expose sensitive endpoints.
# In a Kubernetes NetworkPolicy (example)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: phoenix-mtls-policy
spec:
podSelector:
matchLabels:
app: phoenix
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: trusted-services
ports:
- protocol: TCP
port: 443
4. Use runtime checks and avoid host resolution pitfalls
When calling other services from Phoenix, validate the server hostname against the certificate rather than relying on DNS names alone. This prevents an attacker who can manipulate DNS within the cluster from redirecting traffic to a malicious service.
defmodule MyAppWeb.HttpClient do
@trusted_ca File.read!("priv/ssl/ca_bundle.crt")
def get_secure(url) do
{host, port} = URI.parse(url) |> Map.take([:host, :port]) |> then(fn %{host: h, port: p} -> {h, p || 443} end)
opts = [
certfile: "priv/ssl/client.crt",
keyfile: "priv/ssl/client.key",
cacertfile: @trusted_ca,
depth: 2,
customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
]
case :ssl.connect(String.to_charlist(host), port, opts, 5_000) do
{:ok, socket} ->
:ssl.connect(socket, opts)
# perform request and close
:ssl.close(socket)
{:error, reason} ->
# handle error securely
{:error, reason}
end
end
end