On this page
- The Security Gap in Mock Servers
- TLS/HTTPS Configuration: Rust's Approach
- Mutual TLS (mTLS): Client Certificate Authentication
- TLS Configuration in Practice
- Built-in Tunneling Service: Public URLs for Local Development
- Tunneling Architecture: Connection Management
- Security Considerations for Tunneling
- Production Deployment Patterns
- Certificate Management
- Performance Considerations
- The Bottom Line
🔗The Security Gap in Mock Servers
Most mock servers run on HTTP, unencrypted. That's fine for local development, but when you need to test production-like scenarios—OAuth2 flows, HTTPS-only APIs, or services that require mutual TLS—unencrypted mocks fall short.
The problem isn't just about encryption. Modern APIs require:
- TLS/HTTPS for encrypted connections
- Mutual TLS (mTLS) for client certificate authentication
- Public URLs for OAuth2 callback testing
- Secure tunneling for local development
MockForge v0.2.6 adds production-grade security features: TLS/HTTPS support, mutual TLS (mTLS) for client certificate authentication, and a built-in tunneling service for exposing local servers via public URLs. This post covers the technical implementation.
🔗TLS/HTTPS Configuration: Rust's Approach
MockForge uses rustls for TLS implementation—a pure Rust TLS library that's memory-safe and performant. The TLS configuration is straightforward but powerful:
pub fn load_tls_acceptor(config: &HttpTlsConfig) -> Result<TlsAcceptor> {
use rustls_pemfile::{certs, pkcs8_private_keys};
use std::fs::File;
use std::io::BufReader;
// Load certificate chain from PEM file
let cert_file = File::open(&config.cert_file)?;
let mut cert_reader = BufReader::new(cert_file);
let cert_bytes: Vec<Vec<u8>> = certs(&mut cert_reader)?;
let server_certs = cert_bytes
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>();
if server_certs.is_empty() {
return Err(Error::generic("No certificates found"));
}
// Load private key from PEM file
let key_file = File::open(&config.key_file)?;
let mut key_reader = BufReader::new(key_file);
let mut keys: Vec<Vec<u8>> = pkcs8_private_keys(&mut key_reader)?;
if keys.is_empty() {
return Err(Error::generic("No private keys found"));
}
// Build TLS server configuration
let server_config = rustls::server::ServerConfig::builder()
.with_safe_defaults() // TLS 1.2 and 1.3, secure cipher suites
.with_no_client_auth() // Standard TLS (no client cert required)
.with_single_cert(server_certs, rustls::PrivateKey(keys.remove(0)))?;
Ok(TlsAcceptor::from(Arc::new(server_config)))
}
The with_safe_defaults() method configures secure defaults:
- TLS 1.2 and 1.3 support
- Secure cipher suites only
- No weak protocols or algorithms
This is the right approach: use safe defaults, don't give developers enough rope to hang themselves with insecure configurations.
🔗Mutual TLS (mTLS): Client Certificate Authentication
Mutual TLS adds an extra layer of security by requiring clients to present a certificate. This is common in enterprise APIs and microservices architectures where you need to verify client identity.
pub fn load_tls_acceptor(config: &HttpTlsConfig) -> Result<TlsAcceptor> {
// ... load server certs and keys (same as above) ...
let server_config = if config.require_client_cert {
// Mutual TLS: require client certificates
if let Some(ref ca_file_path) = config.ca_file {
// Load CA certificate for client verification
let ca_file = File::open(ca_file_path)?;
let mut ca_reader = BufReader::new(ca_file);
let ca_certs: Vec<Vec<u8>> = certs(&mut ca_reader)?;
let ca_certs = ca_certs
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>();
// Build root certificate store
let mut root_store = rustls::RootCertStore::empty();
for cert in ca_certs {
root_store.add(&cert)?;
}
// Build with mTLS support
rustls::server::ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(Arc::new(
rustls::server::AllowAnyAuthenticatedClient::new(root_store),
))
.with_single_cert(server_certs, rustls::PrivateKey(keys.remove(0)))?
} else {
return Err(Error::generic(
"Client certificate required but no CA file provided"
));
}
} else {
// Standard TLS: no client certificate required
rustls::server::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(server_certs, rustls::PrivateKey(keys.remove(0)))?
};
Ok(TlsAcceptor::from(Arc::new(server_config)))
}
The key difference is with_client_cert_verifier(). This configures the server to:
- Require client certificates on connection
- Verify client certificates against the CA certificate store
- Reject connections from clients without valid certificates
This is production-grade security. Clients must present a certificate signed by your CA, proving their identity before any data exchange.
🔗TLS Configuration in Practice
The TLS configuration is exposed via YAML/JSON config:
http:
tls:
enabled: true
cert_file: "/path/to/server.crt"
key_file: "/path/to/server.key"
ca_file: "/path/to/ca.crt" # Required for mTLS
require_client_cert: true # Enable mTLS
min_version: "1.2" # Minimum TLS version
Or via CLI:
mockforge serve \
--tls-cert /path/to/server.crt \
--tls-key /path/to/server.key \
--tls-ca /path/to/ca.crt \
--require-client-cert
The configuration is flexible but secure by default. You can't accidentally enable weak TLS versions or insecure cipher suites—rustls's safe defaults prevent it.
🔗Built-in Tunneling Service: Public URLs for Local Development
The tunneling service solves a common problem: you're developing locally, but you need a public URL for OAuth2 callbacks or webhook testing. Instead of deploying to a staging environment, MockForge provides a built-in tunneling service.
graph TD
A[Local Mock Server] -->|Register| B[Tunnel Service]
B -->|Public URL| C[Internet]
C -->|Request| B
B -->|Forward| A
A -->|Response| B
B -->|Forward| C
The tunneling service:
- Creates a public URL that forwards to your local server
- Handles connection management and reconnection
- Provides rate limiting and security controls
- Supports multiple tunnel providers (ngrok, localtunnel, etc.)
🔗Tunneling Architecture: Connection Management
The tunneling service is built around connection management and request forwarding:
pub struct TunnelService {
provider: Arc<dyn TunnelProvider>,
local_url: String,
rate_limiter: Arc<RateLimiter>,
}
impl TunnelService {
pub async fn create_tunnel(&self) -> Result<Tunnel> {
// Register with tunnel provider
let public_url = self.provider.create_tunnel(&self.local_url).await?;
// Start forwarding loop
let tunnel = Tunnel {
public_url: public_url.clone(),
local_url: self.local_url.clone(),
active: true,
};
// Spawn forwarding task
tokio::spawn(async move {
self.forward_requests(tunnel).await;
});
Ok(tunnel)
}
async fn forward_requests(&self, tunnel: Tunnel) {
while tunnel.active {
// Wait for incoming request on public URL
if let Some(request) = self.provider.accept_request().await {
// Forward to local server
let response = self.forward_to_local(&request).await;
// Send response back through tunnel
self.provider.send_response(&request.id, response).await;
}
}
}
}
The tunneling service handles:
- Connection lifecycle management
- Request forwarding with proper headers
- Error handling and reconnection
- Rate limiting to prevent abuse
🔗Security Considerations for Tunneling
Public URLs expose your local server to the internet. The tunneling service includes security controls:
pub struct TunnelConfig {
pub rate_limit: RateLimit, // Requests per minute
pub allowed_ips: Vec<IpAddr>, // IP whitelist (optional)
pub require_auth: bool, // Require authentication
pub auth_token: Option<String>, // Authentication token
}
impl TunnelService {
fn validate_request(&self, request: &Request) -> Result<()> {
// Rate limiting
if !self.rate_limiter.check(request.client_ip) {
return Err(Error::RateLimitExceeded);
}
// IP whitelist
if let Some(ref allowed) = self.config.allowed_ips {
if !allowed.contains(&request.client_ip) {
return Err(Error::IpNotAllowed);
}
}
// Authentication
if self.config.require_auth {
let token = request.headers.get("Authorization")
.ok_or(Error::MissingAuth)?;
if token != &format!("Bearer {}", self.config.auth_token.as_ref().unwrap()) {
return Err(Error::InvalidAuth);
}
}
Ok(())
}
}
These controls prevent abuse:
- Rate limiting prevents DDoS
- IP whitelisting restricts access
- Authentication adds an extra security layer
🔗Production Deployment Patterns
For production deployments, TLS and mTLS are essential:
Development:
- HTTP for local development (fast iteration)
- Optional tunneling for OAuth2/webhook testing
Staging:
- TLS/HTTPS with self-signed certificates
- mTLS for internal service communication
- Tunneling for external integrations
Production:
- TLS/HTTPS with valid certificates (Let's Encrypt, etc.)
- mTLS for all service-to-service communication
- No tunneling (deploy to proper infrastructure)
The configuration supports all these scenarios:
# Development
http:
tls:
enabled: false
# Staging
http:
tls:
enabled: true
cert_file: "./staging.crt"
key_file: "./staging.key"
require_client_cert: true
ca_file: "./ca.crt"
# Production
http:
tls:
enabled: true
cert_file: "/etc/ssl/certs/server.crt"
key_file: "/etc/ssl/private/server.key"
require_client_cert: true
ca_file: "/etc/ssl/certs/ca.crt"
🔗Certificate Management
Managing certificates is still manual, but the implementation makes it straightforward:
Self-signed certificates (development/staging):
# Generate CA
openssl req -x509 -newkey rsa:4096 -days 365 -nodes \
-keyout ca.key -out ca.crt
# Generate server certificate
openssl req -newkey rsa:4096 -nodes \
-keyout server.key -out server.csr
# Sign with CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365
Production certificates: Use Let's Encrypt, your organization's CA, or a commercial certificate provider. The implementation accepts any valid PEM-formatted certificate.
🔗Performance Considerations
TLS adds overhead, but rustls is optimized for performance:
- Zero-copy TLS: Efficient memory usage
- Async I/O: Non-blocking TLS operations
- Connection pooling: Reuse TLS connections
- Session resumption: Faster reconnection
The overhead is minimal—typically 10-20ms per request, which is negligible for mock servers that aren't serving production traffic.
🔗The Bottom Line
TLS, mTLS, and tunneling transform MockForge from a development tool into a production-ready mocking framework. The implementation is:
- Secure: Safe defaults, proper certificate validation
- Flexible: Supports standard TLS and mTLS
- Production-ready: Handles real-world security requirements
- Developer-friendly: Simple configuration, built-in tunneling
For teams that need to test production-like scenarios—OAuth2 flows, HTTPS-only APIs, or services requiring mTLS—these features make MockForge a viable alternative to deploying staging environments. That's the value of production-grade security in a development tool.