Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
72d06f0
feat(kubernetes): add proxy-pod supervisor topology
TaylorMutch Jul 2, 2026
a15f26c
refactor(kubernetes): run proxy-pod workloads directly
TaylorMutch Jul 10, 2026
129230d
fix(kubernetes): harden proxy-pod workloads
TaylorMutch Jul 13, 2026
b4556c8
docs(rfc): add proxy-pod supervisor topology draft RFC
russellb Aug 20, 2026
38cd20c
feat(kubernetes): configurable proxy-pod cluster DNS peers
russellb Aug 20, 2026
62da677
fix(kubernetes): stop the proxy-pod supervisor when the sandbox stops
russellb Aug 20, 2026
e765704
feat(helm): gated OpenShift nonroot-v2 SCC grant for sandbox pods
russellb Aug 20, 2026
cb7d2fc
fix(kubernetes): make the proxy-pod DNS peer port configurable
russellb Aug 20, 2026
ed41a58
docs(rfc): record proxy-pod OpenShift validation results
russellb Aug 20, 2026
386e796
feat(compute): let drivers declare that a sandbox has no supervisor s…
russellb Aug 21, 2026
7d6eccf
feat(kubernetes): workload entrypoint override for proxy-pod topology
russellb Aug 21, 2026
9738557
fix(kubernetes): derive proxy-pod resource names from the sandbox name
russellb Aug 21, 2026
cfbbe0e
docs(rfc): correct the readiness and workload-command analysis
russellb Aug 21, 2026
36ad3a3
docs(rfc): explain the relay boundary and observability tradeoffs
russellb Aug 21, 2026
8825e0f
fix(cli): explain sessionless topologies instead of surfacing a raw e…
russellb Aug 21, 2026
5df8174
fix(cli): detect a sessionless topology before opening a session
russellb Aug 21, 2026
d3218c8
docs(rfc): note Kata kernel isolation and clarify workload log location
russellb Aug 21, 2026
a2ea65e
fix(kubernetes): strip MAIN_PROCESS_SPEC from proxy-pod workloads
russellb Aug 21, 2026
b73f0ba
fix(cli): warn before implicit detach when a sessionless topology dro…
russellb Aug 21, 2026
0695914
fix(kubernetes): correct proxy-pod companion lifecycle, isolation, an…
russellb Aug 22, 2026
a515294
fix(server): release the sessionless marker when a sandbox is removed
russellb Aug 22, 2026
2d9a221
fix(cli): detect a sessionless topology before session-bound create s…
russellb Aug 22, 2026
3fa96e4
test(e2e): add a capability-scoped proxy-pod suite
russellb Aug 22, 2026
2086e28
docs(rfc): reflect gateway-forward removal in proxy-pod design
russellb Aug 22, 2026
6cf496d
fix(kubernetes): reference proxy-pod companions by CR name in the pod…
russellb Aug 22, 2026
492ff77
fix(kubernetes): harden proxy-pod companion lifecycle and placement
russellb Aug 24, 2026
b60945b
fix(kubernetes): tighten proxy-pod RBAC, GC, and DNS from review
russellb Aug 24, 2026
9956573
fix(kubernetes): reconcile proxy-pod companions idempotently
russellb Aug 24, 2026
5de78f2
fix(supervisor-network): ignore image-baked proxy launch env in combi…
russellb Aug 24, 2026
4adeda8
fix(kubernetes): key proxy-pod companions on UUID, verify 409s, forwa…
russellb Aug 24, 2026
be8b332
fix(kubernetes): least-privilege proxy-pod RBAC and gateway-scoped re…
russellb Aug 24, 2026
df638e6
fix(kubernetes): order proxy-pod egress fence teardown after workload…
russellb Aug 24, 2026
8f009c1
fix(kubernetes): confirm workload exit before fence teardown and hard…
russellb Aug 24, 2026
aa403b1
fix(server): preserve relay status code for sessionless topologies
russellb Aug 24, 2026
5fec6e3
test(helm): align proxy-pod RBAC tests and docs with least-privilege …
russellb Aug 24, 2026
22b7106
fix(kubernetes): harden fence teardown, validation, topology migratio…
russellb Aug 25, 2026
db0b134
fix(server): reject relay RPCs for sessionless sandboxes from durable…
russellb Aug 25, 2026
1b5fd03
docs(proxy-pod): OpenShift example peers/SCC and supervisor log location
russellb Aug 25, 2026
fb75df9
feat(kubernetes): reconcile proxy-pod companions periodically and wat…
russellb Aug 25, 2026
cc20354
ci(kubernetes): run the proxy-pod e2e suite in branch CI
russellb Aug 25, 2026
b2e0772
fix(server): use pub for sandbox_status_is_sessionless
russellb Aug 25, 2026
dc82885
docs(helm): regenerate chart README for supervisor.proxyPod.retainCom…
russellb Aug 25, 2026
3785f28
fix(kubernetes): harden proxy-pod fence reaping, readiness, and watch…
russellb Aug 25, 2026
12d42a2
fix(server): reject sessionless interactive exec and require NotAppli…
russellb Aug 25, 2026
9e2cb51
docs(proxy-pod): correct OpenShift SCC, dns_peers, and migration guid…
russellb Aug 25, 2026
056f77c
fix(kubernetes): confirm CR deletion before fence teardown and fail r…
russellb Aug 25, 2026
0e4a40b
fix(kubernetes): gate migration upkeep on explicit config, not runtim…
russellb Aug 25, 2026
1758aca
docs(openshift): correct SCC guidance for sidecar topologies
russellb Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,71 @@ The shared state directory should preserve `sandbox_gid` inheritance
`@openshell-sidecar-ssh`; the network sidecar verifies its peer PID before
bridging gateway relay requests. No `ssh.sock` file should appear in the shared
state directory.
Inspect all three when sandbox registration or egress enforcement fails:

