fix: enforce IP whitelist middleware in request pipeline (closes #11)
Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 3s
CI/CD Pipeline / Clippy Lints (push) Successful in 41s
CI/CD Pipeline / All Unit Tests (push) Successful in 1m9s
CI/CD Pipeline / Security Audit (push) Successful in 5s
CI/CD Pipeline / Enrollment Tests (push) Successful in 1m11s
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Successful in 56s
CI/CD Pipeline / Build Debian Package (push) Failing after 3s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Failing after 4s
CI/CD Pipeline / Build RPM Package (push) Successful in 2m13s
CI/CD Pipeline / Build Arch Package (push) Successful in 2m17s
CI/CD Pipeline / Build Alpine Package (push) Failing after 3m13s
Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 3s
CI/CD Pipeline / Clippy Lints (push) Successful in 41s
CI/CD Pipeline / All Unit Tests (push) Successful in 1m9s
CI/CD Pipeline / Security Audit (push) Successful in 5s
CI/CD Pipeline / Enrollment Tests (push) Successful in 1m11s
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Successful in 56s
CI/CD Pipeline / Build Debian Package (push) Failing after 3s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Failing after 4s
CI/CD Pipeline / Build RPM Package (push) Successful in 2m13s
CI/CD Pipeline / Build Arch Package (push) Successful in 2m17s
CI/CD Pipeline / Build Alpine Package (push) Failing after 3m13s
Co-authored-by: git-echo <git-echo@moon-dragon.us>
This commit is contained in:
committed by
GitHub
parent
130206a3a3
commit
d0c0790cbf
@ -12,7 +12,10 @@ pub mod whitelist;
|
|||||||
|
|
||||||
pub use crl::{new_shared_state, CrlState, CrlStatus, SharedCrlState};
|
pub use crl::{new_shared_state, CrlState, CrlStatus, SharedCrlState};
|
||||||
pub use mtls::{ClientCertInfo, MtlsConfig, MtlsError, MtlsMiddleware};
|
pub use mtls::{ClientCertInfo, MtlsConfig, MtlsError, MtlsMiddleware};
|
||||||
pub use whitelist::{WhitelistConfig, WhitelistEntry, WhitelistManager, WhitelistMiddleware};
|
pub use whitelist::{
|
||||||
|
WhitelistConfig, WhitelistEntry, WhitelistManager, WhitelistMiddleware,
|
||||||
|
WhitelistMiddlewareService,
|
||||||
|
};
|
||||||
|
|
||||||
/// Combined authentication result
|
/// Combined authentication result
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@ -17,6 +17,12 @@ use std::sync::{Arc, RwLock};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
use futures_util::future::LocalBoxFuture;
|
||||||
|
|
||||||
/// Whitelist entry types
|
/// Whitelist entry types
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum WhitelistEntry {
|
pub enum WhitelistEntry {
|
||||||
@ -282,6 +288,18 @@ impl WhitelistManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a deny-all whitelist manager (fail-closed fallback).
|
||||||
|
///
|
||||||
|
/// Used when the whitelist file cannot be loaded — all IPs are denied
|
||||||
|
/// except health endpoints (handled at middleware level).
|
||||||
|
pub fn new_deny_all() -> Self {
|
||||||
|
Self {
|
||||||
|
entries: Arc::new(RwLock::new(HashSet::new())),
|
||||||
|
config_path: String::new(),
|
||||||
|
watcher: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the number of entries in the whitelist
|
/// Get the number of entries in the whitelist
|
||||||
pub fn entry_count(&self) -> usize {
|
pub fn entry_count(&self) -> usize {
|
||||||
self.entries.read().unwrap().len()
|
self.entries.read().unwrap().len()
|
||||||
@ -426,11 +444,9 @@ pub struct WhitelistMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WhitelistMiddleware {
|
impl WhitelistMiddleware {
|
||||||
/// Create a new whitelist middleware
|
/// Create a new whitelist middleware from an Arc<WhitelistManager>
|
||||||
pub fn new(manager: WhitelistManager) -> Self {
|
pub fn new(manager: Arc<WhitelistManager>) -> Self {
|
||||||
Self {
|
Self { manager }
|
||||||
manager: Arc::new(manager),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the whitelist manager reference
|
/// Get the whitelist manager reference
|
||||||
@ -439,6 +455,99 @@ impl WhitelistMiddleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Actix-web Transform implementation — wraps WhitelistMiddleware as middleware
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for WhitelistMiddleware
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type InitError = ();
|
||||||
|
type Transform = WhitelistMiddlewareService<S>;
|
||||||
|
type Future = futures_util::future::Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
futures_util::future::ok(WhitelistMiddlewareService {
|
||||||
|
service,
|
||||||
|
manager: self.manager.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whitelist middleware service — performs per-request IP checks
|
||||||
|
pub struct WhitelistMiddlewareService<S> {
|
||||||
|
service: S,
|
||||||
|
manager: Arc<WhitelistManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Health/system endpoint paths exempt from IP whitelist enforcement
|
||||||
|
const WHITELIST_EXEMPT_PATHS: &[&str] = &["/health", "/api/v1/system/info"];
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for WhitelistMiddlewareService<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let path = req.path().to_owned();
|
||||||
|
|
||||||
|
// Exempt health and system info endpoints from IP whitelist
|
||||||
|
if WHITELIST_EXEMPT_PATHS.iter().any(|p| path == *p) {
|
||||||
|
debug!(path = %path, "Path exempt from IP whitelist");
|
||||||
|
let fut = self.service.call(req);
|
||||||
|
return Box::pin(fut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get peer address — fail-closed if unavailable
|
||||||
|
let peer_addr = req.peer_addr();
|
||||||
|
match peer_addr {
|
||||||
|
Some(addr) => {
|
||||||
|
if self.manager.is_socket_allowed(&addr) {
|
||||||
|
debug!(
|
||||||
|
peer_addr = %addr,
|
||||||
|
path = %path,
|
||||||
|
"IP whitelist check passed"
|
||||||
|
);
|
||||||
|
let fut = self.service.call(req);
|
||||||
|
Box::pin(fut)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
peer_addr = %addr,
|
||||||
|
path = %path,
|
||||||
|
"IP whitelist denied - connection rejected"
|
||||||
|
);
|
||||||
|
Box::pin(async move {
|
||||||
|
Err(actix_web::error::ErrorForbidden(
|
||||||
|
"IP address not in whitelist",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// No peer address — fail-closed (deny by default)
|
||||||
|
warn!(
|
||||||
|
path = %path,
|
||||||
|
"No peer address available - denying request (fail-closed)"
|
||||||
|
);
|
||||||
|
Box::pin(async move {
|
||||||
|
Err(actix_web::error::ErrorForbidden(
|
||||||
|
"IP address not available - denied by policy",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -511,4 +620,33 @@ mod tests {
|
|||||||
let ip_outside: Ipv4Addr = "192.168.2.100".parse().unwrap();
|
let ip_outside: Ipv4Addr = "192.168.2.100".parse().unwrap();
|
||||||
assert!(!manager.is_allowed(&ip_outside));
|
assert!(!manager.is_allowed(&ip_outside));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_deny_all_blocks_everything() {
|
||||||
|
let manager = WhitelistManager::new_deny_all();
|
||||||
|
// No IPs should be allowed in deny-all mode
|
||||||
|
let ip: Ipv4Addr = "192.168.1.1".parse().unwrap();
|
||||||
|
assert!(!manager.is_allowed(&ip));
|
||||||
|
|
||||||
|
let ip2: Ipv4Addr = "10.0.0.1".parse().unwrap();
|
||||||
|
assert!(!manager.is_allowed(&ip2));
|
||||||
|
|
||||||
|
// Entry count should be 0
|
||||||
|
assert_eq!(manager.entry_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_socket_allowed_ipv6_denied() {
|
||||||
|
let manager = WhitelistManager::new_deny_all();
|
||||||
|
// IPv6 should be denied even with a populated whitelist
|
||||||
|
let socket_v6: SocketAddr = "[::1]:12345".parse().unwrap();
|
||||||
|
assert!(!manager.is_socket_allowed(&socket_v6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exempt_paths_constant() {
|
||||||
|
// Verify the exempt paths include health and system info
|
||||||
|
assert!(WHITELIST_EXEMPT_PATHS.contains(&"/health"));
|
||||||
|
assert!(WHITELIST_EXEMPT_PATHS.contains(&"/api/v1/system/info"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@ -28,7 +28,7 @@ use tracing::{error, info, warn};
|
|||||||
|
|
||||||
use linux_patch_api::api::{configure_api_routes, configure_health_route};
|
use linux_patch_api::api::{configure_api_routes, configure_health_route};
|
||||||
use linux_patch_api::auth::crl::{self, CrlStatus};
|
use linux_patch_api::auth::crl::{self, CrlStatus};
|
||||||
use linux_patch_api::auth::{mtls, MtlsMiddleware, WhitelistManager};
|
use linux_patch_api::auth::{mtls, MtlsMiddleware, WhitelistManager, WhitelistMiddleware};
|
||||||
use linux_patch_api::config::loader::{validate_certs, CertStatus};
|
use linux_patch_api::config::loader::{validate_certs, CertStatus};
|
||||||
use linux_patch_api::enroll;
|
use linux_patch_api::enroll;
|
||||||
use linux_patch_api::packages::cache::PackageCacheState;
|
use linux_patch_api::packages::cache::PackageCacheState;
|
||||||
@ -282,11 +282,12 @@ async fn main() -> Result<()> {
|
|||||||
entries = manager.entry_count(),
|
entries = manager.entry_count(),
|
||||||
"Whitelist manager initialized"
|
"Whitelist manager initialized"
|
||||||
);
|
);
|
||||||
Some(Arc::new(manager))
|
Arc::new(manager)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(error = %e, "Failed to load whitelist - continuing with empty whitelist (all denied)");
|
// Fail-closed: deny all IPs when whitelist cannot be loaded
|
||||||
None
|
warn!(error = %e, "Failed to load whitelist - using deny-all mode (fail-closed)");
|
||||||
|
Arc::new(WhitelistManager::new_deny_all())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -305,9 +306,13 @@ async fn main() -> Result<()> {
|
|||||||
// Configure bind address
|
// Configure bind address
|
||||||
let bind_address = format!("{}:{}", config.server.bind, config.server.port);
|
let bind_address = format!("{}:{}", config.server.bind, config.server.port);
|
||||||
|
|
||||||
|
// Clone whitelist manager for use inside the HttpServer closure
|
||||||
|
let wl = whitelist_manager.clone();
|
||||||
|
|
||||||
// Create server builder
|
// Create server builder
|
||||||
let server_builder = HttpServer::new(move || {
|
let server_builder = HttpServer::new(move || {
|
||||||
let mut app = App::new()
|
let mut app = App::new()
|
||||||
|
.wrap(WhitelistMiddleware::new(wl.clone()))
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.app_data(job_manager_data.clone())
|
.app_data(job_manager_data.clone())
|
||||||
.app_data(backend_data.clone())
|
.app_data(backend_data.clone())
|
||||||
@ -338,8 +343,8 @@ async fn main() -> Result<()> {
|
|||||||
.max_connection_rate(1000);
|
.max_connection_rate(1000);
|
||||||
info!(
|
info!(
|
||||||
mtls_enabled = config.tls_config().is_some(),
|
mtls_enabled = config.tls_config().is_some(),
|
||||||
whitelist_enabled = whitelist_manager.is_some(),
|
whitelist_entries = whitelist_manager.entry_count(),
|
||||||
"Security layer status"
|
"Security layer status (IP whitelist enforced)"
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("Linux Patch API initialized successfully");
|
info!("Linux Patch API initialized successfully");
|
||||||
|
|||||||
Reference in New Issue
Block a user