Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ Security features for the SSH server (`src/server/security/`):
- Automatic cleanup of expired records via background task
- Thread-safe async implementation with `Arc<RwLock<>>`

- **IpAccessControl**: IP-based connection filtering
- Whitelist mode: Only allow connections from specified CIDR ranges
- Blacklist mode: Block connections from specified CIDR ranges
- Blacklist takes priority over whitelist (blocked IPs are always denied)
- Support for both IPv4 and IPv6 addresses and CIDR notation
- Dynamic updates: Add/remove rules at runtime via `SharedIpAccessControl`
- Early rejection at connection level before handler creation
- Thread-safe with fail-closed behavior on lock contention
- Configuration via `allowed_ips` and `blocked_ips` in server config

### Server CLI Binary
**Binary**: `bssh-server`

Expand Down
31 changes: 31 additions & 0 deletions docs/architecture/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,46 @@ security:
idle_timeout: 3600 # Default: 3600 (1 hour)

# IP allowlist (CIDR notation, empty = allow all)
# When configured, only connections from these ranges are allowed
allowed_ips:
- "192.168.1.0/24"
- "10.0.0.0/8"

# IP blocklist (CIDR notation)
# Connections from these ranges are always denied
# Blocked IPs take priority over allowed IPs
blocked_ips:
- "203.0.113.0/24"
```

### IP Access Control

The server supports IP-based connection filtering through `allowed_ips` and `blocked_ips` configuration options:

**Modes of Operation:**

1. **Default Mode** (no `allowed_ips` configured): All IPs are allowed unless explicitly blocked
2. **Whitelist Mode** (`allowed_ips` configured): Only IPs matching allowed ranges can connect

**Priority Rules:**
- Blocked IPs always take priority over allowed IPs
- If an IP matches both `allowed_ips` and `blocked_ips`, the connection is denied
- Connections from blocked IPs are rejected before authentication

**CIDR Notation Examples:**
- `10.0.0.0/8` - All 10.x.x.x addresses (Class A private network)
- `192.168.1.0/24` - All 192.168.1.x addresses
- `192.168.100.50/32` - Single IP address (192.168.100.50)
- `2001:db8::/32` - IPv6 prefix

**Runtime Updates:**
The IP access control supports dynamic updates at runtime through the `SharedIpAccessControl` API, allowing administrators to block or unblock IPs without restarting the server.

**Security Behavior:**
- Connections from blocked IPs are rejected at the connection level before any authentication attempt
- On lock contention (rare), the system defaults to DENY for fail-closed security
- All access control decisions are logged for auditing

## Environment Variable Overrides

The following environment variables can override configuration file settings:
Expand Down
18 changes: 18 additions & 0 deletions src/server/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ pub struct ServerConfig {
/// IP addresses that are never banned (whitelist).
#[serde(default)]
pub whitelist_ips: Vec<String>,

/// Allowed IP ranges in CIDR notation for connection filtering.
///
/// If non-empty, only connections from these ranges are allowed.
/// Empty list means all IPs are allowed (subject to blocked_ips).
#[serde(default)]
pub allowed_ips: Vec<String>,

/// Blocked IP ranges in CIDR notation for connection filtering.
///
/// Connections from these ranges are always denied.
/// Blocked IPs take priority over allowed IPs.
#[serde(default)]
pub blocked_ips: Vec<String>,
}

/// Serializable configuration for public key authentication.
Expand Down Expand Up @@ -260,6 +274,8 @@ impl Default for ServerConfig {
auth_window_secs: default_auth_window_secs(),
ban_time_secs: default_ban_time_secs(),
whitelist_ips: Vec::new(),
allowed_ips: Vec::new(),
blocked_ips: Vec::new(),
}
}
}
Expand Down Expand Up @@ -551,6 +567,8 @@ impl ServerFileConfig {
auth_window_secs: self.security.auth_window,
ban_time_secs: self.security.ban_time,
whitelist_ips: self.security.whitelist_ips,
allowed_ips: self.security.allowed_ips,
blocked_ips: self.security.blocked_ips,
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions src/server/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ pub struct SshHandler {

/// Active channels for this connection.
channels: HashMap<ChannelId, ChannelState>,

/// Whether this connection should be immediately rejected.
/// Set when IP access control denies the connection.
rejected: bool,
}

impl SshHandler {
Expand All @@ -90,6 +94,7 @@ impl SshHandler {
auth_rate_limiter: None,
session_info: Some(SessionInfo::new(peer_addr)),
channels: HashMap::new(),
rejected: false,
}
}

Expand All @@ -114,6 +119,7 @@ impl SshHandler {
auth_rate_limiter: None,
session_info: Some(SessionInfo::new(peer_addr)),
channels: HashMap::new(),
rejected: false,
}
}

Expand All @@ -140,6 +146,7 @@ impl SshHandler {
auth_rate_limiter: Some(auth_rate_limiter),
session_info: Some(SessionInfo::new(peer_addr)),
channels: HashMap::new(),
rejected: false,
}
}

Expand All @@ -163,6 +170,32 @@ impl SshHandler {
auth_rate_limiter: None,
session_info: Some(SessionInfo::new(peer_addr)),
channels: HashMap::new(),
rejected: false,
}
}

/// Create a handler for a rejected connection.
///
/// This handler will immediately reject all authentication attempts.
/// Used when IP access control denies a connection.
pub fn rejected(
peer_addr: Option<SocketAddr>,
config: Arc<ServerConfig>,
sessions: Arc<RwLock<SessionManager>>,
) -> Self {
let auth_provider = config.create_auth_provider();
let rate_limiter = RateLimiter::with_simple_config(1, 0.1);

Self {
peer_addr,
config,
sessions,
auth_provider,
rate_limiter,
auth_rate_limiter: None,
session_info: None, // No session for rejected connections
channels: HashMap::new(),
rejected: true,
}
}

Expand Down Expand Up @@ -246,6 +279,19 @@ impl russh::server::Handler for SshHandler {
"Auth none attempt"
);

// If connection was rejected by IP access control, immediately reject
if self.rejected {
tracing::debug!(
peer = ?self.peer_addr,
"Rejecting auth for IP-blocked connection"
);
return std::future::ready(Ok(Auth::Reject {
proceed_with_methods: None,
partial_success: false,
}))
.left_future();
}

// Create session info if not already created
let peer_addr = self.peer_addr;
let sessions = Arc::clone(&self.sessions);
Expand Down Expand Up @@ -287,6 +333,7 @@ impl russh::server::Handler for SshHandler {
partial_success: false,
})
}
.right_future()
}

/// Handle public key authentication.
Expand Down
53 changes: 52 additions & 1 deletion src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ pub use self::config::{ServerConfig, ServerConfigBuilder};
pub use self::exec::{CommandExecutor, ExecConfig};
pub use self::handler::SshHandler;
pub use self::pty::{PtyConfig as PtyMasterConfig, PtyMaster};
pub use self::security::{AuthRateLimitConfig, AuthRateLimiter};
pub use self::security::{
AccessPolicy, AuthRateLimitConfig, AuthRateLimiter, IpAccessControl, SharedIpAccessControl,
};
pub use self::session::{
ChannelMode, ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager,
};
Expand Down Expand Up @@ -247,6 +249,13 @@ impl BsshServer {
"Auth rate limiter configured"
);

// Create IP access control from configuration
let ip_access_control =
IpAccessControl::from_config(&self.config.allowed_ips, &self.config.blocked_ips)
.context("Failed to configure IP access control")?;

let shared_ip_access = SharedIpAccessControl::new(ip_access_control);

// Start background cleanup task for auth rate limiter
let cleanup_limiter = auth_rate_limiter.clone();
tokio::spawn(async move {
Expand All @@ -262,6 +271,7 @@ impl BsshServer {
sessions: Arc::clone(&self.sessions),
rate_limiter,
auth_rate_limiter,
ip_access_control: shared_ip_access,
};

// Use run_on_socket which handles the server loop
Expand Down Expand Up @@ -294,12 +304,53 @@ struct BsshServerRunner {
rate_limiter: RateLimiter<String>,
/// Auth rate limiter with ban support (fail2ban-like)
auth_rate_limiter: AuthRateLimiter,
/// IP-based access control
ip_access_control: SharedIpAccessControl,
}

impl russh::server::Server for BsshServerRunner {
type Handler = SshHandler;

fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler {
// Check IP access control before creating handler
if let Some(addr) = peer_addr {
let ip = addr.ip();

// Check IP access control (synchronous to avoid blocking)
if self.ip_access_control.check_sync(&ip) == AccessPolicy::Deny {
tracing::info!(
ip = %ip,
"Connection rejected by IP access control"
);
// Return a handler that will immediately reject
// We can't return None here due to trait constraints,
// so we'll mark it for rejection in the handler
return SshHandler::rejected(
peer_addr,
Arc::clone(&self.config),
Arc::clone(&self.sessions),
);
}

// Check if banned by auth rate limiter
// Use try_read to avoid blocking in sync context
if let Ok(is_banned) = tokio::runtime::Handle::try_current()
.map(|h| h.block_on(self.auth_rate_limiter.is_banned(&ip)))
{
if is_banned {
tracing::info!(
ip = %ip,
"Connection rejected from banned IP"
);
return SshHandler::rejected(
peer_addr,
Arc::clone(&self.config),
Arc::clone(&self.sessions),
);
}
}
}

tracing::info!(
peer = ?peer_addr,
"New client connection"
Expand Down
Loading