If `topology = "proxy-pod"` is rendered, each sandbox should have a
separate supervisor Deployment with one supervisor pod, a headless supervisor
Service, a proxy CA Secret, and two per-sandbox NetworkPolicies. The agent pod
should have `openshell.ai/sandbox-role=agent`; the supervisor pod should have
`openshell.ai/sandbox-role=supervisor`; both should share the same
`openshell.ai/sandbox-id`. The supervisor Deployment must have a controlling
`Sandbox` ownerReference. The Deployment pod template must carry the
`openshell.io/sandbox-id` annotation so the TokenReview bootstrap path can mint
a sandbox JWT. For supervisor pods, the gateway validates the
`Pod -> ReplicaSet -> Deployment -> Sandbox` owner chain, so missing
`apps/replicasets get` RBAC can also break bootstrap. Helm renders the
Deployment, ReplicaSet, Service, Secret, and NetworkPolicy RBAC when
`supervisor.topology=proxy-pod` (or when `supervisor.proxyPod.retainCompanionRbac=true`
during a migration away from proxy-pod), and scopes it by workspace mode:
`shared` grants it through the namespaced Role, while `managed` and `operator`
grant it through the ClusterRole (the sandbox namespace is per-workspace).
`list`/`watch` on `apps/deployments` are granted only through the namespaced Role
(shared mode): the supervisor-readiness Deployment watch runs only in shared
mode, and managed/operator modes deliberately avoid cluster-wide Deployment
enumeration, folding readiness in through get/list and the periodic reconcile
instead. If those resources fail with forbidden errors, confirm both the rendered
`gateway.toml` and Helm values use proxy-pod topology (or retainCompanionRbac)
and that the workspace mode's Role/ClusterRole was applied.
Companion cleanup is split: the owner-referenced Deployment, Service, CA Secret,
and supervisor-ingress NetworkPolicy are garbage-collected with the Sandbox CR
(the gateway holds no `delete` on them and no Secret read). The agent egress
NetworkPolicy — the workload's egress fence — carries no owner reference and is
deleted by the gateway only after the workload pod is gone, so a pod that ignores
SIGTERM cannot regain direct egress during its grace period; reconciliation reaps
any fence orphaned by a gateway crash (hence `delete`/`list` on networkpolicies).
Reconciliation runs at watch establishment and then periodically (~30s) while the
sandbox watch is up, so a transiently-failed stop-time supervisor scale-down or a
crash-orphaned fence is corrected without waiting for the watch to drop. If a
deleted sandbox leaves an `os-eg-...` NetworkPolicy behind, or a stopped
sandbox's `os-sup-...` Deployment keeps a replica, check that the gateway's
reconcile ran and that the workload pod actually terminated. When changing `supervisor.topology` away from proxy-pod while proxy-pod
sandboxes still exist, set `supervisor.proxyPod.retainCompanionRbac=true` and
leave it set until every such sandbox is deleted. The driver keeps managing them
by their persisted creation-time topology, so with the flag their companion RBAC,
periodic reconciliation, and readiness watch all keep working. Without it the RBAC
is removed and start/stop and crash-recovery for those sandboxes stop working
until the topology is restored.
If the agent cannot reach the gateway, check DNS to the headless Service, the
agent egress NetworkPolicy DNS exception for kube-dns/CoreDNS, and the
supervisor ingress NetworkPolicy allowing only that agent pod on port `3128`.

