Skip to content

Commit b576086

Browse files
committed
feat: Implement authentication rate limiting (fail2ban-like)
Add AuthRateLimiter with ban support to protect against brute-force attacks: - Track failed authentication attempts per IP address - Automatically ban IPs that exceed max attempts within time window - Configurable max attempts, time window, and ban duration - IP whitelist support for trusted addresses - Automatic cleanup of expired bans and failure records - Background cleanup task running every 60 seconds Configuration options added to SecurityConfig: - auth_window: Time window for counting attempts (default: 300s) - whitelist_ips: IPs exempt from rate limiting Integration with SSH handler: - Check if IP is banned before authentication - Record failures and trigger bans on threshold - Record success to reset failure counter - Logging for ban events Closes #140
1 parent 0e5bcb3 commit b576086

5 files changed

Lines changed: 841 additions & 1 deletion

File tree

src/server/config/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,28 @@ pub struct SecurityConfig {
368368
#[serde(default = "default_max_auth_attempts")]
369369
pub max_auth_attempts: u32,
370370

371+
/// Time window in seconds for counting authentication attempts.
372+
///
373+
/// Failed attempts outside this window are not counted toward the ban threshold.
374+
///
375+
/// Default: 300 (5 minutes)
376+
#[serde(default = "default_auth_window")]
377+
pub auth_window: u64,
378+
371379
/// Ban duration in seconds after exceeding max auth attempts.
372380
///
373381
/// Default: 300 (5 minutes)
374382
#[serde(default = "default_ban_time")]
375383
pub ban_time: u64,
376384

385+
/// IP addresses that are never banned (whitelist).
386+
///
387+
/// These IPs are exempt from rate limiting and banning.
388+
///
389+
/// Example: ["127.0.0.1", "::1"]
390+
#[serde(default)]
391+
pub whitelist_ips: Vec<String>,
392+
377393
/// Maximum number of concurrent sessions per user.
378394
///
379395
/// Default: 10
@@ -449,6 +465,10 @@ fn default_max_auth_attempts() -> u32 {
449465
5
450466
}
451467

468+
fn default_auth_window() -> u64 {
469+
300
470+
}
471+
452472
fn default_ban_time() -> u64 {
453473
300
454474
}
@@ -517,7 +537,9 @@ impl Default for SecurityConfig {
517537
fn default() -> Self {
518538
Self {
519539
max_auth_attempts: default_max_auth_attempts(),
540+
auth_window: default_auth_window(),
520541
ban_time: default_ban_time(),
542+
whitelist_ips: Vec::new(),
521543
max_sessions_per_user: default_max_sessions(),
522544
idle_timeout: default_idle_timeout(),
523545
allowed_ips: Vec::new(),

src/server/handler.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use super::auth::AuthProvider;
3232
use super::config::ServerConfig;
3333
use super::exec::CommandExecutor;
3434
use super::pty::PtyConfig as PtyMasterConfig;
35+
use super::security::AuthRateLimiter;
3536
use super::session::{ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager};
3637
use super::sftp::SftpHandler;
3738
use super::shell::ShellSession;
@@ -57,6 +58,9 @@ pub struct SshHandler {
5758
/// Rate limiter for authentication attempts.
5859
rate_limiter: RateLimiter<String>,
5960

61+
/// Auth rate limiter with ban support (fail2ban-like).
62+
auth_rate_limiter: Option<AuthRateLimiter>,
63+
6064
/// Session information for this connection.
6165
session_info: Option<SessionInfo>,
6266

@@ -83,6 +87,7 @@ impl SshHandler {
8387
sessions,
8488
auth_provider,
8589
rate_limiter,
90+
auth_rate_limiter: None,
8691
session_info: Some(SessionInfo::new(peer_addr)),
8792
channels: HashMap::new(),
8893
}
@@ -106,6 +111,33 @@ impl SshHandler {
106111
sessions,
107112
auth_provider,
108113
rate_limiter,
114+
auth_rate_limiter: None,
115+
session_info: Some(SessionInfo::new(peer_addr)),
116+
channels: HashMap::new(),
117+
}
118+
}
119+
120+
/// Create a new SSH handler with shared rate limiters including auth ban support.
121+
///
122+
/// This is the preferred constructor for production use as it shares
123+
/// both rate limiters across all handlers, providing server-wide rate limiting
124+
/// and fail2ban-like functionality.
125+
pub fn with_rate_limiters(
126+
peer_addr: Option<SocketAddr>,
127+
config: Arc<ServerConfig>,
128+
sessions: Arc<RwLock<SessionManager>>,
129+
rate_limiter: RateLimiter<String>,
130+
auth_rate_limiter: AuthRateLimiter,
131+
) -> Self {
132+
let auth_provider = config.create_auth_provider();
133+
134+
Self {
135+
peer_addr,
136+
config,
137+
sessions,
138+
auth_provider,
139+
rate_limiter,
140+
auth_rate_limiter: Some(auth_rate_limiter),
109141
session_info: Some(SessionInfo::new(peer_addr)),
110142
channels: HashMap::new(),
111143
}
@@ -128,6 +160,7 @@ impl SshHandler {
128160
sessions,
129161
auth_provider,
130162
rate_limiter,
163+
auth_rate_limiter: None,
131164
session_info: Some(SessionInfo::new(peer_addr)),
132165
channels: HashMap::new(),
133166
}
@@ -284,6 +317,7 @@ impl russh::server::Handler for SshHandler {
284317
// Clone what we need for the async block
285318
let auth_provider = Arc::clone(&self.auth_provider);
286319
let rate_limiter = self.rate_limiter.clone();
320+
let auth_rate_limiter = self.auth_rate_limiter.clone();
287321
let peer_addr = self.peer_addr;
288322
let user = user.to_string();
289323
let public_key = public_key.clone();
@@ -292,6 +326,23 @@ impl russh::server::Handler for SshHandler {
292326
let session_info = &mut self.session_info;
293327

294328
async move {
329+
// Check if IP is banned (fail2ban-like check)
330+
if let Some(ref limiter) = auth_rate_limiter {
331+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
332+
if limiter.is_banned(&ip).await {
333+
tracing::warn!(
334+
user = %user,
335+
peer = ?peer_addr,
336+
"Rejected auth from banned IP"
337+
);
338+
return Ok(Auth::Reject {
339+
proceed_with_methods: None,
340+
partial_success: false,
341+
});
342+
}
343+
}
344+
}
345+
295346
if exceeded {
296347
tracing::warn!(
297348
user = %user,
@@ -349,6 +400,13 @@ impl russh::server::Handler for SshHandler {
349400
info.authenticate(&user);
350401
}
351402

403+
// Record success to reset failure counter
404+
if let Some(ref limiter) = auth_rate_limiter {
405+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
406+
limiter.record_success(&ip).await;
407+
}
408+
}
409+
352410
Ok(Auth::Accept)
353411
}
354412
Ok(_) => {
@@ -359,6 +417,20 @@ impl russh::server::Handler for SshHandler {
359417
"Public key authentication rejected"
360418
);
361419

420+
// Record failure for ban tracking
421+
if let Some(ref limiter) = auth_rate_limiter {
422+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
423+
let banned = limiter.record_failure(ip).await;
424+
if banned {
425+
tracing::warn!(
426+
user = %user,
427+
peer = ?peer_addr,
428+
"IP banned due to too many failed auth attempts"
429+
);
430+
}
431+
}
432+
}
433+
362434
let proceed = if methods.is_empty() {
363435
None
364436
} else {
@@ -378,6 +450,13 @@ impl russh::server::Handler for SshHandler {
378450
"Error during public key verification"
379451
);
380452

453+
// Record failure for ban tracking
454+
if let Some(ref limiter) = auth_rate_limiter {
455+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
456+
limiter.record_failure(ip).await;
457+
}
458+
}
459+
381460
let proceed = if methods.is_empty() {
382461
None
383462
} else {
@@ -421,6 +500,7 @@ impl russh::server::Handler for SshHandler {
421500
// Clone what we need for the async block
422501
let auth_provider = Arc::clone(&self.auth_provider);
423502
let rate_limiter = self.rate_limiter.clone();
503+
let auth_rate_limiter = self.auth_rate_limiter.clone();
424504
let peer_addr = self.peer_addr;
425505
let user = user.to_string();
426506
// Use Zeroizing to ensure password is securely cleared from memory when dropped
@@ -431,6 +511,23 @@ impl russh::server::Handler for SshHandler {
431511
let session_info = &mut self.session_info;
432512

433513
async move {
514+
// Check if IP is banned (fail2ban-like check)
515+
if let Some(ref limiter) = auth_rate_limiter {
516+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
517+
if limiter.is_banned(&ip).await {
518+
tracing::warn!(
519+
user = %user,
520+
peer = ?peer_addr,
521+
"Rejected password auth from banned IP"
522+
);
523+
return Ok(Auth::Reject {
524+
proceed_with_methods: None,
525+
partial_success: false,
526+
});
527+
}
528+
}
529+
}
530+
434531
// Check if password auth is enabled
435532
if !allow_password {
436533
tracing::debug!(
@@ -504,6 +601,13 @@ impl russh::server::Handler for SshHandler {
504601
info.authenticate(&user);
505602
}
506603

604+
// Record success to reset failure counter
605+
if let Some(ref limiter) = auth_rate_limiter {
606+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
607+
limiter.record_success(&ip).await;
608+
}
609+
}
610+
507611
Ok(Auth::Accept)
508612
}
509613
Ok(_) => {
@@ -513,6 +617,20 @@ impl russh::server::Handler for SshHandler {
513617
"Password authentication rejected"
514618
);
515619

620+
// Record failure for ban tracking
621+
if let Some(ref limiter) = auth_rate_limiter {
622+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
623+
let banned = limiter.record_failure(ip).await;
624+
if banned {
625+
tracing::warn!(
626+
user = %user,
627+
peer = ?peer_addr,
628+
"IP banned due to too many failed password auth attempts"
629+
);
630+
}
631+
}
632+
}
633+
516634
let proceed = if methods.is_empty() {
517635
None
518636
} else {
@@ -532,6 +650,13 @@ impl russh::server::Handler for SshHandler {
532650
"Error during password verification"
533651
);
534652

653+
// Record failure for ban tracking
654+
if let Some(ref limiter) = auth_rate_limiter {
655+
if let Some(ip) = peer_addr.map(|a| a.ip()) {
656+
limiter.record_failure(ip).await;
657+
}
658+
}
659+
535660
let proceed = if methods.is_empty() {
536661
None
537662
} else {

src/server/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub mod config;
4949
pub mod exec;
5050
pub mod handler;
5151
pub mod pty;
52+
pub mod security;
5253
pub mod session;
5354
pub mod sftp;
5455
pub mod shell;
@@ -69,6 +70,7 @@ pub use self::config::{ServerConfig, ServerConfigBuilder};
6970
pub use self::exec::{CommandExecutor, ExecConfig};
7071
pub use self::handler::SshHandler;
7172
pub use self::pty::{PtyConfig as PtyMasterConfig, PtyMaster};
73+
pub use self::security::{AuthRateLimitConfig, AuthRateLimiter};
7274
pub use self::session::{
7375
ChannelMode, ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager,
7476
};
@@ -214,10 +216,28 @@ impl BsshServer {
214216
// This allows rapid testing while still providing protection against brute force
215217
let rate_limiter = RateLimiter::with_simple_config(100, 10.0);
216218

219+
// Create auth rate limiter with configuration
220+
let auth_rate_limiter = AuthRateLimiter::new(AuthRateLimitConfig::new(
221+
self.config.max_auth_attempts,
222+
300, // Default 5 minute window
223+
300, // Default 5 minute ban
224+
));
225+
226+
// Start background cleanup task for auth rate limiter
227+
let cleanup_limiter = auth_rate_limiter.clone();
228+
tokio::spawn(async move {
229+
let mut interval = tokio::time::interval(Duration::from_secs(60));
230+
loop {
231+
interval.tick().await;
232+
cleanup_limiter.cleanup().await;
233+
}
234+
});
235+
217236
let mut server = BsshServerRunner {
218237
config: Arc::clone(&self.config),
219238
sessions: Arc::clone(&self.sessions),
220239
rate_limiter,
240+
auth_rate_limiter,
221241
};
222242

223243
// Use run_on_socket which handles the server loop
@@ -248,6 +268,8 @@ struct BsshServerRunner {
248268
sessions: Arc<RwLock<SessionManager>>,
249269
/// Shared rate limiter for authentication attempts across all handlers
250270
rate_limiter: RateLimiter<String>,
271+
/// Auth rate limiter with ban support (fail2ban-like)
272+
auth_rate_limiter: AuthRateLimiter,
251273
}
252274

253275
impl russh::server::Server for BsshServerRunner {
@@ -259,11 +281,12 @@ impl russh::server::Server for BsshServerRunner {
259281
"New client connection"
260282
);
261283

262-
SshHandler::with_rate_limiter(
284+
SshHandler::with_rate_limiters(
263285
peer_addr,
264286
Arc::clone(&self.config),
265287
Arc::clone(&self.sessions),
266288
self.rate_limiter.clone(),
289+
self.auth_rate_limiter.clone(),
267290
)
268291
}
269292

0 commit comments

Comments
 (0)