@@ -1253,6 +1253,13 @@ async fn resolve_and_check_allowed_ips(
12531253 ) ) ;
12541254 }
12551255
1256+ // Block control-plane ports regardless of IP match.
1257+ if BLOCKED_CONTROL_PLANE_PORTS . contains ( & port) {
1258+ return Err ( format ! (
1259+ "port {port} is a blocked control-plane port, connection rejected"
1260+ ) ) ;
1261+ }
1262+
12561263 for addr in & addrs {
12571264 // Always block loopback and link-local
12581265 if is_always_blocked_ip ( addr. ip ( ) ) {
@@ -1275,11 +1282,27 @@ async fn resolve_and_check_allowed_ips(
12751282 Ok ( addrs)
12761283}
12771284
1285+ /// Minimum CIDR prefix length before logging a breadth warning.
1286+ /// CIDRs broader than /16 (65,536+ addresses) may unintentionally expose
1287+ /// control-plane services on the same network.
1288+ const MIN_SAFE_PREFIX_LEN : u8 = 16 ;
1289+
1290+ /// Ports that are always blocked in `resolve_and_check_allowed_ips`, even
1291+ /// when the resolved IP matches an `allowed_ips` entry. These ports belong
1292+ /// to control-plane services that should never be reachable from a sandbox.
1293+ const BLOCKED_CONTROL_PLANE_PORTS : & [ u16 ] = & [
1294+ 2379 , // etcd client
1295+ 2380 , // etcd peer
1296+ 6443 , // Kubernetes API server
1297+ 10250 , // kubelet API
1298+ 10255 , // kubelet read-only
1299+ ] ;
1300+
12781301/// Parse CIDR/IP strings into `IpNet` values, rejecting invalid entries and
12791302/// entries that cover loopback or link-local ranges.
12801303///
12811304/// Returns parsed networks on success, or an error describing which entries
1282- /// are invalid.
1305+ /// are invalid. Logs a warning for overly broad CIDRs.
12831306fn parse_allowed_ips ( raw : & [ String ] ) -> std:: result:: Result < Vec < ipnet:: IpNet > , String > {
12841307 let mut nets = Vec :: with_capacity ( raw. len ( ) ) ;
12851308 let mut errors = Vec :: new ( ) ;
@@ -1297,7 +1320,17 @@ fn parse_allowed_ips(raw: &[String]) -> std::result::Result<Vec<ipnet::IpNet>, S
12971320 } ) ;
12981321
12991322 match parsed {
1300- Ok ( n) => nets. push ( n) ,
1323+ Ok ( n) => {
1324+ if n. prefix_len ( ) < MIN_SAFE_PREFIX_LEN {
1325+ warn ! (
1326+ cidr = %n,
1327+ prefix_len = n. prefix_len( ) ,
1328+ "allowed_ips entry has a very broad CIDR (< /{MIN_SAFE_PREFIX_LEN}); \
1329+ this may expose control-plane services on the same network"
1330+ ) ;
1331+ }
1332+ nets. push ( n) ;
1333+ }
13011334 Err ( _) => errors. push ( format ! ( "invalid CIDR/IP in allowed_ips: {entry}" ) ) ,
13021335 }
13031336 }
@@ -2325,6 +2358,42 @@ mod tests {
23252358 ) ;
23262359 }
23272360
2361+ // --- SEC-005: CIDR breadth warning and control-plane port blocklist ---
2362+
2363+ #[ tokio:: test]
2364+ async fn test_resolve_check_allowed_ips_blocks_control_plane_ports ( ) {
2365+ let nets = parse_allowed_ips ( & [ "0.0.0.0/0" . to_string ( ) ] ) . unwrap ( ) ;
2366+ // K8s API server port
2367+ let result = resolve_and_check_allowed_ips ( "8.8.8.8" , 6443 , & nets) . await ;
2368+ assert ! ( result. is_err( ) ) ;
2369+ assert ! ( result. unwrap_err( ) . contains( "blocked control-plane port" ) ) ;
2370+
2371+ // etcd client port
2372+ let result = resolve_and_check_allowed_ips ( "8.8.8.8" , 2379 , & nets) . await ;
2373+ assert ! ( result. is_err( ) ) ;
2374+ assert ! ( result. unwrap_err( ) . contains( "blocked control-plane port" ) ) ;
2375+
2376+ // kubelet API port
2377+ let result = resolve_and_check_allowed_ips ( "8.8.8.8" , 10250 , & nets) . await ;
2378+ assert ! ( result. is_err( ) ) ;
2379+ assert ! ( result. unwrap_err( ) . contains( "blocked control-plane port" ) ) ;
2380+ }
2381+
2382+ #[ tokio:: test]
2383+ async fn test_resolve_check_allowed_ips_allows_non_control_plane_ports ( ) {
2384+ // Port 443 should not be blocked by the control-plane port list
2385+ let nets = parse_allowed_ips ( & [ "8.8.8.0/24" . to_string ( ) ] ) . unwrap ( ) ;
2386+ let result = resolve_and_check_allowed_ips ( "8.8.8.8" , 443 , & nets) . await ;
2387+ assert ! ( result. is_ok( ) ) ;
2388+ }
2389+
2390+ #[ test]
2391+ fn test_parse_allowed_ips_broad_cidr_is_accepted ( ) {
2392+ // Broad CIDRs are accepted (just warned about) -- design trade-off
2393+ let result = parse_allowed_ips ( & [ "10.0.0.0/8" . to_string ( ) ] ) ;
2394+ assert ! ( result. is_ok( ) ) ;
2395+ }
2396+
23282397 // --- extract_host_from_uri tests ---
23292398
23302399 #[ test]
0 commit comments