A proxy-pod sandbox falls back to `Provisioning` (Ready condition `False`,
reason `DependenciesNotReady`) when its supervisor Deployment has no available
replica: the gateway folds supervisor Deployment availability into sandbox
status so a sandbox never stays Ready while its policy-enforced egress path is
down, and recovers to `Ready` once the supervisor does. In shared mode the
gateway also watches supervisor Deployments (hence `list`/`watch` on
`apps/deployments` in the namespaced Role) and pushes a refreshed status within
seconds of an availability change; managed/operator modes and every mode's
direct `get`/`list` queries and periodic reconcile fold in the same check, so
readiness is never wrong for long even without the watch. If a previously-Ready
sandbox drops to `Provisioning`, inspect the supervisor Deployment (`kubectl -n
<sandbox-namespace> get deploy <os-sup-...>`) and its pod. Companion resource names are keyed on the
immutable sandbox UUID, so the `os-sup-`/`os-svc-`/`os-ca-`/`os-eg-`/`os-ing-`
suffix is stable per sandbox instance and distinct across instances even when
sandbox names repeat.

Inspect the relevant containers when sandbox registration or egress enforcement
fails:

```bash
kubectl -n openshell get configmap openshell-config -o jsonpath='{.data.gateway\.toml}' | grep -E '^\[openshell\.drivers\.kubernetes\]|^topology\s*='
Expand All @@ -522,6 +586,15 @@ kubectl -n <sandbox-namespace> logs <sandbox-pod> -c openshell-supervisor-networ
kubectl -n <sandbox-namespace> logs <sandbox-pod> -c agent --tail=200
```

In `proxy-pod` topology the network supervisor is NOT a container in the sandbox
pod — it runs in the separate per-sandbox supervisor `Deployment`. Get its logs
from that pod instead; the sandbox pod has only the workload `agent` container:

```bash
kubectl -n <sandbox-namespace> logs deploy/<os-sup-...> --tail=200
kubectl -n <sandbox-namespace> logs <sandbox-pod> -c agent --tail=200
```

#### Corporate upstream proxy

When the deployment routes sandbox egress through a corporate HTTP forward
Expand Down
52 changes: 48 additions & 4 deletions .agents/skills/helm-dev-environment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,28 @@ mise run helm:skaffold:run:sidecar
mise run helm:skaffold:run:sidecar-mtls
```

Both commands build the `gateway` and `supervisor` images and deploy the OpenShell Helm
**Supervisor proxy-pod topology** (build once and leave running):
```bash
mise run helm:skaffold:run:proxy-pod
```

All Skaffold commands build the `gateway` and `supervisor` images and deploy the OpenShell Helm
chart. The sidecar profile renders an `openshell-network-init` init container for
nftables setup and an `openshell-supervisor-network` runtime sidecar for proxying.
Binary-aware policy mode runs that sidecar as UID 0 with `SYS_PTRACE` and
`DAC_READ_SEARCH`; relaxed mode can run it as the configured proxy UID, which
must be at least `1000` and distinct from the workload UID. The
sidecar-mTLS profile reuses `ci/values-sidecar.yaml` and restores
`server.disableTls=false` inline for Skaffold. The `pkiInitJob` hook (a pre-install
Job that runs `openshell-gateway generate-certs`) generates mTLS secrets on first
install. Envoy Gateway opt-in; see the Optional Add-ons section below.
`server.disableTls=false` inline for Skaffold. The proxy-pod profile renders
network supervision in a separate supervisor Deployment with one pod and relies
on Kubernetes NetworkPolicy enforcement so the agent pod can reach only its
paired supervisor plus DNS. The
default local k3s/k3d cluster keeps k3s's embedded NetworkPolicy controller
enabled; if you replace the CNI, install a policy-enforcing CNI before using
proxy-pod. The
`pkiInitJob` hook (a pre-install Job that runs `openshell-gateway
generate-certs`) generates mTLS secrets on first install. Envoy Gateway opt-in;
see the Optional Add-ons section below.

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

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

### Kubernetes e2e profiles

Run the default Kubernetes e2e environment:

```bash
mise run e2e:kubernetes
```

Run the sidecar topology e2e environment:

```bash
mise run e2e:kubernetes:sidecar
```

Run the proxy-pod topology e2e environment:

```bash
mise run e2e:kubernetes:proxy-pod
```

The proxy-pod e2e task applies `ci/values-proxy-pod.yaml` through
`OPENSHELL_E2E_KUBE_EXTRA_VALUES`. Use an existing cluster with NetworkPolicy
enforcement, or let the wrapper create the default local k3d/k3s cluster with
k3s's embedded NetworkPolicy controller enabled.

### TLS behaviour

`ci/values-skaffold.yaml` sets `server.disableTls: true`, so Skaffold-based deploys run
Expand Down Expand Up @@ -150,6 +187,12 @@ For a sidecar-profile deployment:
mise run helm:skaffold:delete:sidecar
```

