Skip to content

Commit 914da33

Browse files
authored
feat(kubernetes): add combined topology config surface (#2074)
* feat(kubernetes): add combined topology config surface Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * docs(kubernetes): clarify topology defaults Signed-off-by: Taylor Mutch <taylormutch@gmail.com> --------- Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
1 parent 5477e2f commit 914da33

10 files changed

Lines changed: 194 additions & 2 deletions

File tree

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ impl FromStr for SupervisorSideloadMethod {
5252
}
5353
}
5454

55+
/// How the supervisor is arranged inside Kubernetes sandbox pods.
56+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57+
#[serde(rename_all = "kebab-case")]
58+
pub enum SupervisorTopology {
59+
/// Run networking and process supervision in the agent container.
60+
#[default]
61+
Combined,
62+
}
63+
64+
impl std::fmt::Display for SupervisorTopology {
65+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66+
match self {
67+
Self::Combined => f.write_str("combined"),
68+
}
69+
}
70+
}
71+
72+
impl FromStr for SupervisorTopology {
73+
type Err = String;
74+
75+
fn from_str(s: &str) -> Result<Self, Self::Err> {
76+
match s {
77+
"combined" => Ok(Self::Combined),
78+
other => Err(format!("unknown supervisor topology '{other}'")),
79+
}
80+
}
81+
}
82+
5583
/// Kubernetes `AppArmor` profile requested for the sandbox agent container.
5684
#[derive(Debug, Clone, PartialEq, Eq)]
5785
pub enum AppArmorProfile {
@@ -176,6 +204,8 @@ pub struct KubernetesComputeConfig {
176204
pub supervisor_image_pull_policy: String,
177205
/// How the supervisor binary is delivered into sandbox pods.
178206
pub supervisor_sideload_method: SupervisorSideloadMethod,
207+
/// How the supervisor is arranged for Kubernetes sandbox pods.
208+
pub supervisor_topology: SupervisorTopology,
179209
pub grpc_endpoint: String,
180210
pub ssh_socket_path: String,
181211
pub client_tls_secret_name: String,
@@ -236,6 +266,7 @@ impl Default for KubernetesComputeConfig {
236266
supervisor_image: DEFAULT_SUPERVISOR_IMAGE.to_string(),
237267
supervisor_image_pull_policy: String::new(),
238268
supervisor_sideload_method: SupervisorSideloadMethod::default(),
269+
supervisor_topology: SupervisorTopology::default(),
239270
grpc_endpoint: String::new(),
240271
ssh_socket_path: "/run/openshell/ssh.sock".to_string(),
241272
client_tls_secret_name: String::new(),
@@ -333,6 +364,31 @@ mod tests {
333364
);
334365
}
335366

367+
#[test]
368+
fn default_supervisor_topology_is_combined() {
369+
let cfg = KubernetesComputeConfig::default();
370+
assert_eq!(cfg.supervisor_topology, SupervisorTopology::Combined);
371+
assert_eq!(cfg.supervisor_topology.to_string(), "combined");
372+
}
373+
374+
#[test]
375+
fn serde_override_supervisor_topology_combined() {
376+
let json = serde_json::json!({
377+
"supervisor_topology": "combined"
378+
});
379+
let cfg: KubernetesComputeConfig = serde_json::from_value(json).unwrap();
380+
assert_eq!(cfg.supervisor_topology, SupervisorTopology::Combined);
381+
}
382+
383+
#[test]
384+
fn serde_rejects_invalid_supervisor_topology() {
385+
let json = serde_json::json!({
386+
"supervisor_topology": "unsupported"
387+
});
388+
let err = serde_json::from_value::<KubernetesComputeConfig>(json).unwrap_err();
389+
assert!(err.to_string().contains("unknown variant"));
390+
}
391+
336392
#[test]
337393
fn serde_override_workspace_storage_size() {
338394
let json = serde_json::json!({

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub mod grpc;
77

88
pub use config::{
99
AppArmorProfile, DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME, DEFAULT_WORKSPACE_STORAGE_SIZE,
10-
KubernetesComputeConfig, SupervisorSideloadMethod,
10+
KubernetesComputeConfig, SupervisorSideloadMethod, SupervisorTopology,
1111
};
1212
pub use driver::{KubernetesComputeDriver, KubernetesDriverError};
1313
pub use grpc::ComputeDriverService;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use openshell_core::VERSION;
1111
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
1212
use openshell_driver_kubernetes::{
1313
AppArmorProfile, ComputeDriverService, DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME,
14-
KubernetesComputeConfig, KubernetesComputeDriver, SupervisorSideloadMethod,
14+
KubernetesComputeConfig, KubernetesComputeDriver, SupervisorSideloadMethod, SupervisorTopology,
1515
};
1616

1717
#[derive(Parser, Debug)]
@@ -80,6 +80,13 @@ struct Args {
8080
)]
8181
supervisor_sideload_method: SupervisorSideloadMethod,
8282

83+
#[arg(
84+
long,
85+
env = "OPENSHELL_SUPERVISOR_TOPOLOGY",
86+
default_value = "combined"
87+
)]
88+
supervisor_topology: SupervisorTopology,
89+
8390
#[arg(long, env = "OPENSHELL_ENABLE_USER_NAMESPACES")]
8491
enable_user_namespaces: bool,
8592

@@ -117,6 +124,7 @@ async fn main() -> Result<()> {
117124
.unwrap_or_else(|| openshell_core::config::DEFAULT_SUPERVISOR_IMAGE.to_string()),
118125
supervisor_image_pull_policy: args.supervisor_image_pull_policy.unwrap_or_default(),
119126
supervisor_sideload_method: args.supervisor_sideload_method,
127+
supervisor_topology: args.supervisor_topology,
120128
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
121129
ssh_socket_path: args.sandbox_ssh_socket_path,
122130
client_tls_secret_name: args.client_tls_secret_name.unwrap_or_default(),

deploy/helm/openshell/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ add `ci/values-spire.yaml` to the OpenShell release values files.
237237
| supervisor.image.repository | string | `"ghcr.io/nvidia/openshell/supervisor"` | Supervisor image repository. |
238238
| supervisor.image.tag | string | `""` | Supervisor image tag. Defaults to the chart appVersion when empty. |
239239
| supervisor.sideloadMethod | string | `""` | How the supervisor binary is delivered into sandbox pods. Empty (default) = auto-detect from cluster version: K8s >= v1.35 -> "image-volume" (ImageVolume enabled by default; GA in v1.36) K8s < v1.35 -> "init-container" (copies via init container + emptyDir) On K8s v1.33-v1.34 with the ImageVolume feature gate manually enabled, set this to "image-volume" explicitly. |
240+
| supervisor.topology | string | `"combined"` | Supervisor pod topology for Kubernetes sandboxes. "combined" runs networking and process supervision in the agent container. |
240241
| tolerations | list | `[]` | Tolerations for the gateway pod. |
241242
| workload.allowMultiReplicaStatefulSet | bool | `false` | Allow replicaCount > 1 while rendering a StatefulSet. Prefer workload.kind=deployment for external database-backed multi-replica gateways; this override exists for operators who explicitly require StatefulSet identity or storage semantics. |
242243
| workload.kind | string | `"statefulset"` | Gateway workload controller kind. Use `statefulset` for the default SQLite database, or `deployment` when server.externalDbSecret points at an external database. |

deploy/helm/openshell/templates/gateway-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ data:
113113
grpc_endpoint = {{ include "openshell.grpcEndpoint" . | quote }}
114114
service_account_name = {{ include "openshell.sandboxServiceAccountName" . | quote }}
115115
supervisor_sideload_method = {{ include "openshell.supervisorSideloadMethod" . | quote }}
116+
supervisor_topology = {{ .Values.supervisor.topology | default "combined" | quote }}
116117
sa_token_ttl_secs = {{ .Values.server.sandboxJwt.k8sSaTokenTtlSecs | default 3600 }}
117118
{{- if .Values.server.providerTokenGrants.spiffe.enabled }}
118119
provider_spiffe_workload_api_socket_path = {{ .Values.server.providerTokenGrants.spiffe.workloadApiSocketPath | quote }}

deploy/helm/openshell/tests/gateway_config_test.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ tests:
8383
path: data["gateway.toml"]
8484
pattern: '(?ms)\[openshell\.drivers\.kubernetes\].*?service_account_name\s*=\s*"openshell-sandbox"'
8585

86+
- it: renders combined supervisor topology by default under [openshell.drivers.kubernetes]
87+
template: templates/gateway-config.yaml
88+
asserts:
89+
- matchRegex:
90+
path: data["gateway.toml"]
91+
pattern: '(?ms)\[openshell\.drivers\.kubernetes\].*?supervisor_topology\s*=\s*"combined"'
92+
93+
- it: renders explicit combined supervisor topology under [openshell.drivers.kubernetes]
94+
template: templates/gateway-config.yaml
95+
set:
96+
supervisor.topology: combined
97+
asserts:
98+
- matchRegex:
99+
path: data["gateway.toml"]
100+
pattern: '(?ms)\[openshell\.drivers\.kubernetes\].*?supervisor_topology\s*=\s*"combined"'
101+
86102
- it: renders sandbox image pull secrets under [openshell.drivers.kubernetes]
87103
template: templates/gateway-config.yaml
88104
set:

deploy/helm/openshell/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ supervisor:
4444
# On K8s v1.33-v1.34 with the ImageVolume feature gate manually enabled,
4545
# set this to "image-volume" explicitly.
4646
sideloadMethod: ""
47+
# -- Supervisor pod topology for Kubernetes sandboxes.
48+
# "combined" runs networking and process supervision in the agent container.
49+
topology: "combined"
4750

4851
# -- Image pull secrets attached to gateway and helper pods.
4952
imagePullSecrets: []

docs/kubernetes/setup.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ The most commonly changed values are:
160160
| `server.enableLoopbackServiceHttp` | Enable local plaintext HTTP for loopback sandbox service URLs. Defaults to `true`. |
161161
| `pkiInitJob.serverDnsNames` / `certManager.serverDnsNames` | Additional gateway server DNS SANs. Wildcard SANs also enable sandbox service URLs under that domain. |
162162
| `supervisor.sideloadMethod` | How the supervisor binary is delivered into sandbox pods. Leave empty to auto-detect based on cluster version: clusters running Kubernetes 1.35 or later use `image-volume` (ImageVolume GA in 1.36); older clusters use `init-container`. Set explicitly to `image-volume` on Kubernetes 1.33 or 1.34 with the ImageVolume feature gate enabled, or to `init-container` to force the legacy path on any version. |
163+
| `supervisor.topology` | Sandbox pod topology. Refer to [Topology](/kubernetes/topology). |
163164

164165
Use a values file for repeatable deployments:
165166

@@ -243,6 +244,7 @@ The gateway exposes `/healthz` for process liveness and `/readyz` for dependency
243244

244245
## Next Steps
245246

247+
- To review Kubernetes sandbox topology, refer to [Topology](/kubernetes/topology).
246248
- To enable automatic certificate rotation with cert-manager, refer to [Managing Certificates](/kubernetes/managing-certificates).
247249
- To expose the gateway externally without port-forwarding, refer to [Ingress](/kubernetes/ingress).
248250
- To configure OIDC or reverse-proxy authentication, refer to [Access Control](/kubernetes/access-control).

docs/kubernetes/topology.mdx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
title: "Kubernetes Sandbox Topology"
5+
sidebar-title: "Topology"
6+
description: "Review the default combined supervisor topology for Kubernetes sandbox pods."
7+
keywords: "Generative AI, Cybersecurity, Kubernetes, Sandboxing, RuntimeClass"
8+
position: 2
9+
---
10+
11+
Kubernetes sandbox pods run the OpenShell supervisor in `combined` topology by
12+
default. Combined topology keeps network, filesystem, and process controls in
13+
the agent pod so the supervisor can enforce the complete OpenShell sandbox
14+
contract before launching the workload.
15+
16+
## Choose a Topology
17+
18+
The default `combined` topology preserves the full OpenShell enforcement model.
19+
Use it when you need OpenShell to apply all sandbox controls inside the workload
20+
pod and your cluster policy permits the required Linux capabilities.
21+
22+
| Topology | Use when | Main tradeoff |
23+
|---|---|---|
24+
| `combined` | You need OpenShell network, filesystem, and process controls in the sandbox workload. | The agent container carries the Linux capabilities the supervisor needs. |
25+
26+
Additional Kubernetes sandbox topologies are still being designed. Until they
27+
are documented as supported configuration values, `combined` is the only
28+
supported value for `supervisor.topology`.
29+
30+
## Privilege Model
31+
32+
The long-running container permissions for `combined` topology are:
33+
34+
| Topology | Pod or container | UID/GID | Privilege escalation | Capabilities | Result |
35+
|---|---|---|---|---|---|
36+
| `combined` | Agent container, which also runs the supervisor | Not forced by topology | Not explicitly disabled by the driver | Adds `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE`, and `SYSLOG`; adds `SETUID`, `SETGID`, and `DAC_READ_SEARCH` when user namespaces are enabled | Full supervisor controls run in the agent container. |
37+
38+
Short-lived setup containers still have the permissions needed to prepare the
39+
pod:
40+
41+
| Topology | Setup container | UID/GID | Privilege escalation | Capabilities | Purpose |
42+
|---|---|---|---|---|---|
43+
| `combined` | Supervisor install init container | `0` | Not set | Not set | Copies the supervisor binary into the agent container volume. |
44+
45+
## Combined Topology
46+
47+
Combined topology is the original Kubernetes mode and remains the default. The
48+
agent container starts the OpenShell supervisor, and the supervisor launches the
49+
workload after applying sandbox setup.
50+
51+
Combined topology keeps these controls in one supervisor path:
52+
53+
- Network endpoint and L7 policy enforcement.
54+
- Filesystem policy enforcement.
55+
- Process and binary identity checks.
56+
- Privilege drop into the sandbox user.
57+
- Gateway relay, SSH sessions, exec, and file sync.
58+
59+
Because the supervisor performs network namespace setup and process/filesystem
60+
controls from the agent container, Kubernetes grants that container elevated
61+
Linux capabilities. Use this mode when you need the complete OpenShell sandbox
62+
contract and your cluster policy permits those capabilities.
63+
64+
## RuntimeClass Isolation
65+
66+
RuntimeClass isolation can add a stronger container boundary for the sandbox
67+
workload when the cluster supports it. Runtime classes do not replace the
68+
combined topology's supervisor controls; they add another isolation boundary
69+
around the same supervised workload.
70+
71+
You can set a default runtime class in the Kubernetes driver configuration or
72+
override it per sandbox with driver config:
73+
74+
```shell
75+
openshell sandbox create \
76+
--driver-config-json '{"kubernetes":{"pod":{"runtime_class_name":"kata-containers"}}}' \
77+
-- claude
78+
```
79+
80+
## Configure Combined Mode
81+
82+
For direct gateway TOML configuration, leave `supervisor_topology` unset, or
83+
set it to `combined`, to use the default single-container supervisor path:
84+
85+
```toml
86+
[openshell.drivers.kubernetes]
87+
supervisor_topology = "combined"
88+
```
89+
90+
When the Helm chart renders `gateway.toml`, leave `supervisor.topology` unset,
91+
or set it to `combined`, to produce the same driver configuration:
92+
93+
```yaml
94+
supervisor:
95+
topology: combined
96+
```
97+
98+
## Next Steps
99+
100+
- To install OpenShell on Kubernetes, refer to [Setup](/kubernetes/setup).
101+
- To configure gateway authentication, refer to [Access Control](/kubernetes/access-control).
102+
- To review the driver fields, refer to [Gateway Configuration File](/reference/gateway-config).

docs/reference/gateway-config.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ supervisor_image_pull_policy = "IfNotPresent"
177177
# Use the image volume on Kubernetes >= 1.35 (GA in 1.36); switch to "init-container"
178178
# on older clusters or where the ImageVolume feature gate is off.
179179
supervisor_sideload_method = "image-volume"
180+
# "combined" runs networking and process supervision in the sandbox agent
181+
# container and preserves the existing Kubernetes sandbox behavior.
182+
supervisor_topology = "combined"
180183
grpc_endpoint = "https://openshell-gateway.agents.svc:8080"
181184
ssh_socket_path = "/run/openshell/ssh.sock"
182185
client_tls_secret_name = "openshell-client-tls"

0 commit comments

Comments
 (0)