Container Escape in Rails with Mutual Tls
Container Escape in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability
A container escape in a Rails application that uses mutual TLS (mTLS) typically occurs when runtime protections are misaligned with the strict transport security that mTLS enforces. mTLS ensures both client and server present valid certificates, which reduces the risk of on-path attacks but does not inherently limit what a compromised request can do inside the container. If the Rails process runs with elevated privileges or has access to the host filesystem, an attacker who tricks the application into forwarding or relaying traffic can exploit Rails components or system calls to break out of the container boundary.
For example, an attacker might leverage an unrestricted Net::HTTP call or an unvalidated redirect to reach the container’s host metadata service (e.g., 169.254.169.254), which is often accessible from within containers. Even with mTLS verifying inbound connections, Rails code that initiates outbound requests without network segmentation can become a channel for escape. Similarly, if the container’s volume mounts expose sensitive host paths, Rails code that reads or writes those paths may inadvertently provide an avenue to reach host binaries or configuration files. Without network policies that restrict egress and runtime controls that limit filesystem exposure, mTLS secures the API channel but does not prevent an attacker from leveraging Rails internals or system utilities to move laterally or escalate within the host environment.
In this context, middleBrick’s scans are valuable because they test the unauthenticated attack surface and include checks such as SSRF and Unsafe Consumption. An OpenAPI/Swagger spec analyzed by middleBrick can reveal outbound HTTP client definitions or permissive proxy settings that, when combined with runtime behaviors, may indicate a path toward container escape. By cross-referencing spec definitions with runtime findings, middleBrick can highlight endpoints that perform external requests or interact with cloud metadata, helping you identify risky integrations before they are exercised in production.
Consider a scenario where a Rails controller uses mTLS for inbound requests but then calls an external service without constraining the target URL. If an attacker can control a parameter that influences the request target, they may direct the Rails app to the container host or metadata service. Even with client certificates enforced on incoming connections, the outbound call may lack certificate pinning or network segmentation, allowing the container to reach internal endpoints that should remain isolated. This illustrates how mTLS on ingress does not automatically protect egress, and how Rails code must explicitly limit destinations and validate responses to reduce the container escape risk.
Mutual Tls-Specific Remediation in Rails — concrete code fixes
To harden a Rails app using mutual TLS, focus on strict certificate validation, controlled egress, and least-privilege runtime configurations. Below are concrete code examples that demonstrate how to implement mTLS correctly and reduce the container escape surface.
1. Enforce client and server certificate validation in Rails
Configure Rails to require and verify client certificates for incoming requests. This example uses a Rack middleware approach to validate the client certificate against a trusted CA before the request reaches the controller layer.
# config/initializers/middleware.rb
class MtlsVerification
def initialize(app)
@app = app
end
def call(env)
cert = env["SSL_CLIENT_CERT"]
unless cert&.present?
return [403, { "Content-Type" => "text/plain" }, ["Client certificate required"]]
end
store = OpenSSL::X509::Store.new
store.add_file(Rails.root.join("certs", "ca.pem").to_s)
store.verify_flags = OpenSSL::X509::V_FLAG_PARTIAL_CHAIN
begin
cert_store = OpenSSL::X509::Store.new
cert_store.add_cert(cert)
unless cert_store.verify(store).any?
return [403, { "Content-Type" => "text/plain" }, ["Certificate verification failed"]]
end
rescue OpenSSL::X509::StoreError => e
return [403, { "Content-Type" => "text/plain" }, ["Certificate error: #{e.message}"]]
end
@app.call(env)
end
end
Rails.application.config.middleware.use MtlsVerification
2. Secure outbound HTTP calls with pinned certificates and restricted targets
When Rails makes outbound requests (for example to integrate with other services), enforce certificate pinning and avoid passing unchecked user input into the URL. This example uses Net::HTTP with explicit CA verification and a strict whitelist of allowed hosts.
# app/services/secure_client.rb
class SecureClient
ALLOWED_HOSTS = ["api.trusted.example.com", "internal.service.local"]
def self.get(path, client_cert_path, client_key_path)
uri = URI.parse(path)
raise "Host not allowed" unless ALLOWED_HOSTS.include?(uri.host)
cert = OpenSSL::X509::Certificate.new(File.read(client_cert_path))
key = OpenSSL::PKey::RSA.new(File.read(client_key_path))
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.cert = cert
http.key = key
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.ca_file = Rails.root.join("certs", "ca.pem").to_s
http.ssl_version = "TLSv1_2"
request = Net::HTTP::Get.new(uri.request_uri)
http.request(request)
end
end
3. Limit filesystem and host access in the container
Even with mTLS enforced, ensure the Rails container does not mount sensitive host paths and does not run as root. Use read-only filesystems where possible and drop Linux capabilities. In your container definition, restrict egress to known service endpoints and block access to the host metadata service. These runtime controls complement mTLS by reducing what an attacker can reach if Rails is compromised.
4. Validate and sanitize inputs that influence external interactions
Never allow user-controlled data to directly form URLs or command arguments. Use strong parameter validation and avoid methods that concatenate raw input into system commands or HTTP calls. This prevents SSRF-style techniques that could be used to pivot from the Rails app to the container host.
# app/controllers/api_proxies_controller.rb
class ApiProxiesController < ApplicationController
before_action :validate_target_url
def show
response = SecureClient.get(params[:url], "/path/to/client.pem", "/path/to/client.key")
render plain: response.body
end
private
def validate_target_url
uri = URI.parse(params[:url])
unless uri.host&.end_with?("trusted.example.com")
render plain: "Invalid target", status: :bad_request
throw(:abort)
end
rescue URI::InvalidURIError
render plain: "Invalid URL", status: :bad_request
throw(:abort)
end
end