feat(kubernetes): add proxy-pod topology (in-pod process supervisor, out-of-pod proxy) - #2885
feat(kubernetes): add proxy-pod topology (in-pod process supervisor, out-of-pod proxy)#2885russellb wants to merge 3 commits into
Conversation
4308aa6 to
575a447
Compare
3f1f2f2 to
492ff77
Compare
|
I'm still running local review, but I think this is ready to look at and try out |
Major update: proxy-pod now keeps the full supervisor feature setThis PR has been redesigned. The earlier revision moved the entire The new design moves only the network proxy into the separate per-sandbox Retained (validated on a live OpenShift / OVN-Kubernetes cluster):
Still least-privileged. The agent pod adds no privileges to keep this Safe with the proxy out of reach. The agent pod holds a per-sandbox The one remaining structural gap vs. the in-pod topologies is process |
|
The proxy-pod mode appears to leave the provider SPIFFE Workload API socket mounted into the agent pod. The base pod template adds SPIFFE_WORKLOAD_API_VOLUME_NAME and OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET when provider_spiffe_enabled is true. In sidecar mode both are removed from the agent container. The proxy-pod transform does not do the same cleanup. The workload API is only needed in the supervisor proxy pod. |
|
Correct — the proxy-pod transform was missing the SPIFFE cleanup that sidecar does. Fixed in b3b3c75. The proxy-pod agent transform now strips all three from the agent pod: the This reinforces the scoped-credential model: the agent pod's process-kind token is already denied provider RPCs, and now it also has no Workload API socket to obtain an SVID from. |
2000krysztof
left a comment
There was a problem hiding this comment.
Suggestion: topology-aware SCC guidance for OpenShift
This section instructs all OpenShift users to grant privileged SCC to the sandbox service account. With the proxy-pod topology, the enforcement boundary moves outside the pod, sandbox pods run with drop: ALL capabilities and need zero elevated privileges.
Could this guide be updated to show the topology-specific path?
- proxy-pod: nonroot-v2 is sufficient (built-in, no security exception required). The chart already supports sandboxServiceAccount.openshift.nonrootSCC=true which handles this automatically — no manual oc adm policy step needed.
- combined / sidecar: still requires privileged SCC as today.
As written, users deploying proxy-pod on OpenShift will follow these instructions and grant privileged unnecessarily, which undermines the security benefit this topology provides.
OpenShell/deploy/helm/openshell/README.md
Lines 49 to 50 in 06f0aa6
Validated under Kata Containers (VM-based RuntimeClass)Ran the proxy-pod topology on OpenShift with Confirmed real VM isolation: the agent pod reports the Full feature set works in-guest:
No topology-specific issues surfaced under Kata. |
|
@2000krysztof Correct — the chart README's quick-start granted The README OpenShift section is now split by topology:
Edited the |
|
OpenShift/OVN validation found that proxy-pod agent Pods receive A controlled test confirmed that adding only |
|
Thanks, @jhjaggars! I pulled your change in. |
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; run proxy-pod workloads directly and harden them. Signed-off-by: Taylor Mutch <taylormutch@gmail.com> Signed-off-by: Russell Bryant <rbryant@redhat.com>
Add the Kubernetes `proxy-pod` supervisor topology. The process supervisor stays in the agent pod and launches the workload as in `combined`; only the network proxy moves out, into a paired, per-sandbox supervisor Deployment. The sole thing that crosses the pod boundary is the workload's egress, fenced by Kubernetes NetworkPolicy rather than pod-local nftables. Because the supervisor still runs beside the workload, `proxy-pod` keeps the full supervisor contract (SSH, exec, connect, upload/download, port forwarding, `openshell logs`, and Landlock/seccomp filesystem+process policy) while producing the least-privileged sandbox pod of any topology: runAsNonRoot, all Linux capabilities dropped, no privilege escalation, no privileged init container, no shared process namespace. It is the only topology admitted by a stock OpenShift non-root SCC with no bespoke security grant. Per-sandbox companion resources (keyed on the immutable sandbox UUID): - a supervisor Deployment running the network proxy (`--mode=network`), - a headless Service fronting it, - a generated proxy-CA Secret, - an agent-egress NetworkPolicy (workload may reach only the proxy and cluster DNS) and a supervisor-ingress NetworkPolicy (only the paired workload may reach the proxy port). The Deployment, Service, Secret, and supervisor-ingress policy are owner-referenced to the Sandbox CR and garbage-collected with it. The agent-egress fence is deliberately not owner-referenced: Kubernetes GC does not order sibling deletion, so a GC-owned fence could be removed alongside the workload pod and let a SIGTERM-ignoring workload regain direct egress during its grace period. The gateway manages the fence directly, deleting it only after the workload pod is gone and reaping any fence orphaned by a gateway crash. Scoped credential: the agent pod holds a per-sandbox, process-kind gateway JWT (`SandboxCallerKind::Process`, minted from the pod's `openshell.ai/sandbox-role=agent` label). It can drive its own sandbox's control plane (relays, policy fetch, log push, token refresh) but is denied `GetSandboxProviderEnvironment`, `ExchangeProviderSubjectToken`, and `GetInferenceBundle` at the multiplex authorization chokepoint. Provider credentials and inference routing live only with the out-of-pod proxy; the provider SPIFFE Workload API socket is stripped from the agent pod entirely. The in-pod supervisor owns its own gateway session (reported readiness model `Required`), so relays work locally over an abstract SSH socket with no sidecar control socket, shared PID namespace, or cross-pod SSH trust. A `gateway_peers` egress rule permits that session through the agent-egress fence. Readiness and lifecycle: - `SupervisorSessionModel` on the driver `DriverSandboxStatus` contract lets a topology report whether it runs an in-sandbox supervisor session (`UNSPECIFIED` preserves existing behavior). - A `wait-for-proxy` agent-pod init container (new `wait-for-tcp` supervisor subcommand) makes pod readiness transitively mean egress works. - The supervisor Deployment's live availability folds into sandbox readiness (tri-state, so a transient GET error never fails open to Ready); a shared-mode Deployment watch pushes status within seconds, with get/list and a periodic reconcile as backstops. The periodic reconcile also corrects supervisor replica drift and reaps orphaned egress fences. - `stop`/`start` scale the supervisor Deployment to zero and back. - Agent-container command/args overrides are rejected in every topology (the supervisor is the entrypoint). OpenShift enablement: - Configurable cluster DNS peers (`proxy_pod.dns_peers`) and gateway peers (`proxy_pod.gateway_peers`) with per-peer namespace/pod selectors and port; empty lists are rejected and an empty peer renders no rule rather than allow-all. - A gated grant of the built-in `nonroot-v2` SCC (`sandboxServiceAccount.openshift.nonrootSCC`, ClusterRole/Binding only, no custom SCC object). Topology-aware install guidance in the chart README and docs: combined/sidecar require `privileged`; proxy-pod uses `nonroot-v2`. Under a VM RuntimeClass (e.g. Kata Containers), the workload and proxy land in separate pods and therefore separate VMs with separate kernels, raising the isolation ceiling beyond the shared-VM in-pod topologies. HA correctness: `SupervisorSession=NotApplicable` is published as a durable status condition so every gateway replica (not just the reconciler lease holder) rejects relay-backed RPCs with an immediate terminal error; corporate upstream-proxy credential Secrets are rejected for proxy-pod (they would land in the workload pod); and `supervisor.proxyPod.retainCompanionRbac` keeps the companion RBAC, reconcile, and readiness watch working for existing proxy-pod sandboxes after the gateway's configured topology is switched away. Includes the RFC (`rfc/proxy-pod-topology-DRAFT.md`), docs, Helm values and tests, and a capability-scoped e2e suite. Continues the proxy-pod work begun by Taylor Mutch. Signed-off-by: Russell Bryant <rbryant@redhat.com>
Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
f9d1454 to
15c16b4
Compare
|
I pretty strongly think we should do this for the podman driver too; podman very intentionally supports Kubernetes concepts like pods, so we can move the proxy out to a separate container in the same pod there too. |
Summary
Adds the Kubernetes
proxy-podsupervisor topology. The process supervisorstays in the agent pod and launches the workload as it does in
combined;only the network proxy moves out, into a paired, per-sandbox supervisor
Deployment. The sole thing that crosses the pod boundary is the workload'segress, which is fenced by two per-sandbox Kubernetes
NetworkPolicyobjectsrather than by pod-local nftables.
Because the supervisor still runs beside the workload,
proxy-podkeeps thefull supervisor contract — SSH,
exec,connect, upload/download, portforwarding,
openshell logs, and Landlock/seccomp filesystem+process policy —while producing the least-privileged sandbox pod any OpenShell topology
produces:
runAsNonRoot, all Linux capabilities dropped, no privilegeescalation, no added caps at any layer, no privileged init container, no shared
process namespace. That is what lets it run on OpenShift under a stock,
Red Hat-shipped non-root SCC with no bespoke security grant.
Two mechanisms make this safe with the supervisor out of one process' reach:
gateway JWT. It can drive its own sandbox's control plane (relays, policy
fetch, log push, token refresh) but is denied provider-secret and inference
RPCs at the gateway authorization chokepoint. Provider credentials and
inference routing live only with the out-of-pod proxy.
the proxy (plus cluster DNS); a supervisor-ingress policy lets only the
paired workload reach the proxy port. All other egress is dropped by the CNI.
This supersedes the earlier network-only proxy-pod design (supervisor entirely
out of the pod), which lost SSH/exec/sync/filesystem policy and required a
workload-command override. Keeping the process supervisor in-pod recovers those
features; see the design-pivot note below.
This continues @TaylorMutch's original proxy-pod work in #2077 (and the
earlier #2016), rebased onto current
main. It is the sibling of thecni-sidecartopology in #2606.Design pivot: in-pod process supervisor
The original revision of this PR moved the entire supervisor out of the
sandbox pod. That made SSH/
exec/upload/sync structurally impossible (everyrelay targets something inside the sandbox, and the SSH server needs the
workload's PID/mount/network namespaces), dropped filesystem/process policy, and
required running the image through a
containers.agent.commandoverride.The current design moves only the network proxy out. The process supervisor
— the piece that launches the workload, applies Landlock/seccomp, and serves
relays — stays in the agent pod and owns its own scoped gateway session. Nothing
about the workload's namespaces has to cross a pod boundary, so the feature loss
is recovered. The privilege win is preserved because network enforcement (the
only part that needed
NET_ADMIN/nftables) is what left the pod.Sandbox-pod privileges: none added
To retain full functionality the agent pod needs zero added privileges:
runAsNonRoottrueallowPrivilegeEscalationfalsecapabilitiesdrop: [ALL](none added)NET_ADMIN)shareProcessNamespace/ host namespaces / hostPathFilesystem/process policy works without privilege because Landlock is
unprivileged and seccomp applies under
no_new_privs(set byallowPrivilegeEscalation:false); relays bind an abstract unix socket (no caps);network/L7 policy is enforced out-of-pod, so the agent needs no
NET_ADMIN.proxy-podmaps toProcessEnforcementMode::NetworkOnly, which skips only theroot/capability-requiring setup while still enforcing the child sandbox.
Topology tradeoffs (combined / sidecar / cni-sidecar / proxy-pod)
In every topology the network supervisor (the OpenShell proxy) is the egress
policy engine. The topologies differ in where the proxy runs, how the
workload is confined to it, and — for
proxy-pod— the credential scope in thesandbox pod.
combined(default)sidecarcni-sidecarproxy-podNetworkPolicyexec/connect/ upload / syncopenshell logsSYS_PTRACE,DAC_READ_SEARCH¹SYS_PTRACE,DAC_READ_SEARCH¹NetworkPolicyenforcementprivilegedprivilegedCNI¹ On the in-pod network sidecar container (
--mode=network), added whenbinary-aware network policy is enabled — the capabilities (plus the shared PID
namespace) let the in-pod network supervisor read
/proc/<pid>of workloadprocesses to attribute connections to a binary/PID. That is exactly the feature
proxy-podgives up: its proxy runs in a separate pod with no view of theworkload's processes, so it needs neither capability. The agent container drops
all capabilities in every topology.
When to use which.
combinedstays the default.sidecar/cni-sidecarkeep the full contract while lowering pod privilege, at the cost of a per-pod
privileged init container or a node-level CNI DaemonSet.
proxy-podkeeps thefull contract and the lowest pod privilege — the only topology admitted by a
stock OpenShift non-root SCC — at the cost of a second pod per sandbox, a
dependency on CNI
NetworkPolicyenforcement, and a scoped sandbox-podcredential. It also raises the isolation ceiling under a VM
RuntimeClass:because the workload and proxy are in separate pods, Kata places them in separate
VMs with separate kernels, so a kernel compromise in the workload VM does not by
itself reach the proxy or its provider credentials.
The remaining structural gap versus the in-pod topologies is process
attribution on network events: the proxy runs in a different pod and cannot map
a connection back to the originating PID/binary, so those events render without
process identity.
RFC
rfc/proxy-pod-topology-DRAFT.md(included in this PR, unnumbered pending amaintainer-assigned number) documents the in-pod-process-supervisor design:
motivation, per-sandbox resources, the credential-scope and NetworkPolicy model,
OpenShift enablement (cluster DNS peers/port, stock non-root SCC), readiness
without an in-sandbox session, the retained feature set, remaining tradeoffs,
risks, alternatives, and open questions. The superseded network-only design is
retained in the RFC for context.
Related Issue
Continues @TaylorMutch's proxy-pod PR #2077, which references #1827, #981,
#899, and #1305. Maintainers assign the RFC number from the originating issue
before it leaves draft.
Changes
Topology:
proxy-podsupervisor topology: the agent pod runs the processsupervisor (
--mode=process) which launches the workload and serves relays;a per-sandbox supervisor
Deploymentruns the network proxy (--mode=network),fronted by a headless
Service, a generated proxy CASecret, and anagent-egress / supervisor-ingress
NetworkPolicypair.non-root supervisor-binary sideload init container; the SSH relay uses a
netns-scoped abstract socket so no writable
/runpath is needed.owner-referenced to the Sandbox CR and GC'd with it. The agent-egress fence is
deliberately not owner-referenced (Kubernetes GC does not order sibling
deletion, so a GC-owned fence could be removed alongside the workload pod and
let a
SIGTERM-ignoring workload regain direct egress); the gateway manages itdirectly, deleting it only after the workload pod is gone and reaping any fence
orphaned by a gateway crash.
sandbox-name reuse. Nested
proxy_pod.proxy_uid/proxy_pod.affinityconfigand Helm values.
Scoped credential (new):
SandboxCallerKind {Full, Process}claim in the sandbox JWT. The agent pod isminted a
Process-kind token (based on itsopenshell.ai/sandbox-role=agentpod label);
Processcallers are deniedGetSandboxProviderEnvironment,ExchangeProviderSubjectToken, andGetInferenceBundleat the multiplexauthorization chokepoint. Token refresh preserves the caller kind.
OpenShift enablement:
proxy_pod.dns_peers) and gateway peers(
proxy_pod.gateway_peers, so the in-pod supervisor's own gateway session isallowed through the egress fence), with per-peer namespace/pod selectors and
port. Empty lists are rejected; an empty peer renders no rule rather than
allow-all.
sandboxServiceAccount.openshift.nonrootSCC,default off) — a ClusterRole/Binding only, no custom SCC object.
Readiness & lifecycle:
proxy-podreportsSupervisorSessionModel::Required(relays are available).wait-for-proxyagent-pod init container (via await-for-tcpsupervisorsubcommand) so pod readiness transitively means egress works.
Deployment's live availability folds into sandbox readiness(tri-state, so a transient GET error never fails open to
Ready); a shared-modeDeployment watch pushes status within seconds, with get/list and the periodic
reconcile as backstops.
orphaned egress fences (re-confirming the Sandbox CR is gone immediately before
deleting a fence).
stop/startscale the supervisor Deployment to zero and back.Correctness fixes (rebase + cluster testing):
proxy-pod(would landin the workload pod).
containers.agent.command/argsoverrides in every topology (thesupervisor is the entrypoint).
OpenShift
nonroot-v2admission/startup fixes found in cluster validation).Docs: RFC,
docs/kubernetes/topology.mdx,docs/kubernetes/openshift.mdx,docs/reference/sandbox-compute-drivers.mdx,docs/reference/gateway-config.mdx,Helm README, and the
debug-openshell-cluster/helm-dev-environmentskills.Testing
cargo test -p openshell-driver-kubernetes(251) and-p openshell-server --lib(1432).mise run helm:test— 122 pass (proxy-pod DNS/gateway-peer, SCC-grant, RBAC cases).mise run pre-commit— clean.proxy_pode2e suite (rewritten for the in-pod design) asserts aplain create reaches
Readyand thatexecand upload round-trip through thein-pod supervisor.
sandbox reaches
Readywith the process supervisor running non-root in theagent pod (all four init containers non-root, admitted by a stock non-root SCC);
exec,exec --tty(PTY →/dev/pts/0), interactiveconnect,upload/download, and
ssh -Lport forwarding all work;openshell logsstreams workload/supervisor output and shows Landlock policy being applied;
the agent pod's
Process-kind token is denied provider RPCs (gateway returnsPERMISSION_DENIED) while the controller'sFulltoken succeeds; workloadegress is fenced (direct egress blocked, proxied egress policy-evaluated:
allow → 200, deny → 403 at CONNECT) with the generated CA trusted; the in-pod
supervisor reaches the gateway via the
gateway_peersegress rule.RuntimeClass): re-validated the full flow withruntimeClassName: kataon both pods. Each pod runs in its own Kata VM(confirmed via the
hypervisorCPU flag and VM-bounded memory), so theworkload and network proxy get separate VMs/kernels. Ready,
exec/--tty,upload/download, the cross-VM egress fence (direct blocked, proxied L7 403),
the scoped process-kind credential, in-guest Landlock policy, and
openshell logsall work; no topology-specific issues under Kata.Checklist