Skip to content

Commit 5048448

Browse files
committed
feat(kubernetes): add proxy-pod supervisor topology
Add the Kubernetes proxy-pod topology with one supervisor Deployment and Service per sandbox, NetworkPolicy confinement, proxy-pod Helm/Skaffold configuration, topology documentation, and focused supervisor identity tests. Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
1 parent 969e97b commit 5048448

35 files changed

Lines changed: 2640 additions & 337 deletions

File tree

.agents/skills/debug-openshell-cluster/SKILL.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,28 @@ all denied, inspect that file and the network sidecar logs.
299299
The shared state directory should preserve `sandbox_gid` inheritance
300300
(`02775`), and the SSH socket should be group-connectable (`0660`) so the
301301
network sidecar can bridge gateway relay requests to the process supervisor.
302-
Inspect all three when sandbox registration or egress enforcement fails:
302+
303+
If `supervisor_topology = "proxy-pod"` is rendered, each sandbox should have a
304+
separate supervisor Deployment with one supervisor pod, a headless supervisor
305+
Service, a proxy CA Secret, and two per-sandbox NetworkPolicies. The agent pod
306+
should have `openshell.ai/sandbox-role=agent`; the supervisor pod should have
307+
`openshell.ai/sandbox-role=supervisor`; both should share the same
308+
`openshell.ai/sandbox-id`. The supervisor Deployment must have a controlling
309+
`Sandbox` ownerReference. The Deployment pod template must carry the
310+
`openshell.io/sandbox-id` annotation so the TokenReview bootstrap path can mint
311+
a sandbox JWT. For supervisor pods, the gateway validates the
312+
`Pod -> ReplicaSet -> Deployment -> Sandbox` owner chain, so missing
313+
`apps/replicasets get` RBAC can also break bootstrap. Helm renders the
314+
Deployment, ReplicaSet, Service, Secret, and NetworkPolicy RBAC only when
315+
`supervisor.topology=proxy-pod`; if those resources fail with forbidden errors,
316+
confirm both the rendered `gateway.toml` and Helm values use proxy-pod topology.
317+
If the agent cannot reach the gateway, check DNS to the headless Service, the
318+
agent egress NetworkPolicy DNS exception for kube-dns/CoreDNS, and the
319+
supervisor ingress NetworkPolicy allowing only that agent pod on ports `3128`
320+
and `18080`.
321+
322+
Inspect the relevant containers when sandbox registration or egress enforcement
323+
fails:
303324

304325
```bash
305326
kubectl -n openshell get configmap openshell-config -o jsonpath='{.data.gateway\.toml}' | grep supervisor_topology

.agents/skills/helm-dev-environment/SKILL.md

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,25 @@ mise run helm:skaffold:run:sidecar
7070
mise run helm:skaffold:run:sidecar-mtls
7171
```
7272

73-
Both commands build the `gateway` and `supervisor` images and deploy the OpenShell Helm
73+
**Supervisor proxy-pod topology** (build once and leave running):
74+
```bash
75+
mise run helm:skaffold:run:proxy-pod
76+
```
77+
78+
All Skaffold commands build the `gateway` and `supervisor` images and deploy the OpenShell Helm
7479
chart. The sidecar profile renders an `openshell-network-init` init container for
7580
nftables setup and a non-root `openshell-supervisor-network` runtime sidecar for
7681
proxying. The sidecar-mTLS profile reuses `ci/values-sidecar.yaml` and restores
77-
`server.disableTls=false` inline for Skaffold. The `pkiInitJob` hook (a pre-install
78-
Job that runs `openshell-gateway generate-certs`) generates mTLS secrets on first
79-
install. Envoy Gateway opt-in; see the Optional Add-ons section below.
82+
`server.disableTls=false` inline for Skaffold. The proxy-pod profile renders
83+
network supervision in a separate supervisor Deployment with one pod and relies
84+
on Kubernetes NetworkPolicy enforcement so the agent pod can reach only its
85+
paired supervisor plus DNS. The
86+
default local k3s/k3d cluster keeps k3s's embedded NetworkPolicy controller
87+
enabled; if you replace the CNI, install a policy-enforcing CNI before using
88+
proxy-pod. The
89+
`pkiInitJob` hook (a pre-install Job that runs `openshell-gateway
90+
generate-certs`) generates mTLS secrets on first install. Envoy Gateway opt-in;
91+
see the Optional Add-ons section below.
8092