For a proxy-pod-profile deployment:

```bash
mise run helm:skaffold:delete:proxy-pod
```

### Delete the cluster entirely

```bash
Expand Down Expand Up @@ -275,6 +318,7 @@ for dependencies still declared in `Chart.yaml`.
| `deploy/helm/openshell/ci/values-high-availability.yaml` | HA test overlay (`replicaCount: 2` with external PostgreSQL Secret) |
| `deploy/helm/openshell/ci/values-keycloak.yaml` | Keycloak OIDC overlay |
| `deploy/helm/openshell/ci/values-sidecar.yaml` | Supervisor sidecar topology overlay for Kubernetes e2e/dev |
| `deploy/helm/openshell/ci/values-proxy-pod.yaml` | Supervisor proxy-pod topology overlay for Kubernetes e2e/dev; requires NetworkPolicy enforcement |
| `deploy/helm/openshell/ci/values-spire.yaml` | SPIFFE/SPIRE provider token grant overlay |
| `deploy/helm/openshell/ci/values-spire-stack.yaml` | SPIRE hardened chart values for local dev |
| `deploy/helm/openshell/ci/values-tls-disabled.yaml` | Lint-only: TLS + auth disabled (reverse-proxy edge termination) |
Expand Down
11 changes: 11 additions & 0 deletions .agents/skills/openshell-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,17 @@ remain updateable.

## Workflow 3: Sandbox Lifecycle

> **Proxy-pod topology is sessionless.** When the Kubernetes driver runs with
> `supervisor.topology=proxy-pod`, the sandbox has no in-pod supervisor session,
> so relay-backed operations — a trailing `-- <command>`, `--upload`,
> `--forward`, `--editor`, `sandbox connect`, `sandbox exec`, `sandbox upload`,
> and `sandbox download` — are rejected with a topology-specific error. Run the
> workload as the container entrypoint via
> `--driver-config-json '{"kubernetes":{"containers":{"agent":{"command":[...]}}}}'`
> and create with `--detach`. Bake required files into the image instead of
> uploading. The rest of this workflow applies to `combined` and `sidecar`
> topologies.

### Create with options

```bash
Expand Down
24 changes: 22 additions & 2 deletions .github/workflows/branch-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,24 @@ jobs:
e2e-task: e2e:kubernetes:workspace-operator
cli-artifact-prefix: rust-binary-cli

kubernetes-proxy-pod-e2e:
needs: [pr_metadata, build-gateway, build-supervisor, build-cli]
if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true'
permissions:
actions: read
contents: read
packages: read
uses: ./.github/workflows/e2e-kubernetes-test.yml
with:
image-tag: ${{ github.sha }}
# kind's default CNI does not enforce NetworkPolicies, so this exercises
# the proxy-pod control-plane contract (companions, readiness, sessionless
# relay rejection). The CNI-enforced egress isolation test is tracked
# separately and needs a policy-enforcing CNI.
job-name: Kubernetes E2E (proxy-pod topology)
e2e-task: e2e:kubernetes:proxy-pod
cli-artifact-prefix: rust-binary-cli

kubernetes-ha-e2e:
needs: [pr_metadata, build-gateway, build-supervisor, build-cli]
if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true'
Expand Down Expand Up @@ -254,7 +272,7 @@ jobs:

