HIGH api key exposurenestjsmutual tls

Api Key Exposure in Nestjs with Mutual Tls

Api Key Exposure in Nestjs with Mutual Tls — how this specific combination creates or exposes the vulnerability

Api key exposure in a NestJS service that also uses mutual TLS (mTLS) can occur when application-layer secrets (API keys) are handled in addition to, or instead of, relying on transport-layer client certificates. mTLS ensures that both client and server present valid certificates during the TLS handshake, providing strong identity and encryption. However, it does not automatically protect an API key that is embedded in request headers, query parameters, or logs by the application.

In NestJS, developers sometimes add an API key check (for example a static key in an environment variable) alongside mTLS, mistakenly assuming the two controls provide redundant security. If the API key is passed in an HTTP header such as x-api-key, and the server logs incoming requests (including headers) at any level — console, file, or third-party monitoring — the key can be inadvertently exposed. This is an example of data exposure, one of the 12 parallel security checks performed by middleBrick. Even with mTLS preventing unauthorized clients from connecting, a logged API key can be harvested from logs or error messages and reused.

Another exposure path involves error handling. If a NestJS application throws detailed errors that include header values when mTLS client identity is missing or invalid, developers might inadvertently include the API key in stack traces or error responses sent to the caller. While mTLS prevents connections from unknown clients, misconfigured global exception filters can still reflect sensitive header content in responses. Furthermore, if the API key is stored in configuration files or environment variables without runtime protection, and those files are included in container images or version control, the key is exposed independent of the mTLS channel.

The interplay also surfaces in observability and debugging workflows. Developers might add middleware that copies headers to logs for tracing; if the API key is present, it persists in log stores. middleBrick’s Data Exposure and Encryption checks highlight such risks by correlating runtime findings with spec definitions and identifying where secrets might leak through logs, error responses, or misconfigured transports. mTLS secures the pipe, but does not prevent application-level leakage of the key itself.

Additionally, if the API key is used as a secondary authorization token after mTLS authentication, consider the risk of privilege escalation (BOLA/IDOR) or over-privileged usage. A compromised key, even when mTLS is in place, can be used to perform actions the client certificate alone would not allow if authorization checks are weak. This is why input validation and property authorization checks are run in parallel with encryption and transport checks.

Mutual Tls-Specific Remediation in Nestjs — concrete code fixes

Remediation focuses on ensuring the API key is never logged, transmitted in error messages, or stored insecurely, while properly enforcing mTLS in NestJS. Below are concrete patterns and code examples.

1. Enforce mTLS in NestJS with the built-in HTTP adapter

Configure the underlying HTTP server to request (or require) client certificates. In NestJS you can customize the HTTPS server options via the HTTPS_PORT setup and a custom adapter or platform configuration. Example using the native https module:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from 'fs';
import * as https from 'https';

async function bootstrap() {
  const httpsOptions = {
    key: fs.readFileSync('path/to/server.key'),
    cert: fs.readFileSync('path/to/server.crt'),
    ca: fs.readFileSync('path/to/ca.crt'),
    requestCert: true,
    rejectUnauthorized: true,
  };

  const app = await NestFactory.create(AppModule, httpsOptions);
  await app.listen(3000);
}
bootstrap();

Here requestCert: true and rejectUnauthorized: true ensure the server requests and validates client certificates. The CA bundle must be the issuer of allowed client certs.

2. Remove API keys from runtime exposure in headers and logs

Avoid passing API keys in headers. If they must be used, ensure they are stripped from logs. Implement a logging filter that scrubs sensitive headers:

import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class SanitizeLoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    const request = context.switchToHttp().getRequest();
    // Remove sensitive headers before logging
    const { headers } = request;
    const safeHeaders = { ...headers };
    delete safeHeaders['x-api-key'];
    // safeHeaders can be logged instead
    console.log('Incoming request', {
      url: request.url,
      method: request.method,
      headers: safeHeaders,
    });
    return next.handle();
  }
}

Register the interceptor globally in your main application file to apply it to all routes.

3. Secure configuration and runtime secrets

Do not store API keys in source code or unencrypted config files. Use environment variables injected at runtime and, where possible, a secrets manager. At minimum, ensure environment files are in .gitignore and container images do not contain plain-text keys.

4. Authorization checks after mTLS identity mapping

After mTLS authentication, map the client certificate subject to an internal role or identity, then enforce property-level authorization. Avoid using the API key as a substitute for proper RBAC/ABAC checks:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class ApiKeyAndCertGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredScopes = this.reflector.get('scopes', context.getHandler());
    const request = context.switchToHttp().getRequest();
    // mTLS client identity is available via request.client.verifiedCert or similar
    const client = request.client;
    const apiKey = request.headers['x-api-key'];

    // Validate API key format and scope mapping here without logging it
    if (!this.isValidKeyForClient(apiKey, client)) {
      return false;
    }
    return true;
  }

  private isValidKeyForClient(key: string, client: any): boolean {
    // Implement lookup without logging the key
    return key && key.length === 32 && client && client.verified === true;
  }
}

Combine these practices to ensure mTLS handles transport identity while the API key is treated as a sensitive application secret that is never logged or exposed.

Frequently Asked Questions

Does mTLS alone prevent all forms of API key exposure?
No. mTLS secures the transport and client identity but does not prevent the application from logging, mishandling, or leaking API keys in headers, error messages, or observability data. Application-level hygiene is still required.
Should I use both mTLS and an API key in NestJS?
If you use both, ensure the API key is never logged or transmitted unnecessarily and is validated in a way that does not weaken mTLS. Often mTLS alone is sufficient for service-to-service identity; adding an API key should be justified by a clear threat model and implemented with strict data exposure controls.