8193
The gateway Service uses ClusterIP. Access is via Envoy Gateway (port `8080`) or `kubectl port-forward`.
8294

@@ -85,6 +97,31 @@ The gateway Service uses ClusterIP. Access is via Envoy Gateway (port `8080`) or
8597
create the Secret named `openshell-ha-pg` with a `uri` key, then run
8698
`mise run helm:skaffold:run` or `mise run helm:skaffold:dev`.
8799

100+
### Kubernetes e2e profiles
101+
102+
Run the default Kubernetes e2e environment:
103+
104+
```bash
105+
mise run e2e:kubernetes
106+
```
107+
108+
Run the sidecar topology e2e environment:
109+
110+
```bash
111+
mise run e2e:kubernetes:sidecar
112+
```
113+
114+
Run the proxy-pod topology e2e environment:
115+
116+
```bash
117+
mise run e2e:kubernetes:proxy-pod
118+
```
119+
120+
The proxy-pod e2e task applies `ci/values-proxy-pod.yaml` through
121+
`OPENSHELL_E2E_KUBE_EXTRA_VALUES`. Use an existing cluster with NetworkPolicy
122+
enforcement, or let the wrapper create the default local k3d/k3s cluster with
123+
k3s's embedded NetworkPolicy controller enabled.
124+
88125
### TLS behaviour
89126

90127
`ci/values-skaffold.yaml` sets `server.disableTls: true`, so Skaffold-based deploys run
@@ -147,6 +184,12 @@ For a sidecar-profile deployment:
147184
mise run helm:skaffold:delete:sidecar
148185
```
149186

187+
For a proxy-pod-profile deployment:
188+
189+
```bash
190+
mise run helm:skaffold:delete:proxy-pod
191+
```
192+
150193
### Delete the cluster entirely
151194

152195
```bash
@@ -272,6 +315,7 @@ for dependencies still declared in `Chart.yaml`.
272315
| `deploy/helm/openshell/ci/values-high-availability.yaml` | HA test overlay (`replicaCount: 2` with external PostgreSQL Secret) |
273316
| `deploy/helm/openshell/ci/values-keycloak.yaml` | Keycloak OIDC overlay |
274317
| `deploy/helm/openshell/ci/values-sidecar.yaml` | Supervisor sidecar topology overlay for Kubernetes e2e/dev |
318+
| `deploy/helm/openshell/ci/values-proxy-pod.yaml` | Supervisor proxy-pod topology overlay for Kubernetes e2e/dev; requires NetworkPolicy enforcement |
275319
| `deploy/helm/openshell/ci/values-spire.yaml` | SPIFFE/SPIRE provider token grant overlay |
276320
| `deploy/helm/openshell/ci/values-spire-stack.yaml` | SPIRE hardened chart values for local dev |
277321
| `deploy/helm/openshell/ci/values-tls-disabled.yaml` | Lint-only: TLS + auth disabled (reverse-proxy edge termination) |

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ http-body-util = "0.1"
3737
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"] }
3838
rustls = { version = "0.23", default-features = false, features = ["std", "logging", "tls12", "ring"] }
3939
rustls-pemfile = "2"
40-
rcgen = { version = "0.13", features = ["crypto", "pem"] }
40+
rcgen = { version = "0.13", features = ["crypto", "pem", "x509-parser"] }
4141
webpki-roots = "1"
4242

4343
# CLI

