Tokio, Tower, Hyper and Rustls: Building High-Performance and Secure Servers in Rust — Part 6
In the earlier parts of this series, we explored the fundamentals of building HTTP(S) servers with (m)TLS using Rust. In this article…
Tokio, Tower, Hyper and Rustls: Building High-Performance and Secure Servers in Rust — Part 6
In the earlier parts of this series, we explored the fundamentals of building HTTP(S) servers with (m)TLS using Rust. In this article, we’ll extend our architecture by introducing additional middleware layers that add functionality to the request/response pipeline. Specifically, we’ll look at three layers:
- Delay Layer — A testing utility that introduces an artificial delay before a request is processed. It also serves as a clear example of implementing asynchronous logic inside the
poll_ready()method. - JWT Layer — A security layer that enables client authentication via JSON Web Tokens (JWT), offering an alternative to mTLS.
- Inspection Layer — A filter that inspects request paths using configurable regular expressions and only allows approved patterns.
Delay Layer
The Delay Layer introduces a configurable delay before a request is passed down the service stack. It’s especially useful for simulating slow responses or testing timeouts.
This delay is implemented inside the poll_ready() method by using a Tokio Sleep future. The method returns Poll::Pending until the sleep has elapsed, at which point the inner service becomes available. The call() method, on the other hand, remains unchanged and immediately forwards the request to the next layer.
Configuration
The delay can be enabled and configured in Config.toml as follows:
...
# activate the delay-layer
# the layers configured/enabled for this server
[Server.Layers]
enabled = [.., "Delay", ...]
...
# Configure the Delay layer
[Server.Layers.Delay]
delay_micros = 10
...
This configuration injects a 10-microsecond delay before the server processes each request.
Implementation
Here’s the implementation of the DelayLayer and its corresponding service:
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
use tokio::time::{self, Sleep};
use tower::{Layer, Service};
/// A Tower layer that adds an artificial delay to poll_ready and tags logs with a server name.
#[derive(Clone)]
pub struct DelayLayer {
delay: Duration,
server_name: Arc<String>,
}
impl DelayLayer {
pub fn new(delay: Duration, server_name: impl Into<String>) -> Self {
Self {
delay,
server_name: Arc::new(server_name.into()),
}
}
}
impl<S> Layer<S> for DelayLayer {
type Service = DelayService<S>;
fn layer(&self, inner: S) -> Self::Service {
DelayService {
inner,
delay: self.delay,
sleep: None,
server_name: Arc::clone(&self.server_name),
}
}
}
pub struct DelayService<S> {
inner: S,
delay: Duration,
sleep: Option<Pin<Box<Sleep>>>,
server_name: Arc<String>,
}
impl<S: Clone> Clone for DelayService<S> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
delay: self.delay,
sleep: None, // Don't clone sleep state!
server_name: Arc::clone(&self.server_name),
}
}
}
impl<S, Request> Service<Request> for DelayService<S>
where
S: Service<Request>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
// If we haven't started sleeping yet, create a new Sleep.
if self.sleep.is_none() {
tracing::info!(
"{}: Injecting a delay of {:?} before readiness",
self.server_name,
self.delay
);
self.sleep = Some(Box::pin(time::sleep(self.delay)));
}
// Now, poll the Sleep future.
let sleep = self.sleep.as_mut().unwrap();
if Pin::new(sleep).poll(cx).is_pending() {
return Poll::Pending;
}
// Sleep is done, clear it for next time.
self.sleep = None;
// Now delegate to the inner service.
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
tracing::info!(
"{}: Passing request through DelayLayer",
self.server_name
);
self.inner.call(req)
}
}
JWT Layer
As an alternative to mutual TLS (mTLS), JSON Web Tokens (JWT) can be used for client authentication and authorization. In this setup, JWTs are signed with a private key, and the server holds the corresponding public key(s) to verify incoming tokens. Sample code for generating JWTs is available in the associated GitHub repository.
Configuration
To enable the JWT layer, you must add it to the list of enabled layers in Config.toml. The only required configuration is a list of paths to public keys, which will be used to verify incoming tokens:
...
# the layers configured/enabled for this server
[Server.Layers]
#enabled = [..., "JWT", ...]
...
# Path to keys in case of authentication = "JWT"
# this can be a list of jwt_public_keys
[Server.Layers.JWT]
jwt_public_keys=["./jwt/public_key.pem"]
This configuration supports multiple public keys, allowing for key rotation or support for multiple issuers. The layer only allows requests that include an Authorization header in the format:
Authorization: Bearer <token>
Requests without a valid token will be rejected with a 401 Unauthorized response.
Implementation and Code Overview
The JWT layer is implemented as a Tower Layer, wrapping a service to enforce authentication before forwarding the request.
Key Components:
**JwtAuthLayer**: Initializes the layer with one or more public keys used to validate incoming JWTs. These keys are loaded from the provided file paths using a helper functionload_decoding_keys().**JwtAuthService**: Intercepts each incoming request, extracts the bearer token from theAuthorizationheader, and validates it using theverify_jwt()function. If the token is valid, the request continues to the inner service. Otherwise, the layer returns an unauthorized response.
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use bytes::Bytes;
use http_body_util::Full;
#[cfg(feature = "boxed_body")]
use http_body_util::BodyExt;
use hyper::{Request, Response, StatusCode};
use tower::{Layer, Service};
use tracing::error;
use server::ServiceRespBody;
#[cfg(feature = "boxed_body")]
use server::SrvError;
use jsonwebtoken::DecodingKey; // <-- ADDED
use crate::utils::{Claims, load_decoding_keys, verify_jwt};
#[derive(Clone)]
pub struct JwtAuthLayer {
decoding_keys: Arc<Vec<DecodingKey>>,
server_name: Arc<String>,
}
impl JwtAuthLayer {
#[allow(dead_code)]
pub fn new(key_files: Vec<String>, server_name: impl Into<String>) -> Self {
let decoding_keys = load_decoding_keys(&key_files);
Self {
decoding_keys: Arc::new(decoding_keys),
server_name: Arc::new(server_name.into()),
}
}
}
impl<S> Layer<S> for JwtAuthLayer {
type Service = JwtAuthService<S>;
fn layer(&self, inner: S) -> Self::Service {
JwtAuthService {
inner,
decoding_keys: self.decoding_keys.clone(),
server_name: Arc::clone(&self.server_name),
}
}
}
#[derive(Clone)]
pub struct JwtAuthService<S> {
inner: S,
decoding_keys: Arc<Vec<DecodingKey>>,
server_name: Arc<String>,
}
impl<S, ReqBody> Service<Request<ReqBody>> for JwtAuthService<S>
where
S: Service<Request<ReqBody>, Response = Response<ServiceRespBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = Response<ServiceRespBody>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
let mut inner = self.inner.clone();
let decoding_keys = self.decoding_keys.clone();
let server_name = Arc::clone(&self.server_name);
Box::pin(async move {
// extract the token string from the request's Authorization header
let token = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.map(str::trim);
match token {
Some(token) => match verify_jwt(token, &decoding_keys) {
Ok(_claims) => {
req.extensions_mut().insert::<Claims>(_claims);
inner.call(req).await
}
Err(e) => {
error!("{}: Invalid JWT: {:?}", server_name, e);
unauthorized_response()
}
},
None => {
error!("{}: Missing or invalid Authorization header", server_name);
unauthorized_response()
}
}
})
}
}
fn unauthorized_response<T>() -> Result<Response<ServiceRespBody>, T> {
#[cfg(feature = "boxed_body")]
let body: ServiceRespBody = Full::new(Bytes::from("Unauthorized"))
.map_err(SrvError::from)
.boxed();
#[cfg(not(feature = "boxed_body"))]
let body: ServiceRespBody = Full::new(Bytes::from("Unauthorized"));
let mut resp: Response<ServiceRespBody> = Response::new(body);
*resp.status_mut() = StatusCode::UNAUTHORIZED;
Ok(resp)
}
Inspection Layer
The Inspection Layer adds an additional security layer by restricting access to certain paths based on regular expression (regex) patterns. While the underlying service may expose multiple endpoints, this layer ensures only explicitly approved URL patterns are accessible.
This can be particularly useful when:
- You want to restrict production endpoints while leaving others available in development.
- You need fine-grained, method-based access control (e.g., only allow certain
GETrequests with specific query parameters).
Configuration
The allowed paths are defined in the Config.toml file. For each HTTP method (GET, POST, etc.), you can specify a map of endpoints and their allowed regex patterns:
...
# Enable the Inspection layer
[Server.Layers]
enabled = ["Inspection"]
...
# Define allowed GET paths with regex validation
[Server.AllowedPathes.GET]
"/" = ["^/?$", "^/\\?name=.*$"]
"/help" = ["^/help\\??(topic=.*)?$"]
"/name" = ["^/name\\??id=\\d+$"]
With the above setup:
- Only
GET /,GET /?name=...,GET /help,GET /help?topic=..., andGET /name?id=123are allowed. - Any other paths or query patterns will be blocked with a
403 Forbidden.
You can also define Rules for POST, PUT, etc.
How the Code Works
InspectionLayer and InspectionService: These are standard Tower middleware structures. The InspectionLayer wraps an inner service with logic that checks each request against a set of allowed patterns.
use bytes::Bytes;
#[cfg(feature = "boxed_body")]
use http_body_util::BodyExt;
use http_body_util::Full;
use hyper::{Request, Response, StatusCode};
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tower::{Layer, Service};
use crate::configuration::CompiledAllowedPathes; // Adjust to your module path
#[cfg(feature = "boxed_body")]
use server::SrvError;
#[derive(Clone)]
pub struct InspectionLayer {
rules: Arc<CompiledAllowedPathes>,
server_name: Arc<String>,
}
impl InspectionLayer {
/// Create a new InspectionLayer with rules and a server name.
pub fn new(rules: CompiledAllowedPathes, server_name: impl Into<String>) -> Self {
Self {
rules: Arc::new(rules),
server_name: Arc::new(server_name.into()),
}
}
}
impl<S> Layer<S> for InspectionLayer {
type Service = InspectionService<S>;
fn layer(&self, inner: S) -> Self::Service {
InspectionService {
inner,
allowed_pathes: self.rules.clone(),
server_name: Arc::clone(&self.server_name),
}
}
}
#[derive(Clone)]
pub struct InspectionService<S> {
inner: S,
allowed_pathes: Arc<CompiledAllowedPathes>,
server_name: Arc<String>,
}
use server::ServiceRespBody;
impl<S, ReqBody> Service<Request<ReqBody>> for InspectionService<S>
where
S: Service<Request<ReqBody>, Response = Response<ServiceRespBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = Response<ServiceRespBody>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
// Extracts the method, path, and query string.
// Check if the request is allowed via the is_allowed function.
let method = req.method().as_str().to_uppercase();
let uri = req.uri().clone();
let path = uri.path().to_string();
let query = uri.query().unwrap_or("").to_string();
let allow = self.allowed_pathes.is_allowed(&method, &path, &query);
let server_name = Arc::clone(&self.server_name);
if allow {
//If the request matches one of the allowed regex patterns
let mut inner = self.inner.clone();
Box::pin(async move { inner.call(req).await })
} else {
// if not allowed:
// Log the blocked attempt.
// Return a 403 Forbidden response with a simple body.
tracing::warn!(
"{}: Blocked request: {} {}?{}",
server_name,
method,
path,
query
);
#[cfg(feature = "boxed_body")]
let body: ServiceRespBody =
Full::new(Bytes::from("Request does not match allowed patterns"))
.map_err(SrvError::from)
.boxed();
#[cfg(not(feature = "boxed_body"))]
let body: ServiceRespBody =
Full::new(Bytes::from("Request does not match allowed patterns"));
let response = Response::builder()
.status(StatusCode::FORBIDDEN)
.body(body)
.expect("Failed to build response");
Box::pin(async move { Ok(response) })
}
}
}
Path Matching Logic
Function is_allowed:
- Builds the full path including the query string.
- Looks up regex patterns for the given method and path.
- Returns
trueif any pattern matches the full path. - Returns
falseif no patterns match or if the method is unsupported.
pub fn is_allowed(&self, method: &str, path: &str, query: &str) -> bool {
let full_path = if query.is_empty() {
path.to_string()
} else {
format!("{}?{}", path, query)
};
let map = match method {
"GET" => &self.get,
"POST" => &self.post,
"PUT" => &self.put,
"DELETE" => &self.delete,
_ => return false,
};
map.get(path)
.map(|regexes| regexes.iter().any(|r| r.is_match(&full_path)))
.unwrap_or(false)
}
Regex Compilation on Startup
To avoid recompiling regexes on each request, the system compiles all configured patterns at startup:
pub fn from_raw(routes: &Option<AllowedPathes>) -> Result<Self, Error> {
fn compile(
map: &Option<HashMap<String, Vec<String>>>,
) -> Result<HashMap<String, Vec<Regex>>, Error> {
let mut compiled = HashMap::new();
if let Some(map) = map {
for (k, patterns) in map {
let regexes = patterns
.iter()
.map(|p| {
Regex::new(p).map_err(|e| Error::msg(format!("Invalid regex: {}", e)))
})
.collect::<Result<Vec<_>, _>>()?;
compiled.insert(k.clone(), regexes);
}
}
Ok(compiled)
}
Ok(Self {
get: compile(&routes.as_ref().and_then(|r| r.get.clone()))?,
post: compile(&routes.as_ref().and_then(|r| r.post.clone()))?,
put: compile(&routes.as_ref().and_then(|r| r.put.clone()))?,
delete: compile(&routes.as_ref().and_then(|r| r.delete.clone()))?,
})
}
This ensures:
- Patterns are validated early (bad regexes will crash the server on startup).
- Runtime performance is optimal (no re-parsing per request).
Summary
The Inspection Layer is a powerful security tool that:
- Provides fine-grained access control per HTTP method.
- Uses fast precompiled regexes.
- Acts as a gatekeeper before requests reach inner logic.
It’s especially useful for:
- Locking down public APIs
- Validating query parameters
- Controlling unexpected or undocumented routes
Conclusion
In this part of the series, we expanded our Rust-based server architecture by introducing three powerful and reusable Tower layers:
- The Delay Layer, useful for testing response timing and simulating slow services.
- The JWT Layer, which provides secure and configurable client authentication using public-key verified tokens.
- The Inspection Layer, which acts as a customizable access control mechanism using regex-based path validation.
These layers showcase how Tower’s middleware model allows you to build composable, testable, and production-ready server features with minimal overhead. They also demonstrate how security and performance considerations can be embedded directly into the service stack, rather than added as afterthoughts.
In the final part of the series, we’ll complete the picture by:
- Implementing a benchmarking client that supports both JWT and (m)TLS authentication for performance testing.
- Introducing a routing layer that transforms our server into a basic reverse proxy — enabling dynamic request forwarding based on request path.
With these additions, the server evolves from a secure microservice into a flexible and extensible platform, suitable for more complex deployment scenarios.
메타데이터
- post_id
- ebb5cdf120d8
- slug
- tokio-tower-hyper-and-rustls-building-high-performance-and-secure-servers-in-rust-part-6-ebb5cdf120d8
- url
- https://medium.com/@alfred.weirich/tokio-tower-hyper-and-rustls-building-high-performance-and-secure-servers-in-rust-part-6-ebb5cdf120d8
- canonical_url
- https://medium.com/@alfred.weirich/tokio-tower-hyper-and-rustls-building-high-performance-and-secure-servers-in-rust-part-6-ebb5cdf120d8
- author_url
- https://medium.com/@alfred.weirich
- status
- ok
- fetched_at
- 2026-07-19 02:56:21