Api Key Exposure in Hapi with Mutual Tls
Api Key Exposure in Hapi with Mutual Tls — how this specific combination creates or exposes the vulnerability
Hapi is a rich framework for building services and APIs in Node.js. When you add mutual TLS (mTLS) to Hapi, you introduce certificate-based client authentication alongside any other mechanisms you use such as API keys. The combination can inadvertently expose API keys if routing, logging, or proxying logic does not properly respect the mTLS boundaries and treats authenticated requests as fully trusted.
In an mTLS setup, the server verifies client certificates and may also extract information from the authenticated TLS session (e.g., the client certificate subject or serial) to identify or authorize requests. If the application also accepts API keys via headers, query parameters, or cookies, and does not enforce strict scoping between the TLS identity and the API key, an attacker who gains a valid client certificate could use it to invoke endpoints while supplying a different or stolen API key. Because mTLS provides transport-layer assurance only, the server might log or audit the request using the API key value, exposing it in logs, metrics, or error traces.
Another exposure vector arises when mTLS is terminated at a load balancer or gateway and the backend Hapi service receives the request over plain HTTP internally. If the service blindly trusts internal headers forwarded by the ingress (such as X-API-Key) and does not validate that the request originated from a properly authorized service identity, an attacker who compromises network routing or an internal host could inject or replay API keys. Even with mTLS in place, failing to validate the client certificate against the key owner on each request can lead to privilege escalation where one client’s key is used to access another client’s data.
Consider a typical Hapi server that reads an API key from a header after mTLS authentication:
const Hapi = require('@hapi/hapi');
const fs = require('fs');
const init = async () => {
const server = Hapi.server({
port: 443,
tls: {
cert: fs.readFileSync('/etc/certs/server.crt'),
key: fs.readFileSync('/etc/certs/server.key'),
ca: [fs.readFileSync('/etc/certs/ca.crt')],
requestCert: true,
rejectUnauthorized: true
}
});
server.ext('onRequest', (request, h) => {
const apiKey = request.headers['x-api-key'];
if (!apiKey) {
return h.response({ error: 'missing_key' }).code(401);
}
// TODO: verify that apiKey matches the certificate identity
request.pre.apiKey = apiKey;
return h.continue;
});
server.route({
method: 'GET',
path: '/v1/account/{id}',
handler: (request, h) => {
// Risk: logging the raw key in an error or audit trail
request.log(['api', 'debug'], { apiKey: request.pre.apiKey, accountId: request.params.id });
return { account: request.params.id };
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch(err => {
console.error(err);
process.exit(1);
});
In this example, the API key is extracted after mTLS authentication but is not cryptically bound to the certificate. An attacker presenting a valid client certificate could iterate over accounts by changing the path parameter while supplying different API keys, testing whether the server enforces ownership between the certificate and the key. If the server logs the key in clear text (as shown), the key becomes exposed in log aggregation systems, violating data exposure principles covered by one of middleBrick’s 12 checks.
Such misalignment also complicates compliance mapping. OWASP API Top 10 A07:2021 (Identification and Authentication Failures) and data exposure controls from frameworks like PCI-DSS and SOC2 require clear separation of authentication factors and protection of keys in transit and at rest. middleBrick’s scans can flag these risks by correlating runtime behavior with OpenAPI specs and highlighting where key handling does not align with mTLS identity.
Mutual Tls-Specific Remediation in Hapi — concrete code fixes
To reduce exposure when using mTLS with API keys in Hapi, bind the key to the certificate identity and avoid logging raw keys. Use the TLS session details to derive or validate the key, and enforce strict routing and audit controls.
First, validate that the API key matches a property of the client certificate (for example, a SAN or a custom extension). Do not accept an API key unless it can be verified against the certificate subject or a mapped value stored server-side.
const Hapi = require('@hapi/hapi');
const fs = require('fs');
const init = async () => {
const server = Hapi.server({
port: 443,
tls: {
cert: fs.readFileSync('/etc/certs/server.crt'),
key: fs.readFileSync('/etc/certs/server.key'),
ca: [fs.readFileSync('/etc/certs/ca.crt')],
requestCert: true,
rejectUnauthorized: true
}
});
server.ext('onRequest', (request, h) => {
const tlsPeer = request.socket.getPeerCertificate();
const apiKey = request.headers['x-api-key'];
if (!apiKey) {
return h.response({ error: 'missing_key' }).code(401);
}
// Map certificate serial or SAN to allowed API key; this mapping should be stored securely
const certSerial = tlsPeer.serialNumber;
const allowedKeyForCert = lookupKeyForCertificate(certSerial); // implement this securely
if (allowedKeyForCert !== apiKey) {
return h.response({ error: 'key_mismatch' }).code(403);
}
request.pre.certSerial = certSerial;
request.pre.apiKey = apiKey;
return h.continue;
});
server.route({
method: 'GET',
path: '/v1/account/{id}',
handler: (request, h) => {
// Use pre-validated values; avoid logging the raw API key
request.log(['api', 'debug'], {
certSerial: request.pre.certSerial,
accountId: request.params.id
});
return { account: request.params.id };
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch(err => {
console.error(err);
process.exit(1);
});
function lookupKeyForCertificate(serial) {
// Replace with secure lookup (e.g., database or KMS)
const mapping = {
'AA:BB:CC:DD:EE:FF': 'key-abc-123'
};
return mapping[serial] || null;
}
Second, ensure that mTLS is not bypassed internally. If your deployment terminates TLS at an ingress and forwards to Hapi over HTTP, require and validate the client certificate at the edge and propagate only a verified identity header (not the raw key). Avoid using X-API-Key alone to authorize requests that should be tied to mTLS identity.
server.ext('onRequest', (request, h) => {
// Example: require a verified header set by the trusted gateway
const verifiedCertId = request.headers['x-verified-cert-id'];
if (!verifiedCertId) {
return h.response({ error: 'unverified_request' }).code(400);
}
request.pre.certId = verifiedCertId;
return h.continue;
});
Finally, configure continuous monitoring and scanning. Use middleBrick’s CLI to scan your Hapi endpoints and verify that API key handling aligns with mTLS usage. The GitHub Action can enforce that new routes or changes do not weaken the binding between certificates and keys, while the MCP Server in your IDE can provide immediate feedback during development.