Skip to content

Commit 4ff0d12

Browse files
committed
fix(sandbox): add CIDR breadth warning and control-plane port blocklist (SEC-005)
Defense-in-depth for the allowed_ips feature: - Log a warning when a CIDR entry has a prefix length < /16, as overly broad ranges may unintentionally expose control-plane services - Block K8s API (6443), etcd (2379/2380), and kubelet (10250/10255) ports unconditionally in resolve_and_check_allowed_ips, even when the resolved IP matches an allowed_ips entry
1 parent ef90799 commit 4ff0d12

File tree

1 file changed

+71
-2
lines changed

1 file changed

+71
-2
lines changed

crates/openshell-sandbox/src/proxy.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
12831306
fn 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

Comments
 (0)