Credential Stuffing with Mutual Tls
How Credential Stuffing Manifests in Mutual TLS
Mutual TLS (mTLS) replaces password‑based authentication with X.509 client certificates. When correctly enforced, an attacker who only possesses stolen username/password pairs cannot complete the TLS handshake, because the server will reject the connection before any application‑level request is processed.
Credential stuffing becomes relevant when an mTLS‑protected service inadvertently offers a fallback authentication mechanism (e.g., HTTP Basic Auth, API key, or a token endpoint that accepts username/password). Attackers then target that fallback path with large volumes of credential pairs, hoping to obtain a valid session token or, in some cases, to directly acquire a client certificate from a self‑service enrollment endpoint.
Typical code paths where this can happen include:
- An Express.js server that creates a TLS socket with
request.client.authorizedchecked, but if the check fails it proceeds to apassport.authenticate('basic')handler. - A Go
net/httpserver that enablestls.Config.ClientAuth = tls.RequireAndVerifyClientCertbut also registers a separate/loginhandler that processesusernameandpasswordform fields. - A certificate enrollment API (e.g.,
/api/v1/certs/enroll) that is intended to be accessed only after presenting a valid client cert, but the endpoint does not validate the cert and instead validates a submitted username/password to issue a new cert.
In each case, the attacker’s credential‑stuffing attempt succeeds at the fallback layer, obtains a bearer token or a newly issued client certificate, and then uses that credential to call the mTLS‑protected API. Real‑world incidents have shown this pattern: CVE‑2021-22986 (F5 BIG‑IP iControl REST) allowed authentication bypass via a secondary endpoint that accepted username/password, and similar misconfigurations have been reported in Kubernetes API servers where the /authz endpoint fell back to basic auth when client cert validation was disabled.
OWASP API Security Top 10 2023 lists broken authentication (API2) as a primary risk, explicitly citing credential stuffing as a common attack technique. When mTLS is misconfigured to allow password‑based fallback, the service inherits the same credential‑stuffing risk that plagues traditional username/password APIs.
Mutual TLS‑Specific Detection
Detecting credential‑stuffing risk in an mTLS context requires looking beyond the TLS handshake and examining the application‑level authentication flows that may exist alongside it. middleBrick’s unauthenticated black‑box scan performs the following relevant checks:
- Authentication check – probes for alternative auth schemes (Basic Auth, API key, form‑based login) on the same host.
- BOLA/IDOR and BFLA checks – verify whether any endpoint that issues or manages client certificates is accessible without presenting a valid client cert.
- Rate Limiting check – determines whether login or certificate‑enrollment endpoints enforce request throttling, a key defense against credential stuffing.
- Input Validation check – looks for lack of validation on username/password fields that could enable brute‑force.
- Data Exposure check – ensures that error messages do not reveal whether a username exists (which would aid attackers).
Example: scanning a staging API with the middleBrick CLI:
# Install the CLI (npm)
npm i -g middlebrick
# Scan the target URL
middlebrick scan https://api.example.com
The output includes a per‑category breakdown. If the scanner finds a /login endpoint responding with HTTP 200 to posted credentials while the same host requires a client cert for /data, it will flag the authentication check as a finding with severity “Medium” (or higher if no rate limiting is present). The finding includes remediation guidance such as “Disable basic auth fallback” or “Apply rate limiting to the login endpoint”.
Because middleBrick tests the unauthenticated attack surface, it will also detect whether the server’s TLS configuration inadvertently allows a connection without a client cert (e.g., tls.Config.ClientAuth = tls.NoClientCert) and then proceeds to an application‑level auth path. This combination—TLS misconfiguration plus a password fallback—is exactly the credential‑stuffing scenario specific to mutual TLS.
Mutual TLS‑Specific Remediation
The most effective remediation is to eliminate any password‑based fallback and enforce strict client‑certificate validation at the TLS layer. Below are language‑specific examples that show how to configure the server to reject connections lacking a valid client cert and how to secure any remaining credential‑based endpoints.
Node.js (Express) – enforcing mTLS and removing basic auth
const tls = require('tls');
const fs = require('fs');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
ca: fs.readFileSync('ca.pem'), // trust root for client certs
requestCert: true, // ask for a client cert
rejectUnauthorized: true, // abort handshake if cert invalid or missing
// Optional: enable OCSP/CRL checking via external lib
};
const server = tls.createServer(options, (cleartextStream) => {
// At this point, cleartextStream.authorized === true only if a valid cert was presented
// No further auth checks are needed for mTLS‑protected routes
app(cleartextStream);
});
// Example of a protected route – no password checks needed
app.get('/data', (req, res) => {
res.json({ message: 'sensitive data' });
});
// If you must keep a legacy login endpoint, protect it with rate limiting
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // limit each IP to 10 requests per window
standardHeaders: true,
legacyHeaders: false,
});
app.post('/login', loginLimiter, (req, res) => {
// Validate credentials – but note this endpoint should be isolated
// and never used to issue client certs for mTLS access
const { username, password } = req.body;
// … authentication logic …
res.sendStatus(username === 'admin' && password === 'secret' ? 200 : 401);
});
server.listen(8443);
Go (net/http) – strict mTLS and rate‑limited fallback
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
"time"
"golang.org/x/time/rate"
)
func main() {
caCert, err := ioutil.ReadFile("ca.pem")
if err != nil { log.Fatal(err) }
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
// Optional: set VerifyPeerCertificate for OCSP/CRL
}
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/data":
// mTLS already validated; no further auth needed
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"sensitive data"}`))
case "/login":
// Rate‑limit this endpoint to mitigate credential stuffing
limiter := rate.NewLimiter(1*time.Minute, 5) // 5 req/min per IP
if !limiter.Allow() {
w.WriteHeader(http.StatusTooManyRequests)
return
}
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Parse credentials (example only – use proper validation/hashing)
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if validateCredentials(username, password) {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}),
}
log.Fatal(server.ListenAndServeTLS("server-cert.pem", "server-key.pem"))
}
func validateCredentials(u, p string) bool {
// Replace with real password verification (bcrypt, argon2, etc.)
return u == "admin" && p == "secret"
}
Key takeaways:
- Set
requestCert: true(Node) ortls.Config.ClientAuth = tls.RequireAndVerifyClientCert(Go) andrejectUnauthorized: trueto abort the handshake when a client cert is missing or invalid. - If a legacy username/password endpoint must exist, isolate it (different sub‑path or port) and apply strict rate limiting, account lockout after failed attempts, and ensure it never issues client certs for mTLS access.
- Validate the client certificate chain, expiration, and revocation status (OCSP/CRL) to prevent attackers from using stolen or self‑signed certs.
- Monitor logs for repeated TLS handshake failures (indicating missing client certs) and for spikes in authentication endpoint requests—both are useful detection signals.
By eliminating password‑based fallbacks and hardening any remaining credential pathways, the credential‑stuffing attack surface specific to mutual TLS is effectively removed.