Skip to content

Commit bf55d03

Browse files
author
lr90
committed
feat(kubernetes): support corporate upstream proxy
Signed-off-by: lr90 <qiuweimin@matrixorigin.cn>
1 parent d2c44b0 commit bf55d03

18 files changed

Lines changed: 1072 additions & 24 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

architecture/sandbox.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,14 @@ file and builds the `Proxy-Authorization: Basic` header; a credential that is
176176
empty, contains control characters, or is not in `user:pass` form is fatal on
177177
both sides.
178178

179+
For Kubernetes sandboxes, the operator configures a Secret name and key rather
180+
than a gateway-host file path. Kubernetes projects that Secret only into the
181+
container that runs network supervision. Proxy credential Secrets require the
182+
sidecar topology, which gives them a separate container boundary from the
183+
workload. Combined topology is rejected because Kubernetes `fsGroup` volume
184+
permission handling can make a shared credential mount readable by the sandbox
185+
group.
186+
179187
The Basic header travels over the plain-TCP connection to the `http://` proxy,
180188
so it is readable on the network path between sandbox host and proxy.
181189
Configuring `proxy_auth_file` therefore requires the explicit opt-in

crates/openshell-driver-kubernetes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ miette = { workspace = true }
3737

3838
[dev-dependencies]
3939
temp-env = "0.3"
40+
toml = { workspace = true }
4041

4142
[lints]
4243
workspace = true

