Api Key Exposure in Rails with Mutual Tls
Api Key Exposure in Rails with Mutual Tls
Mutual Transport Layer Security (mTLS) in a Ruby on Rails application adds a strong authentication layer by requiring both the client and server to present valid certificates. While mTLS significantly reduces risks like impersonation and man-in-the-middle attacks, it does not inherently protect sensitive data such as API keys if they are mishandled within the application. The presence of mTLS can create a false sense of security, leading developers to overlook proper secrets management. When API keys are stored in plaintext, logged inadvertently, or exposed through error messages, they remain vulnerable regardless of the transport security enforced by mTLS.
In a Rails environment, common exposure scenarios include storing API keys in configuration files that are accidentally committed to version control, printing keys to logs, or embedding them in JavaScript sent to the browser. Even with mTLS in place, if an API key is included in an HTTP header or query parameter and the application logic does not enforce strict access controls, an authenticated client could potentially leak or misuse the key. Attackers who compromise a client certificate or exploit a misconfigured mTLS setup might gain access to endpoints that accept and process API keys, enabling horizontal movement or privilege escalation.
Another concern arises when mTLS is used to authenticate services, but the Rails application still relies on API keys for downstream authorization or third-party integrations. If these keys are passed over mTLS-secured channels but stored or handled insecurely in memory, they can be exposed through debugging interfaces, background jobs, or dependency vulnerabilities. For example, using Rails.application.credentials without encrypting sensitive values or failing to rotate keys regularly can lead to long-lived exposures. The combination of mTLS and API keys requires disciplined practices: treat the transport security as one layer, and apply robust encryption, access controls, and monitoring at the application level to prevent key leakage.
Mutual Tls-Specific Remediation in Rails
To securely implement mTLS in Rails, enforce client certificate verification at the web server or load balancer level and validate the certificate fields in the application when necessary. Below are concrete examples demonstrating how to configure and use mTLS in a Rails environment.
Example 1: Enforcing mTLS at the web server (NGINX)
Configure NGINX to require and validate client certificates before proxying requests to the Rails app. This ensures that only clients with trusted certificates can reach your Rails endpoints.
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
ssl_client_certificate /etc/ssl/certs/ca.pem;
ssl_verify_client on;
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
}
}
Example 2: Accessing client certificate details in Rails
After mTLS is enforced at the infrastructure level, you can read verified client certificate information in your Rails controllers to make authorization decisions. This example shows how to inspect the client DN and verify the certificate status passed by the web server.
class Api::BaseController < ApplicationController
before_action :verify_client_certificate
private
def verify_client_certificate
unless request.headers['X-SSL-Client-Verify'] == 'SUCCESS'
render json: { error: 'Client certificate verification failed' }, status: :unauthorized
end
# Optionally inspect subject DN for additional checks
client_dn = request.headers['X-SSL-Client-DN']
# Implement custom mapping or validation as needed
end
end
Example 3: Using mTLS with Faraday for secure outbound calls
When your Rails app makes outbound requests to other services using mTLS, configure Faraday with client certificates to ensure mutual authentication. This example demonstrates setting up a Faraday connection with client certificate and key files.
conn = Faraday.new(url: 'https://partner-api.example.com') do |faraday|
faraday.ssl.client_cert = OpenSSL::X509::Certificate.new(File.read('path/to/client.crt'))
faraday.ssl.client_key = OpenSSL::PKey::RSA.new(File.read('path/to/client.key'))
faraday.ssl.ca_file = 'path/to/ca_bundle.pem'
faraday.adapter Faraday.default_adapter
end
response = conn.get('/v1/resource')