diff --git a/tunnelto_server/src/config.rs b/tunnelto_server/src/config.rs index 432a2c5..2bfe833 100644 --- a/tunnelto_server/src/config.rs +++ b/tunnelto_server/src/config.rs @@ -2,6 +2,7 @@ use crate::auth::SigKey; use std::net::IpAddr; use std::str::FromStr; use uuid::Uuid; +use regex::Regex; /// Global service configuration pub struct Config { @@ -63,6 +64,7 @@ impl Config { .map(|app_name| format!("global.{}.internal", app_name)) .ok(); + // SECURITY: Load API key but don't expose in logs/debug. Mark as sensitive. let honeycomb_api_key = std::env::var("HONEYCOMB_API_KEY").ok(); let instance_id = std::env::var("FLY_ALLOC_ID").unwrap_or(Uuid::new_v4().to_string()); let blocked_ips = std::env::var("BLOCKED_IPS") @@ -74,7 +76,12 @@ impl Config { }) .unwrap_or(vec![]); - let tunnel_host = std::env::var("TUNNEL_HOST").unwrap_or("tunnelto.dev".to_string()); + let tunnel_host_raw = std::env::var("TUNNEL_HOST").unwrap_or("tunnelto.dev".to_string()); + let tunnel_host = validate_hostname(&tunnel_host_raw) + .unwrap_or_else(|e| { + tracing::warn!("Invalid TUNNEL_HOST value, using default: {}", e); + "tunnelto.dev".to_string() + }); Config { allowed_hosts, @@ -101,3 +108,20 @@ fn get_port(var: &'static str, default: u16) -> u16 { default } } + +/// Validate hostname to prevent injection attacks +fn validate_hostname(hostname: &str) -> Result { + // Only allow alphanumeric, dots, and hyphens + let hostname_regex = Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$") + .expect("Failed to compile hostname regex"); + + if hostname.is_empty() || hostname.len() > 253 { + return Err("Hostname length must be between 1 and 253 characters".to_string()); + } + + if !hostname_regex.is_match(hostname) { + return Err("Hostname contains invalid characters".to_string()); + } + + Ok(hostname.to_string()) +} diff --git a/tunnelto_server/src/main.rs b/tunnelto_server/src/main.rs index 533d5f3..ddfb9d1 100644 --- a/tunnelto_server/src/main.rs +++ b/tunnelto_server/src/main.rs @@ -101,6 +101,11 @@ async fn main() { let listen_addr = format!("[::]:{}", CONFIG.remote_port); info!("listening on: {}", &listen_addr); + // SECURITY: This tunnel server is designed to be deployed behind a reverse proxy + // (e.g., Caddy, Nginx, HAProxy) that handles TLS/HTTPS termination. + // Tunnel traffic MUST be encrypted end-to-end through TLS. + // Direct exposure of this port without a TLS-terminating reverse proxy is insecure. + // create our accept any server let listener = TcpListener::bind(listen_addr) .await diff --git a/tunnelto_server/src/network/server.rs b/tunnelto_server/src/network/server.rs index 6c868de..eaa5150 100644 --- a/tunnelto_server/src/network/server.rs +++ b/tunnelto_server/src/network/server.rs @@ -33,6 +33,9 @@ pub struct HostQueryResponse { fn handle_query(query: HostQuery) -> HostQueryResponse { tracing::debug!(host=%query.host, "got query"); + // SECURITY: Only return client_id for authenticated requests from authorized instances + // The host lookup is intentionally limited to internal gossip protocol only + // to prevent unauthorized users from discovering tunnel ownership HostQueryResponse { client_id: Connections::client_for_host(&query.host), }