On this page

🔗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:

  1. Require client certificates on connection
  2. Verify client certificates against the CA certificate store
  3. 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:

  1. Creates a public URL that forwards to your local server
  2. Handles connection management and reconnection
  3. Provides rate limiting and security controls
  4. 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.