core-e2e-result:
name: Core E2E result
needs: [pr_metadata, build-gateway, build-supervisor, build-cli, build-driver-vm-linux, e2e, kubernetes-e2e, kubernetes-external-driver-e2e, kubernetes-workspace-managed-e2e, kubernetes-workspace-operator-e2e]
needs: [pr_metadata, build-gateway, build-supervisor, build-cli, build-driver-vm-linux, e2e, kubernetes-e2e, kubernetes-external-driver-e2e, kubernetes-workspace-managed-e2e, kubernetes-workspace-operator-e2e, kubernetes-proxy-pod-e2e]
if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true'
runs-on: ubuntu-latest
steps:
Expand All @@ -269,6 +287,7 @@ jobs:
KUBERNETES_EXTERNAL_DRIVER_E2E_RESULT: ${{ needs.kubernetes-external-driver-e2e.result }}
KUBERNETES_WORKSPACE_MANAGED_E2E_RESULT: ${{ needs.kubernetes-workspace-managed-e2e.result }}
KUBERNETES_WORKSPACE_OPERATOR_E2E_RESULT: ${{ needs.kubernetes-workspace-operator-e2e.result }}
KUBERNETES_PROXY_POD_E2E_RESULT: ${{ needs.kubernetes-proxy-pod-e2e.result }}
run: |
set -euo pipefail
failed=0
Expand All @@ -281,7 +300,8 @@ jobs:
"kubernetes-e2e:$KUBERNETES_E2E_RESULT" \
"kubernetes-external-driver-e2e:$KUBERNETES_EXTERNAL_DRIVER_E2E_RESULT" \
"kubernetes-workspace-managed-e2e:$KUBERNETES_WORKSPACE_MANAGED_E2E_RESULT" \
"kubernetes-workspace-operator-e2e:$KUBERNETES_WORKSPACE_OPERATOR_E2E_RESULT"; do
"kubernetes-workspace-operator-e2e:$KUBERNETES_WORKSPACE_OPERATOR_E2E_RESULT" \
"kubernetes-proxy-pod-e2e:$KUBERNETES_PROXY_POD_E2E_RESULT"; do
name="${item%%:*}"
result="${item#*:}"
if [ "$result" != "success" ]; then
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ http-body-util = "0.1"
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"] }
rustls = { version = "0.23", default-features = false, features = ["std", "logging", "tls12", "ring"] }
rustls-pemfile = "2"
rcgen = { version = "0.13", features = ["crypto", "pem"] }
rcgen = { version = "0.13", features = ["crypto", "pem", "x509-parser"] }
webpki-roots = "1"
rustls-native-certs = "0.8"

Expand Down
26 changes: 22 additions & 4 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,30 @@ Podman, and VM drivers deliver the initial token through supervisor-only
runtime material; Kubernetes supervisors exchange a projected ServiceAccount
token through `IssueSandboxToken`. The gateway validates that projected token
with Kubernetes `TokenReview`, requires the configured sandbox service account,
checks the returned pod binding against the live pod UID, and verifies the pod's
controlling `Sandbox` ownerReference against the live Sandbox CR UID and
sandbox-id label before minting the gateway JWT. The bootstrap path accepts
checks the returned pod binding against the live pod UID, and verifies the
pod's ownership against the live Sandbox CR UID and sandbox-id label before
minting the gateway JWT. Agent pods must be directly controlled by the
`Sandbox` CR. Proxy-pod supervisor pods may be controlled through the Kubernetes
`Pod -> ReplicaSet -> Deployment -> Sandbox` chain. The bootstrap path accepts
both `agents.x-k8s.io/v1beta1` ownerReferences from newer Agent Sandbox
controllers and `agents.x-k8s.io/v1alpha1` ownerReferences from existing
deployments. Supervisors renew gateway JWTs in memory before expiry only while
deployments. The proxy-pod gateway Role follows least privilege: the supervisor
Deployment, Service, CA Secret, and supervisor-ingress NetworkPolicy are
owner-referenced to the Sandbox CR and garbage-collected with it, so the gateway
holds no `delete` on them (Deployment create/get/patch, Service
create/get, Secret create only, plus get on the ReplicaSet for the owner-chain
check). In shared (single-namespace) mode the namespaced Role also grants
Deployment `list`/`watch`, backing a supervisor Deployment watch that pushes a
refreshed sandbox status within seconds of a supervisor availability change;
managed and operator modes deliberately omit those verbs to avoid cluster-wide
Deployment enumeration, folding readiness in through get/list instead. A periodic
reconcile (alongside the one at watch establishment) corrects supervisor replica
drift and reaps orphaned fences without waiting for the watch to drop. The agent
egress NetworkPolicy — the workload's egress fence — carries
no owner reference so it can outlive the workload pod during deletion; the gateway
manages its lifecycle directly and holds create/get/delete/list on NetworkPolicies
for ordered teardown and orphan reaping.
Supervisors renew gateway JWTs in memory before expiry only while
the sandbox record still exists. Older tokens are not server-revoked; shared
deployments bound replay exposure with short `gateway_jwt.ttl_secs` lifetimes.
The config default is
Expand Down
Loading
Loading