crates/openshell-driver-kubernetes/src/config.rs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,25 @@ pub struct KubernetesComputeConfig {
253253
pub topology: SupervisorTopology,
254254
/// Sidecar-only settings used when `topology = "sidecar"`.
255255
pub sidecar: KubernetesSidecarConfig,
256+
/// Corporate HTTP forward proxy used by the network supervisor for
257+
/// policy-approved TLS CONNECT egress.
258+
pub https_proxy: Option<String>,
259+
/// Comma-separated destinations that bypass the corporate proxy while
260+
/// continuing through `OpenShell` policy evaluation.
261+
pub no_proxy: Option<String>,
262+
/// Name of the Kubernetes Secret holding the `user:pass` proxy credential.
263+
/// The Secret is mounted only in the network-supervising container. The
264+
/// driver validates this reference at startup; the supervisor validates
265+
/// the Secret content when kubelet mounts it before accepting egress.
266+
pub proxy_auth_secret_name: Option<String>,
267+
/// Key in `proxy_auth_secret_name` containing the `user:pass` credential.
268+
pub proxy_auth_secret_key: Option<String>,
269+
/// Explicit acknowledgement that Basic authentication is cleartext over
270+
/// the connection to an `http://` forward proxy.
271+
pub proxy_auth_allow_insecure: Option<bool>,
272+
/// Send hostnames rather than validated IPs in CONNECT requests. This is a
273+
/// last-resort compatibility mode for hostname-filtering proxy ACLs.
274+
pub proxy_connect_by_hostname: Option<bool>,
256275
pub grpc_endpoint: String,
257276
pub ssh_socket_path: String,
258277
pub client_tls_secret_name: String,
@@ -346,6 +365,12 @@ impl Default for KubernetesComputeConfig {
346365
supervisor_sideload_method: SupervisorSideloadMethod::default(),
347366
topology: SupervisorTopology::default(),
348367
sidecar: KubernetesSidecarConfig::default(),
368+
https_proxy: None,
369+
no_proxy: None,
370+
proxy_auth_secret_name: None,
371+
proxy_auth_secret_key: None,
372+
proxy_auth_allow_insecure: None,
373+
proxy_connect_by_hostname: None,
349374
grpc_endpoint: String::new(),
350375
ssh_socket_path: openshell_core::container_paths::SSH_SOCKET_PATH.to_string(),
351376
client_tls_secret_name: String::new(),
@@ -395,6 +420,88 @@ impl KubernetesComputeConfig {
395420
self.sidecar.validate_proxy_uid()
396421
}
397422

423+
/// Validate the operator-owned corporate upstream proxy configuration.
424+
pub fn validate_upstream_proxy_config(&self) -> Result<(), String> {
425+
use openshell_core::driver_utils::{UpstreamProxyUrlError, parse_upstream_proxy_url};
426+
427+
if let Some(url) = &self.https_proxy {
428+
parse_upstream_proxy_url(url).map_err(|err| match err {
429+
UpstreamProxyUrlError::Empty => "https_proxy must not be empty when set".to_string(),
430+
UpstreamProxyUrlError::InlineCredentials => "https_proxy must not embed credentials in the URL; supply them through proxy_auth_secret_name and proxy_auth_secret_key".to_string(),
431+
err => format!("https_proxy {err}"),
432+
})?;
433+
}
434+
435+
if let Some(list) = self.no_proxy.as_deref() {
436+
if list.trim().is_empty() {
437+
return Err("no_proxy must not be empty when set; omit it instead".to_string());
438+
}
439+
if self.https_proxy.is_none() {
440+
return Err("no_proxy is set but no https_proxy is configured".to_string());
441+
}
442+
}
443+
444+
let secret_name = self.proxy_auth_secret_name.as_deref();
445+
let secret_key = self.proxy_auth_secret_key.as_deref();
446+
match (secret_name, secret_key) {
447+
(None, None) => {
448+
if self.proxy_auth_allow_insecure == Some(true) {
449+
return Err("proxy_auth_allow_insecure is set but no proxy credential Secret is configured".to_string());
450+
}
451+
}
452+
(Some(name), Some(key)) => {
453+
if name.trim().is_empty() || key.trim().is_empty() {
454+
return Err(
455+
"proxy credential Secret name and key must not be empty".to_string()
456+
);
457+
}
458+
if !is_dns1123_subdomain(name) {
459+
return Err(
460+
"proxy_auth_secret_name must be a valid Kubernetes DNS-1123 subdomain"
461+
.to_string(),
462+
);
463+
}
464+
if !key
465+
.bytes()
466+
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
467+
{
468+
return Err(
469+
"proxy_auth_secret_key must contain only letters, digits, '.', '-', or '_'"
470+
.to_string(),
471+
);
472+
}
473+
if self.https_proxy.is_none() {
474+
return Err(
475+
"proxy credential Secret is set but no https_proxy is configured"
476+
.to_string(),
477+
);
478+
}
479+
if self.proxy_auth_allow_insecure != Some(true) {
480+
return Err("proxy credentials use cleartext Basic auth over the connection to the http:// proxy; set proxy_auth_allow_insecure = true to accept that exposure, or remove the credential Secret".to_string());
481+
}
482+
if self.topology == SupervisorTopology::Combined {
483+
return Err(
484+
"proxy credential Secrets require topology = \"sidecar\"; combined topology shares the credential mount with the workload and fsGroup can make it readable by the sandbox user"
485+
.to_string(),
486+
);
487+
}
488+
}
489+
_ => {
490+
return Err(
491+
"proxy_auth_secret_name and proxy_auth_secret_key must be set together"
492+
.to_string(),
493+
);
494+
}
495+
}
496+
497+
if self.proxy_connect_by_hostname.is_some() && self.https_proxy.is_none() {
498+
return Err(
499+
"proxy_connect_by_hostname is set but no https_proxy is configured".to_string(),
500+
);
501+
}
502+
Ok(())
503+
}
504+
398505
/// Resolve the sandbox UID/GID pair.
399506
///
400507
/// Resolution order:
@@ -475,6 +582,20 @@ impl KubernetesComputeConfig {
475582
}
476583
}
477584

585+
fn is_dns1123_subdomain(value: &str) -> bool {
586+
!value.is_empty()
587+
&& value.len() <= 253
588+
&& value.split('.').all(|label| {
589+
!label.is_empty()
590+
&& label.len() <= 63
591+
&& !label.starts_with('-')
592+
&& !label.ends_with('-')
593+
&& label
594+
.bytes()
595+
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
596+
})
597+
}
598+
478599
fn validate_provider_spiffe_workload_api_socket_path_value(
479600
socket_path: &str,
480601
) -> Result<(), String> {
@@ -966,4 +1087,126 @@ mod tests {
9661087
let uid = cfg.resolve_sandbox_uid(None);
9671088
assert_eq!(cfg.resolve_sandbox_gid(uid, None), uid);
9681089
}
1090+
1091+
#[test]
1092+
fn upstream_proxy_config_accepts_http_proxy_without_credentials() {
1093+
let cfg = KubernetesComputeConfig {
1094+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1095+
no_proxy: Some(".svc.cluster.local,10.96.0.0/12".to_string()),
1096+
..KubernetesComputeConfig::default()
1097+
};
1098+
assert!(cfg.validate_upstream_proxy_config().is_ok());
1099+
}
1100+
1101+
#[test]
1102+
fn upstream_proxy_config_accepts_secret_credentials_with_acknowledgement() {
1103+
let cfg = KubernetesComputeConfig {
1104+
topology: SupervisorTopology::Sidecar,
1105+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1106+
proxy_auth_secret_name: Some("corporate-proxy-auth".to_string()),
1107+
proxy_auth_secret_key: Some("credentials".to_string()),
1108+
proxy_auth_allow_insecure: Some(true),
1109+
..KubernetesComputeConfig::default()
1110+
};
1111+
assert!(cfg.validate_upstream_proxy_config().is_ok());
1112+
}
1113+
1114+
#[test]
1115+
fn toml_deserializes_sidecar_upstream_proxy_settings() {
1116+
let cfg: KubernetesComputeConfig = toml::from_str(
1117+
r#"
1118+
topology = "sidecar"
1119+
https_proxy = "http://proxy.corp.example:8080"
1120+
no_proxy = ".svc.cluster.local,10.96.0.0/12"
1121+
proxy_auth_secret_name = "corporate-proxy-auth"
1122+
proxy_auth_secret_key = "credentials"
1123+
proxy_auth_allow_insecure = true
1124+
proxy_connect_by_hostname = true
1125+
"#,
1126+
)
1127+
.unwrap();
1128+
assert!(cfg.validate_upstream_proxy_config().is_ok());
1129+
assert_eq!(
1130+
cfg.https_proxy.as_deref(),
1131+
Some("http://proxy.corp.example:8080")
1132+
);
1133+
assert_eq!(
1134+
cfg.proxy_auth_secret_name.as_deref(),
1135+
Some("corporate-proxy-auth")
1136+
);
1137+
}
1138+
1139+
#[test]
1140+
fn upstream_proxy_config_rejects_incoherent_auxiliary_settings() {
1141+
for cfg in [
1142+
KubernetesComputeConfig {
1143+
no_proxy: Some(".svc".to_string()),
1144+
..KubernetesComputeConfig::default()
1145+
},
1146+
KubernetesComputeConfig {
1147+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1148+
proxy_auth_secret_name: Some("corporate-proxy-auth".to_string()),
1149+
..KubernetesComputeConfig::default()
1150+
},
1151+
KubernetesComputeConfig {
1152+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1153+
proxy_auth_secret_name: Some("corporate-proxy-auth".to_string()),
1154+
proxy_auth_secret_key: Some("credentials".to_string()),
1155+
..KubernetesComputeConfig::default()
1156+
},
1157+
KubernetesComputeConfig {
1158+
proxy_connect_by_hostname: Some(true),
1159+
..KubernetesComputeConfig::default()
1160+
},
1161+
] {
1162+
assert!(cfg.validate_upstream_proxy_config().is_err());
1163+
}
1164+
}
1165+
1166+
#[test]
1167+
fn upstream_proxy_config_rejects_unsupported_proxy_scheme() {
1168+
let cfg = KubernetesComputeConfig {
1169+
https_proxy: Some("https://proxy.corp.example:8443".to_string()),
1170+
..KubernetesComputeConfig::default()
1171+
};
1172+
let err = cfg.validate_upstream_proxy_config().unwrap_err();
1173+
assert!(err.contains("https_proxy"), "{err}");
1174+
}
1175+
1176+
#[test]
1177+
fn upstream_proxy_config_rejects_invalid_secret_name() {
1178+
let cfg = KubernetesComputeConfig {
1179+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1180+
proxy_auth_secret_name: Some("Not_A_Secret".to_string()),
1181+
proxy_auth_secret_key: Some("credentials".to_string()),
1182+
proxy_auth_allow_insecure: Some(true),
1183+
..KubernetesComputeConfig::default()
1184+
};
1185+
let err = cfg.validate_upstream_proxy_config().unwrap_err();
1186+
assert!(err.contains("proxy_auth_secret_name"), "{err}");
1187+
}
1188+
1189+
#[test]
1190+
fn upstream_proxy_config_rejects_credentials_in_combined_topology() {
1191+
let cfg = KubernetesComputeConfig {
1192+
topology: SupervisorTopology::Combined,
1193+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1194+
proxy_auth_secret_name: Some("corporate-proxy-auth".to_string()),
1195+
proxy_auth_secret_key: Some("credentials".to_string()),
1196+
proxy_auth_allow_insecure: Some(true),
1197+
..KubernetesComputeConfig::default()
1198+
};
1199+
let err = cfg.validate_upstream_proxy_config().unwrap_err();
1200+
assert!(err.contains("topology = \"sidecar\""), "{err}");
1201+
}
1202+
1203+
#[test]
1204+
fn upstream_proxy_config_allows_explicit_false_acknowledgement_without_credentials() {
1205+
let cfg = KubernetesComputeConfig {
1206+
https_proxy: Some("http://proxy.corp.example:8080".to_string()),
1207+
proxy_auth_allow_insecure: Some(false),
1208+
..KubernetesComputeConfig::default()
1209+
};
1210+
assert!(cfg.validate_upstream_proxy_config().is_ok());
1211+
}
9691212
}

0 commit comments

Comments
 (0)