diff --git a/Cargo.lock b/Cargo.lock index 10ebc53..e2d9c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,7 +1201,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "nora-cli" -version = "0.2.22" +version = "0.2.24" dependencies = [ "clap", "flate2", @@ -1215,7 +1215,7 @@ dependencies = [ [[package]] name = "nora-registry" -version = "0.2.22" +version = "0.2.24" dependencies = [ "async-trait", "axum", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "nora-storage" -version = "0.2.22" +version = "0.2.24" dependencies = [ "axum", "base64", diff --git a/nora-registry/src/config.rs b/nora-registry/src/config.rs index 61a0e4d..0c1eeec 100644 --- a/nora-registry/src/config.rs +++ b/nora-registry/src/config.rs @@ -249,6 +249,8 @@ impl Default for AuthConfig { /// - `NORA_RATE_LIMIT_GENERAL_BURST` - General burst size #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RateLimitConfig { + #[serde(default = "default_rate_limit_enabled")] + pub enabled: bool, #[serde(default = "default_auth_rps")] pub auth_rps: u64, #[serde(default = "default_auth_burst")] @@ -263,6 +265,9 @@ pub struct RateLimitConfig { pub general_burst: u32, } +fn default_rate_limit_enabled() -> bool { + true +} fn default_auth_rps() -> u64 { 1 } @@ -285,6 +290,7 @@ fn default_general_burst() -> u32 { impl Default for RateLimitConfig { fn default() -> Self { Self { + enabled: default_rate_limit_enabled(), auth_rps: default_auth_rps(), auth_burst: default_auth_burst(), upload_rps: default_upload_rps(), @@ -426,6 +432,9 @@ impl Config { } // Rate limit config + if let Ok(val) = env::var("NORA_RATE_LIMIT_ENABLED") { + self.rate_limit.enabled = val.to_lowercase() == "true" || val == "1"; + } if let Ok(val) = env::var("NORA_RATE_LIMIT_AUTH_RPS") { if let Ok(v) = val.parse::() { self.rate_limit.auth_rps = v; diff --git a/nora-registry/src/main.rs b/nora-registry/src/main.rs index 8f8ba46..75b2b1a 100644 --- a/nora-registry/src/main.rs +++ b/nora-registry/src/main.rs @@ -210,6 +210,7 @@ async fn run_server(config: Config, storage: Storage) { // Log rate limiting configuration info!( + enabled = config.rate_limit.enabled, auth_rps = config.rate_limit.auth_rps, auth_burst = config.rate_limit.auth_burst, upload_rps = config.rate_limit.upload_rps, @@ -264,16 +265,49 @@ async fn run_server(config: Config, storage: Storage) { None }; - // Create rate limiters before moving config to state - let auth_limiter = rate_limit::auth_rate_limiter(&config.rate_limit); - let upload_limiter = rate_limit::upload_rate_limiter(&config.rate_limit); - let general_limiter = rate_limit::general_rate_limiter(&config.rate_limit); + let rate_limit_enabled = config.rate_limit.enabled; // Initialize Docker auth with proxy timeout let docker_auth = registry::DockerAuth::new(config.docker.proxy_timeout); let http_client = reqwest::Client::new(); + // Registry routes (shared between rate-limited and non-limited paths) + let registry_routes = Router::new() + .merge(registry::docker_routes()) + .merge(registry::maven_routes()) + .merge(registry::npm_routes()) + .merge(registry::cargo_routes()) + .merge(registry::pypi_routes()) + .merge(registry::raw_routes()); + + // Routes WITHOUT rate limiting (health, metrics, UI) + let public_routes = Router::new() + .merge(health::routes()) + .merge(metrics::routes()) + .merge(ui::routes()) + .merge(openapi::routes()); + + let app_routes = if rate_limit_enabled { + // Create rate limiters before moving config to state + let auth_limiter = rate_limit::auth_rate_limiter(&config.rate_limit); + let upload_limiter = rate_limit::upload_rate_limiter(&config.rate_limit); + let general_limiter = rate_limit::general_rate_limiter(&config.rate_limit); + + let auth_routes = auth::token_routes().layer(auth_limiter); + let limited_registry = registry_routes.layer(upload_limiter); + + Router::new() + .merge(auth_routes) + .merge(limited_registry) + .layer(general_limiter) + } else { + info!("Rate limiting DISABLED"); + Router::new() + .merge(auth::token_routes()) + .merge(registry_routes) + }; + let state = Arc::new(AppState { storage, config, @@ -287,35 +321,9 @@ async fn run_server(config: Config, storage: Storage) { http_client, }); - // Token routes with strict rate limiting (brute-force protection) - let auth_routes = auth::token_routes().layer(auth_limiter); - - // Registry routes with upload rate limiting - let registry_routes = Router::new() - .merge(registry::docker_routes()) - .merge(registry::maven_routes()) - .merge(registry::npm_routes()) - .merge(registry::cargo_routes()) - .merge(registry::pypi_routes()) - .merge(registry::raw_routes()) - .layer(upload_limiter); - - // Routes WITHOUT rate limiting (health, metrics, UI) - let public_routes = Router::new() - .merge(health::routes()) - .merge(metrics::routes()) - .merge(ui::routes()) - .merge(openapi::routes()); - - // Routes WITH rate limiting - let rate_limited_routes = Router::new() - .merge(auth_routes) - .merge(registry_routes) - .layer(general_limiter); - let app = Router::new() .merge(public_routes) - .merge(rate_limited_routes) + .merge(app_routes) .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) // 100MB default body limit .layer(middleware::from_fn(request_id::request_id_middleware)) .layer(middleware::from_fn(metrics::metrics_middleware)) diff --git a/nora-registry/src/rate_limit.rs b/nora-registry/src/rate_limit.rs index 8d59452..807d211 100644 --- a/nora-registry/src/rate_limit.rs +++ b/nora-registry/src/rate_limit.rs @@ -10,6 +10,7 @@ use crate::config::RateLimitConfig; use tower_governor::governor::GovernorConfigBuilder; +use tower_governor::key_extractor::SmartIpKeyExtractor; /// Create rate limiter layer for auth endpoints (strict protection against brute-force) pub fn auth_rate_limiter( @@ -35,11 +36,12 @@ pub fn auth_rate_limiter( pub fn upload_rate_limiter( config: &RateLimitConfig, ) -> tower_governor::GovernorLayer< - tower_governor::key_extractor::PeerIpKeyExtractor, + SmartIpKeyExtractor, governor::middleware::StateInformationMiddleware, axum::body::Body, > { let gov_config = GovernorConfigBuilder::default() + .key_extractor(SmartIpKeyExtractor) .per_second(config.upload_rps) .burst_size(config.upload_burst) .use_headers() @@ -53,11 +55,12 @@ pub fn upload_rate_limiter( pub fn general_rate_limiter( config: &RateLimitConfig, ) -> tower_governor::GovernorLayer< - tower_governor::key_extractor::PeerIpKeyExtractor, + SmartIpKeyExtractor, governor::middleware::StateInformationMiddleware, axum::body::Body, > { let gov_config = GovernorConfigBuilder::default() + .key_extractor(SmartIpKeyExtractor) .per_second(config.general_rps) .burst_size(config.general_burst) .use_headers() @@ -102,6 +105,7 @@ mod tests { #[test] fn test_custom_config() { let config = RateLimitConfig { + enabled: true, auth_rps: 10, auth_burst: 20, upload_rps: 500,