architecture/gateway.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ Podman, and VM drivers deliver the initial token through supervisor-only
6464
runtime material; Kubernetes supervisors exchange a projected ServiceAccount
6565
token through `IssueSandboxToken`. The gateway validates that projected token
6666
with Kubernetes `TokenReview`, requires the configured sandbox service account,
67-
checks the returned pod binding against the live pod UID, and verifies the pod's
68-
controlling `Sandbox` ownerReference against the live Sandbox CR UID and
69-
sandbox-id label before minting the gateway JWT. The bootstrap path accepts
67+
checks the returned pod binding against the live pod UID, and verifies the
68+
pod's ownership against the live Sandbox CR UID and sandbox-id label before
69+
minting the gateway JWT. Agent pods must be directly controlled by the
70+
`Sandbox` CR. Proxy-pod supervisor pods may be controlled through the Kubernetes
71+
`Pod -> ReplicaSet -> Deployment -> Sandbox` chain. The bootstrap path accepts
7072
both `agents.x-k8s.io/v1beta1` ownerReferences from newer Agent Sandbox
7173
controllers and `agents.x-k8s.io/v1alpha1` ownerReferences from existing
7274
deployments. Supervisors renew gateway JWTs in memory before expiry only while

crates/openshell-core/src/sandbox_env.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,17 @@ pub const NETWORK_BINARY_IDENTITY: &str = "OPENSHELL_NETWORK_BINARY_IDENTITY";
4646
/// File written by the network supervisor when sidecar networking is ready.
4747
pub const SUPERVISOR_READY_FILE: &str = "OPENSHELL_SUPERVISOR_READY_FILE";
4848

49+
/// TCP address the process supervisor waits for before starting when the
50+
/// network supervisor runs outside the agent process.
51+
pub const SUPERVISOR_READY_ADDR: &str = "OPENSHELL_SUPERVISOR_READY_ADDR";
52+
4953
/// File written by the process supervisor with the workload entrypoint PID and
5054
/// read by the network sidecar for process/binary-bound network policy checks.
5155
pub const ENTRYPOINT_PID_FILE: &str = "OPENSHELL_ENTRYPOINT_PID_FILE";
5256

57+
/// Address where an external network supervisor forwards gateway gRPC traffic.
58+
pub const GATEWAY_FORWARD_ADDR: &str = "OPENSHELL_GATEWAY_FORWARD_ADDR";
59+
5360
/// Local protobuf policy snapshot written by the network sidecar and read by
5461
/// the process-only supervisor in Kubernetes sidecar topology.
5562
pub const SIDECAR_POLICY_SNAPSHOT_FILE: &str = "OPENSHELL_SIDECAR_POLICY_SNAPSHOT_FILE";
@@ -61,10 +68,26 @@ pub const SIDECAR_PROVIDER_ENV_SNAPSHOT_FILE: &str = "OPENSHELL_SIDECAR_PROVIDER
6168
/// Optional TLS server name override used when connecting to the gateway.
6269
pub const GATEWAY_TLS_SERVER_NAME: &str = "OPENSHELL_GATEWAY_TLS_SERVER_NAME";
6370

71+
/// Explicit URL injected into sandbox child processes for proxy-mode egress.
72+
///
73+
/// Kubernetes proxy-pod topology uses a headless Service DNS name, which
74+
/// cannot be represented by the policy's `SocketAddr` proxy field.
75+
pub const PROXY_URL: &str = "OPENSHELL_PROXY_URL";
76+
77+
/// Explicit listener address for the network supervisor's HTTP CONNECT proxy.
78+
pub const PROXY_BIND_ADDR: &str = "OPENSHELL_PROXY_BIND_ADDR";
79+
6480
/// Directory where the network supervisor writes the proxy CA files consumed
6581
/// by workload child processes.
6682
pub const PROXY_TLS_DIR: &str = "OPENSHELL_PROXY_TLS_DIR";
6783

84+
/// Optional CA certificate PEM path used by the network supervisor instead of
85+
/// generating an ephemeral CA.
86+
pub const PROXY_CA_CERT_PATH: &str = "OPENSHELL_PROXY_CA_CERT_PATH";
87+
88+
/// Optional CA private key PEM path paired with [`PROXY_CA_CERT_PATH`].
89+
pub const PROXY_CA_KEY_PATH: &str = "OPENSHELL_PROXY_CA_KEY_PATH";
90+
6891
/// Path to the CA certificate for mTLS communication with the gateway.
6992
pub const TLS_CA: &str = "OPENSHELL_TLS_CA";
7093

