Credential Stuffing in Axum with Mutual Tls
Credential Stuffing in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated attack where valid credentials from one breach are replayed against an application to hijack accounts. In Axum, enabling Mutual Transport Layer Security (mTLS) means the server requests and validates a client certificate in addition to (or instead of) traditional login credentials. While mTLS strengthens channel authentication, it does not prevent credential stuffing at the application layer. An attacker can still automate login requests using stolen username and password pairs. If the service accepts both a certificate and a password, the presence of mTLS may create a false sense of security, leading developers to relax other controls such as rate limiting or account lockout. Each TLS handshake presents a new connection, and automated tools can open many connections to avoid per-connection certificate checks while systematically submitting credential pairs.
Another exposure arises when mTLS is inconsistently enforced. For example, an endpoint might require a client certificate for administrative paths but omit enforcement for the public login route. Attackers will target the weaker path, using credential stuffing against the login endpoint while bypassing mTLS requirements. Additionally, if certificate validation is performed but the application still relies on static passwords, the password remains the single factor an attacker can brute-force at scale. Session management flaws can compound the issue: a successful credential stuffing attack may yield valid sessions that persist beyond the intended lifetime, especially if tokens or cookies do not rotate on each authenticated request or lack strict binding to the client certificate context.
From an API security scanning perspective, middleBrick checks whether authentication is applied consistently across routes and whether controls such as rate limiting and anomaly detection are present regardless of mTLS. The scanner does not assume that mTLS alone stops automated credential reuse; it highlights gaps where passwords remain the primary credential and where excessive permissions or missing throttling enable rapid testing of stolen credentials. Findings include weak input validation on credentials, missing multi-factor options, and lack of behavioral analysis, all of which remain relevant even when client certificates are used. Understanding this helps teams align implementation with frameworks such as OWASP API Security Top 10 and reduces risk of account takeover via credential stuffing despite the presence of mTLS.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
To reduce credential stuffing risk while using mTLS in Axum, enforce strict client certificate validation on all routes, avoid optional authentication, and layer rate limiting and anomaly detection on the login path. Below are concrete code examples that demonstrate a secure setup.
Enforce mTLS on all routes with proper certificate validation
Use Axum middleware to require and verify client certificates on every request. This ensures that no route accidentally allows unauthenticated TLS access.
use axum::{
async_trait,
extract::Request,
middleware::Next,
response::Response,
};
use hyper::Body;
use std::convert::Infallible;
use tokio_rustls::rustls::{ServerConfig, RootCertStore};
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio_rustls::TlsAcceptor;
use std::sync::Arc;
async fn build_tls_acceptor() -> TlsAcceptor {
let mut root_store = RootCertStore::empty();
// Load your trusted CA certificates that validate client certificates
root_store.add(&CertificateDer::from(vec::from([/* CA bytes */]))).expect("valid cert");
let mut server_config = ServerConfig::builder()
.with_no_client_auth() // we will enforce client auth manually to inspect details
.with_safe_defaults()
.with_root_certificates(root_store)
.with_single_cert(
vec![CertificateDer::from(vec![/* server cert */])],
PrivateKeyDer::Pkcs8(/* server key */.into()),
)
.expect("valid server cert");
// Require client authentication but handle verification in middleware to apply consistent rules
server_config.client_auth_root_store = Some(root_store);
server_config.client_auth_mode = tokio_rustls::rustls::server::ClientAuthMode::Request;
TlsAcceptor::from(Arc::new(server_config))
}
pub fn require_client_cert(
) -> impl Fn(Request, Next) -> Pin> + Send + '_>> {
let acceptor = build_tls_acceptor();
move |req: Request, next: Next| {
let acceptor = acceptor.clone();
async move {
// Perform TLS handshake and extract peer certificate
// (In production, integrate with your connection layer to obtain the TLS state)
// For illustration, assume we have a function `peer_certs(req)` that returns Vec
let peer_certs = peer_certs(req); // implement this based on your transport
if peer_certs.is_empty() {
return Ok(Response::builder().status(400).body(Body::from("Client certificate required")).unwrap());
}
// Optionally validate certificate fields, revocation, etc.
let _ = peer_certs; // use validation logic here
Ok(next.run(req).await)
}
}
}
// Apply globally
let app = Router::new()
.route("/login", post(login_handler))
.layer(middleware::from_fn(require_client_cert()));
Apply rate limiting and anomaly detection on the login route
Even with mTLS, the login endpoint should be protected against automated credential submission. Use a per-username or per-IP rate limiter and track failed attempts to detect stuffing patterns.
use axum::{
async_trait,
extract::State,
middleware::Next,
response::Response,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tower_http::limit::RateLimitLayer;
use std::net::IpAddr;
struct RateState {
attempts: HashMap>,
max_attempts: usize,
window: Duration,
}
impl RateState {
fn new(max_attempts: usize, window: Duration) -> Self {
Self { attempts: HashMap::new(), max_attempts, window }
}
fn record(&mut self, key: String) -> bool {
let now = Instant::now();
let window_start = now - self.window;
self.attempts.entry(key.clone()).or_default().retain(|t| *t > window_start);
self.attempts.get_mut(&key).map(|v| {
v.push(now);
v.len() > self.max_attempts
}).unwrap_or(false)
}
}
async fn login_handler(
State(state): State>>,
// extract credentials from request body
) -> Response {
// validate credentials
// if failed {
// let mut s = state.lock().unwrap();
// if s.record(username_or_ip) {
// return Response::builder().status(429).body("Too many attempts").unwrap();
// }
// }
Response::builder().status(200).body(Body::empty()).unwrap()
}
// Alternatively, use tower-http rate limiting for IP-based throttling
let app = Router::new()
.route("/login", post(login_handler))
.layer(RateLimitLayer::new(10, Duration::from_secs(60))); // 10 requests per minute
Consistent authentication and input validation
Ensure mTLS and password checks are both required where appropriate, and validate all inputs to prevent injection or bypass. Do not treat mTLS as a replacement for strong password policies and secure session handling.
async fn login_handler(
// extract username/password from JSON body
// verify certificate binding to identity if needed
) -> Result {
// verify password strength, length, and breach checks
// ensure constant-time comparison for credentials
Ok(Json(json!({"status": "ok"})))
}