Skip to content

Commit 9cb72ba

Browse files
feloyjohntmyers
andauthored
feat(docker): support corporate proxy CA bundles (#3549)
* feat(docker): support corporate proxy CA bundles Closes #3545 Validate and stage operator-owned proxy CA bundles for Docker supervisors, add corporate proxy E2E coverage, and document the trust contract. Signed-off-by: Philippe Martin <phmartin@redhat.com> * fix(docker): validate proxy config on startup Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(docker): use the E2E workload image for proxy tests Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(docker): generate strict corporate proxy certificates Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(docker): surface intercepted TLS fixture errors Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(docker): drain buffered TLS proxy data Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(docker): relay intercepted HTTP deterministically Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --------- Signed-off-by: Philippe Martin <phmartin@redhat.com> Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> Co-authored-by: John Myers <9696606+johntmyers@users.noreply.github.com>
1 parent 2fe5a0e commit 9cb72ba

10 files changed

Lines changed: 1533 additions & 15 deletions

File tree

‎architecture/sandbox.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,13 @@ cross the authenticated vsock channel. A gateway-host proxy is addressed as
585585

586586
The Docker driver runs `openshell-supervisor` in a separate companion container.
587587
Its private named volume contains supervisor bootstrap and channel material.
588-
The workload container receives only `openshell-sandbox`, public interception
589-
CA material, and the other sandbox half of the authenticated channel.
588+
The driver bounded-reads operator proxy credentials and CA bundles into that
589+
supervisor-only volume and passes fixed container paths to the supervisor; it
590+
never exposes gateway-host paths to sandbox-controlled configuration. The
591+
corporate CA extends supervisor upstream trust and is folded into the public
592+
combined trust bundle generated for workload processes. The workload container
593+
receives only `openshell-sandbox`, public interception and combined CA
594+
material, and the other sandbox half of the authenticated channel.
590595

591596
For Kubernetes, the operator configures a Secret name and key rather than a
592597
gateway-host file path. Kubernetes projects that Secret only into the separate

‎crates/openshell-driver-docker/README.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,32 @@ through the Docker archive API. No workload launch depends on a host bind
5050
mount or a tool supplied by the workload image, so the same path works with
5151
local, remote, and VM-backed Docker daemons.
5252

53+
## Corporate Proxy Egress
54+
55+
`[openshell.drivers.docker]` accepts the operator-owned corporate proxy fields
56+
`https_proxy`, `no_proxy`, `proxy_auth_file`,
57+
`proxy_auth_allow_insecure`, `proxy_connect_by_hostname`, and
58+
`proxy_ca_bundle`. Sandbox environment, image contents, and per-sandbox driver
59+
configuration cannot select or override them.
60+
61+
The driver validates the proxy URL and cross-field relationships at gateway
62+
startup. It bounded-reads proxy credentials and the PEM CA bundle before use.
63+
Missing, unreadable, empty, oversized, malformed, or certificate-free CA files
64+
fail closed. A CA bundle requires `https_proxy`, although the proxy URL may be
65+
`http://` when the proxy intercepts destination TLS.
66+
67+
For each supervisor launch, Docker copies the credential and CA contents into
68+
the existing supervisor-only named volume. It passes only the fixed container
69+
paths `/.openshell/supervisor/upstream-proxy-auth` and
70+
`/.openshell/supervisor/upstream-proxy-ca-bundle.pem` to the supervisor. It
71+
does not bind-mount the gateway-host files, which preserves remote-daemon
72+
support and keeps host paths out of workload container metadata.
73+
74+
The supervisor trusts the configured CA for its TLS connection to an HTTPS
75+
proxy and for destination certificates re-signed by a TLS-intercepting proxy.
76+
It also includes the corporate root in the generated combined trust bundle
77+
used by workload processes.
78+
5379
## Identity and Workspace
5480

5581
Before creating the workload, the driver pins the image ID and reads its

‎crates/openshell-driver-docker/src/lib.rs‎

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ const BOUNDARY_CERTIFICATE_MOUNT_PATH: &str = "/.openshell/channel/sandbox/serve
107107
const BOUNDARY_PRIVATE_KEY_MOUNT_PATH: &str = "/.openshell/channel/sandbox/server.key";
108108
const SUPERVISOR_STATE_MOUNT_PATH: &str = "/.openshell/supervisor";
109109
const SUPERVISOR_PROXY_AUTH_MOUNT_PATH: &str = "/.openshell/supervisor/upstream-proxy-auth";
110+
const SUPERVISOR_PROXY_CA_BUNDLE_MOUNT_PATH: &str =
111+
"/.openshell/supervisor/upstream-proxy-ca-bundle.pem";
110112
const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str =
111113
openshell_core::driver_utils::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR;
112114
const DRIVER_ADMITTED_BACKEND: &str = openshell_sandbox_backend::BACKEND_NAME;
@@ -218,6 +220,10 @@ pub struct DockerComputeConfig {
218220
#[serde(flatten)]
219221
pub upstream_proxy: UpstreamProxyConfig,
220222

223+
/// Gateway-host PEM CA bundle trusted for the corporate proxy and for
224+
/// server certificates re-signed by a TLS-intercepting proxy.
225+
pub proxy_ca_bundle: Option<PathBuf>,
226+
221227
/// Host UNIX socket projected into the supervisor for provider identity.
222228
pub provider_spiffe_workload_api_socket: Option<PathBuf>,
223229

@@ -242,6 +248,7 @@ impl DockerComputeConfig {
242248
validate_image_pull_policy(self.image_pull_policy)?;
243249
self.upstream_proxy.validate().map_err(Error::config)?;
244250
validate_docker_proxy_auth_file(&self.upstream_proxy)?;
251+
validate_docker_proxy_ca_bundle(self)?;
245252
if let Some(socket) = self.provider_spiffe_workload_api_socket.as_deref() {
246253
openshell_core::driver_utils::validate_provider_spiffe_unix_socket(socket)
247254
.map_err(Error::config)?;
@@ -276,6 +283,7 @@ impl Default for DockerComputeConfig {
276283
sandbox_pids_limit: openshell_core::config::default_sandbox_pids_limit(),
277284
enable_bind_mounts: false,
278285
upstream_proxy: UpstreamProxyConfig::default(),
286+
proxy_ca_bundle: None,
279287
provider_spiffe_workload_api_socket: None,
280288
app_armor_profile: None,
281289
}
@@ -307,6 +315,7 @@ struct DockerDriverRuntimeConfig {
307315
sandbox_pids_limit: Option<std::num::NonZeroI64>,
308316
enable_bind_mounts: bool,
309317
upstream_proxy: UpstreamProxyConfig,
318+
proxy_ca_bundle: Option<PathBuf>,
310319
provider_spiffe_workload_api_socket: Option<PathBuf>,
311320
app_armor_profile: Option<AppArmorProfile>,
312321
}
@@ -837,10 +846,7 @@ impl DockerComputeDriver {
837846
gateway_log_level: &str,
838847
docker_config: &DockerComputeConfig,
839848
) -> CoreResult<Self> {
840-
docker_config
841-
.resource_admission
842-
.validate()
843-
.map_err(Error::config)?;
849+
docker_config.validate_configuration(gateway_bind_address)?;
844850
let socket_path = docker_config
845851
.socket_path
846852
.clone()
@@ -873,15 +879,8 @@ impl DockerComputeDriver {
873879
cdi_supported,
874880
wsl_all_gpu_fallback_enabled,
875881
};
876-
validate_sandbox_pids_limit(docker_config.sandbox_pids_limit)?;
877-
validate_image_pull_policy(docker_config.image_pull_policy)?;
878882
validate_docker_app_armor_profile(docker_config.app_armor_profile.as_ref(), &info)?;
879883
let gateway_port = gateway_bind_address.port();
880-
if gateway_port == 0 {
881-
return Err(Error::config(
882-
"docker compute driver requires a fixed non-zero gateway bind port",
883-
));
884-
}
885884
let mut docker_config = docker_config.clone();
886885
if docker_config.grpc_endpoint.trim().is_empty() {
887886
docker_config.grpc_endpoint = default_docker_supervisor_grpc_endpoint(
@@ -947,6 +946,7 @@ impl DockerComputeDriver {
947946
allow_driver_config: docker_config.allow_driver_config,
948947
resource_admission: docker_config.resource_admission.clone(),
949948
upstream_proxy: docker_config.upstream_proxy.clone(),
949+
proxy_ca_bundle: docker_config.proxy_ca_bundle.clone(),
950950
provider_spiffe_workload_api_socket: docker_config
951951
.provider_spiffe_workload_api_socket
952952
.clone(),
@@ -4713,11 +4713,35 @@ async fn docker_supervisor_bundle_archive(
47134713
&contents,
47144714
)?;
47154715
}
4716+
append_docker_proxy_ca_bundle(&mut archive, config.proxy_ca_bundle.as_deref())?;
47164717
archive
47174718
.into_inner()
47184719
.map_err(|error| Status::internal(format!("finish Docker supervisor archive: {error}")))
47194720
}
47204721

4722+
fn append_docker_proxy_ca_bundle(
4723+
archive: &mut tar::Builder<Vec<u8>>,
4724+
path: Option<&Path>,
4725+
) -> Result<(), Status> {
4726+
let Some(path) = path else {
4727+
return Ok(());
4728+
};
4729+
let path = path
4730+
.to_str()
4731+
.ok_or_else(|| Status::failed_precondition("proxy_ca_bundle must be valid UTF-8"))?;
4732+
let contents =
4733+
openshell_core::driver_utils::read_upstream_proxy_ca_bundle_file(path, "proxy_ca_bundle")
4734+
.map_err(Status::failed_precondition)?;
4735+
append_docker_archive_file(
4736+
archive,
4737+
"upstream-proxy-ca-bundle.pem",
4738+
0o644,
4739+
SUPERVISOR_UID,
4740+
SUPERVISOR_GID,
4741+
contents.as_bytes(),
4742+
)
4743+
}
4744+
47214745
async fn refresh_docker_boundary_authentication(
47224746
sandbox_id: &str,
47234747
config: &DockerDriverRuntimeConfig,
@@ -5120,7 +5144,10 @@ async fn spawn_docker_control_process(
51205144
workspace_root,
51215145
format!("--health-socket-path={SUPERVISOR_HEALTH_SOCKET_PATH}"),
51225146
];
5123-
command.extend(docker_upstream_proxy_cli_args(&config.upstream_proxy));
5147+
command.extend(docker_upstream_proxy_cli_args(
5148+
&config.upstream_proxy,
5149+
config.proxy_ca_bundle.is_some(),
5150+
));
51245151
let mut supervisor_mounts = vec![
51255152
Mount {
51265153
target: Some(BOUNDARY_MOUNT_PATH.to_string()),
@@ -5871,7 +5898,30 @@ fn validate_docker_proxy_auth_file(config: &UpstreamProxyConfig) -> CoreResult<(
58715898
Ok(())
58725899
}
58735900

5874-
fn docker_upstream_proxy_cli_args(config: &UpstreamProxyConfig) -> Vec<String> {
5901+
fn validate_docker_proxy_ca_bundle(config: &DockerComputeConfig) -> CoreResult<()> {
5902+
let Some(path) = config.proxy_ca_bundle.as_ref() else {
5903+
return Ok(());
5904+
};
5905+
if path.as_os_str().is_empty() {
5906+
return Err(Error::config("proxy_ca_bundle must not be empty when set"));
5907+
}
5908+
if config.upstream_proxy.https_proxy.is_none() {
5909+
return Err(Error::config(
5910+
"proxy_ca_bundle is set but no https_proxy is configured",
5911+
));
5912+
}
5913+
let path = path
5914+
.to_str()
5915+
.ok_or_else(|| Error::config("proxy_ca_bundle must be valid UTF-8"))?;
5916+
openshell_core::driver_utils::read_upstream_proxy_ca_bundle_file(path, "proxy_ca_bundle")
5917+
.map_err(Error::config)?;
5918+
Ok(())
5919+
}
5920+
5921+
fn docker_upstream_proxy_cli_args(
5922+
config: &UpstreamProxyConfig,
5923+
proxy_ca_bundle_configured: bool,
5924+
) -> Vec<String> {
58755925
let mut args = Vec::new();
58765926
if let Some(url) = config.https_proxy.as_ref() {
58775927
args.extend(["--upstream-proxy".to_string(), url.clone()]);
@@ -5891,6 +5941,12 @@ fn docker_upstream_proxy_cli_args(config: &UpstreamProxyConfig) -> Vec<String> {
58915941
if config.proxy_connect_by_hostname == Some(true) {
58925942
args.push("--upstream-proxy-connect-by-hostname".to_string());
58935943
}
5944+
if proxy_ca_bundle_configured {
5945+
args.extend([
5946+
"--upstream-proxy-ca-bundle".to_string(),
5947+
SUPERVISOR_PROXY_CA_BUNDLE_MOUNT_PATH.to_string(),
5948+
]);
5949+
}
58945950
args
58955951
}
58965952

0 commit comments

Comments
 (0)