Man In The Middle in Axum with Api Keys
Man In The Middle in Axum with Api Keys — how this specific combination creates or exposes the vulnerability
A Man In The Middle (MitM) scenario in an Axum service that relies on API keys occurs when an attacker can intercept or tamper with traffic between the client and the server. Because API keys are often transmitted in headers, if those requests are sent over unencrypted HTTP, or if TLS is misconfigured (for example, missing certificate validation or accepting self-signed certificates), an on-path attacker can observe or inject the key. This exposes the key, enabling the attacker to impersonate legitimate clients and gain unauthorized access to the Axum endpoints that depend on the key for authorization.
In Axum, API keys are typically validated via middleware that inspects incoming headers. If the transport layer does not guarantee confidentiality and integrity, the key can be read or altered before reaching the middleware, undermining the authorization check. Additionally, if the key is embedded in URLs as a query parameter (e.g., ?api_key=...), it can leak into logs, browser history, or referrer headers, further increasing exposure. A related risk is that some Axum applications may skip TLS verification when calling downstream services or when accepting client certificates, which can allow an attacker to present a fraudulent endpoint and capture the key in transit. The combination of weak transport security and the presence of static API keys means that an intercepted key remains valid until revoked, enabling persistent unauthorized access.
Another subtle vector involves service-to-service communication within a cluster behind a load balancer. If an Axum application forwards requests internally without enforcing mTLS or verifying the authenticity of the caller, an attacker who compromises a network segment can position themselves between services and misuse API keys that were originally intended for external clients. This is especially relevant when OpenAPI specifications are used to generate server code; if the spec does not explicitly enforce HTTPS and strict transport security, generated Axum routes might inadvertently accept cleartext HTTP, creating a MitM surface for API key exposure.
Api Keys-Specific Remediation in Axum — concrete code fixes
Remediation focuses on ensuring API keys are never exposed in cleartext and are validated securely within Axum middleware. Always enforce HTTPS by configuring TLS at the load balancer or application layer, and reject HTTP requests outright. Use middleware that extracts API keys from headers rather than query parameters, and avoid logging keys. Rotate keys regularly and scope them to least privilege. Below are concrete Axum examples demonstrating secure handling.
Secure Axum middleware using API keys over HTTPS
use axum::{
async_trait,
extract::Request,
middleware::Next,
response::IntoResponse,
};
use std::convert::Infallible;
pub struct ApiKeyValidator {
valid_keys: std::collections::HashSet<String>,
}
impl ApiKeyValidator {
pub fn new(keys: Vec<String>) -> Self {
Self {
valid_keys: keys.into_iter().collect(),
}
}
pub fn validate(&self, key: &str) -> bool {
self.valid_keys.contains(key)
}
}
#[async_trait]
impl tower::Layer<S> for ApiKeyValidator {
type Service = ApiKeyFilter<S>;
fn layer(&self, inner: S) -> Self::Service {
ApiKeyFilter {
inner,
validator: self.clone(),
}
}
}
#[derive(Clone)]
pub struct ApiKeyFilter<S> {
inner: S,
validator: ApiKeyValidator,
}
impl<S, ReqBody, ResBody> tower::Service<Request> for ApiKeyFilter<S>
where
S: tower::Service<Request, Response = axum::response::Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
ResBody: Send + 'static,
{
type Response = axum::response::Response;
type Error = Infallible;
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, mut req: Request) -> Self::Future {
let validator = self.validator.clone();
let fut = async move {
let key = req.headers()
.get("X-API-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if validator.validate(key) {
self.inner.call(req).await
} else {
let response = axum::response::Response::builder()
.status(401)
.body(hyper::Body::from("Invalid API key"[..]))
.unwrap();
Ok(response)
}
};
Box::pin(fut)
}
}
/// Enforce HTTPS by requiring a secure connection in production.
pub fn require_https_layer() -> impl tower::Layer<Request, Service = impl tower::Service<Request>> {
struct HttpsEnforcer;
#[async_trait]
impl tower::Layer<S> for HttpsEnforcer {
type Service = HttpsEnforcerService<S>;
fn layer(&self, inner: S) -> Self::Service {
HttpsEnforcerService { inner }
}
}
struct HttpsEnforcerService<S> {
inner: S,
}
impl<S, ReqBody, ResBody> tower::Service<Request> for HttpsEnforcerService<S>
where
S: tower::Service<Request, Response = axum::response::Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
ResBody: Send + 'static,
{
type Response = axum::response::Response;
type Error = Infallible;
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request) -> Self::Future {
let uri = req.uri().clone();
let is_https = uri.scheme_str() == Some("https");
let fut = async move {
if is_https {
Ok(req.into_response())
} else {
let response = axum::response::Response::builder()
.status(400)
.body(hyper::Body::from("HTTPS required"[..]))
.unwrap();
Ok(response)
}
};
Box::pin(fut)
}
}
}
/// Example of integrating both layers in your Axum application.
/// #[tokio::main]
/// async fn main() {
/// let validator = ApiKeyValidator::new(vec!["super-secret-key-123".to_string()]);
/// let https = require_https_layer();
///
/// let app = Router::new()
/// .route("/secure", get(secure_handler))
/// .layer(https)
/// .layer(validator);
///
/// let listener = Tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// }
///
/// async fn secure_handler() -> impl IntoResponse {
/// "Authenticated and over HTTPS"
/// }
Key practices to prevent MitM with API keys in Axum
- Always use TLS with strong ciphers and redirect HTTP to HTTPS at the edge (load balancer or reverse proxy).
- Store API keys in environment variables or a secure secret manager; never hardcode them in source or config files that might be exposed.
- Prefer header-based keys (e.g.,
X-API-Key) over URL query parameters to avoid leakage in logs and browser history. - Implement key rotation and revocation mechanisms so compromised keys can be invalidated quickly.
- Apply strict CORS policies to reduce unintended exposure in browser contexts.
- Use middleware to validate keys centrally, and ensure middleware is applied before routes that require authorization.