crates/openshell-driver-kubernetes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ tracing = { workspace = true }
3434
tracing-subscriber = { workspace = true }
3535
thiserror = { workspace = true }
3636
miette = { workspace = true }
37+
rcgen = { workspace = true }
3738

3839
[dev-dependencies]
3940
temp-env = "0.3"

crates/openshell-driver-kubernetes/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ process supervisor runs in network-only mode and does not apply Landlock
6666
filesystem policy, process privilege dropping, or process/binary identity
6767
checks. Network endpoint and L7 policy remain enforced by the network sidecar.
6868

69+
The `proxy-pod` supervisor topology runs network enforcement and gateway
70+
forwarding in a separate supervisor Deployment with one pod. The agent pod runs
71+
only the process-mode supervisor and reaches the supervisor through a
72+
per-sandbox headless Service. The driver creates an owner-referenced supervisor
73+
Deployment with one replica plus Service, proxy CA Secret, and NetworkPolicy
74+
resources so agent egress is limited to its paired supervisor pod plus DNS. If
75+
the supervisor pod is deleted, the Deployment recreates it.
76+
6977
Sidecar mode uses the pod `fsGroup` to make the projected service-account token
7078
and sandbox client TLS secret group-readable so the non-root process supervisor
7179
can authenticate to the gateway. Treat the agent container as trusted with

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub const DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME: &str = "default";
1515
/// Default storage size for the workspace PVC.
1616
pub const DEFAULT_WORKSPACE_STORAGE_SIZE: &str = "2Gi";
1717

18-
/// Default UID for the long-running Kubernetes network supervisor sidecar.
18+
/// Default UID for the long-running Kubernetes network proxy.
1919
pub const DEFAULT_PROXY_UID: u32 = 1337;
2020

2121
/// How the supervisor binary is delivered into sandbox pods.
@@ -65,13 +65,17 @@ pub enum SupervisorTopology {
6565
/// Run network supervision in a privileged sidecar and process supervision
6666
/// as a low-capability wrapper in the agent container.
6767
Sidecar,
68+
/// Run network supervision in a separate supervisor pod and process
69+
/// supervision as a low-capability wrapper in the agent pod.
70+
ProxyPod,
6871
}
6972

7073
impl std::fmt::Display for SupervisorTopology {
7174
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7275
match self {
7376
Self::Combined => f.write_str("combined"),
7477
Self::Sidecar => f.write_str("sidecar"),
78+
Self::ProxyPod => f.write_str("proxy-pod"),
7579
}
7680
}
7781
}
@@ -83,6 +87,7 @@ impl FromStr for SupervisorTopology {
8387
match s {
8488
"combined" => Ok(Self::Combined),
8589
"sidecar" => Ok(Self::Sidecar),
90+
"proxy-pod" => Ok(Self::ProxyPod),
8691
other => Err(format!("unknown supervisor topology '{other}'")),
8792
}
8893
}
@@ -516,6 +521,16 @@ mod tests {
516521
assert_eq!(cfg.supervisor_topology, SupervisorTopology::Combined);
517522
}
518523

524+
#[test]
525+
fn serde_override_supervisor_topology_proxy_pod() {
526+
let json = serde_json::json!({
527+
"supervisor_topology": "proxy-pod"
528+
});
529+
let cfg: KubernetesComputeConfig = serde_json::from_value(json).unwrap();
530+
assert_eq!(cfg.supervisor_topology, SupervisorTopology::ProxyPod);
531+
assert_eq!(cfg.supervisor_topology.to_string(), "proxy-pod");
532+
}
533+
519534
#[test]
520535
fn serde_override_proxy_uid() {
521536
let json = serde_json::json!({

0 commit comments

Comments
 (0)