Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
040024c
feat(kubernetes): add cni-sidecar supervisor topology
TaylorMutch Jun 30, 2026
e56d1f0
docs(cni): design for cni-sidecar on OpenShift Multus
russellb Jul 30, 2026
76b681f
feat(cni): add multus-chain install mode for OpenShift
russellb Jul 30, 2026
cdc36e8
feat(cni): add gated privileged SCC for OpenShift
russellb Jul 30, 2026
b7f2f90
feat(helm): add OpenShift cni-sidecar values overlay
russellb Jul 30, 2026
07d84a1
docs(cni): document OpenShift multus-chain mode
russellb Jul 30, 2026
202088a
fix(cni): scope privileged SCC to a dedicated CNI service account
russellb Jul 30, 2026
c791869
feat(helm): add minimal sandbox SCC for OpenShift binary-aware policy
russellb Jul 30, 2026
103b387
docs(rfc): propose cni-sidecar supervisor topology
russellb Jul 31, 2026
7e3af74
docs(rfc): link topology PRs in cni-sidecar RFC
russellb Aug 3, 2026
19b1ee2
fix(cni-sidecar): address review findings for OpenShift topology
russellb Aug 3, 2026
0b9085d
fix(cni-sidecar): close egress-enforcement gaps from re-review
russellb Aug 4, 2026
da32f5d
fix(cni-sidecar): harden readiness lifecycle, init, and IPv6 detection
russellb Aug 4, 2026
ea414ae
fix(cni-sidecar): make the CNI installer a cluster singleton
russellb Aug 4, 2026
bcdec5f
fix(cni-sidecar): drop brittle owner guard, upgrade any openshell-cni…
russellb Aug 4, 2026
c207273
fix(cni-sidecar): harden sidecar init, scope API lookups, gate reboots
russellb Aug 4, 2026
8396e40
docs(cni-sidecar): correct multi-release workflow, boot-taint, and si…
russellb Aug 4, 2026
d4d8115
feat(cni-sidecar): auto-aggregate sandbox namespaces into the CNI all…
russellb Aug 4, 2026
1205a36
feat(cni-sidecar): Helm-owned registration marker + wait-for-ack gate
russellb Aug 4, 2026
352f32d
fix(cni): harden cni-sidecar egress enforcement
russellb Aug 6, 2026
a9d5f8b
fix(cni): correct k3s CNI bin path and stale registration docs
russellb Aug 12, 2026
78cc628
chore: stop tracking docs/superpowers scratch dir
russellb Aug 12, 2026
dd774a1
feat(cni): standalone CNI-only install mode for independent lifecycle
russellb Aug 13, 2026
79478a3
docs(cni): point OpenShift multi-gateway guidance at standalone CNI
russellb Aug 13, 2026
b839bfe
fix(cni): review fixes + fail-closed contract-version gate
russellb Aug 13, 2026
ae189f4
docs(rfc): cover CNI contract versioning for mixed-version clusters
russellb Aug 13, 2026
ced96d1
fix(cni): handle CniSidecar in upstream-proxy-auth secret mode
russellb Aug 14, 2026
b9a43fa
fix(helm): skip Agent Sandbox preflight in cni-only mode
russellb Sep 8, 2026
3d0799f
fix(cni): satisfy Rust 1.95 clippy lints in the CNI plugin
Sep 8, 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
42 changes: 40 additions & 2 deletions .agents/skills/helm-dev-environment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,21 @@ 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 CNI-sidecar topology** (build once and leave running):
```bash
mise run helm:skaffold:run:cni-sidecar
```

These 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
`server.disableTls=false` inline for Skaffold. The cni-sidecar profile enables
the privileged OpenShell CNI DaemonSet and uses the sidecar runtime model
without the pod-local network init container. The `pkiInitJob` hook (a pre-install
Job that runs `openshell-gateway generate-certs`) generates mTLS secrets on first
install. The default Skaffold values export gateway and Kubernetes-driver traces to
the collector service installed by `helm:k3s:create`. Envoy Gateway opt-in; see the
Expand Down Expand Up @@ -131,6 +138,30 @@ export only while it is reachable.
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 CNI-sidecar topology e2e environment:

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

The cni-sidecar e2e task applies `ci/values-cni-sidecar.yaml` through
`OPENSHELL_E2E_KUBE_EXTRA_VALUES` and requires OpenShell CNI installer
permissions on the target cluster's nodes.

### TLS behaviour

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

For a cni-sidecar-profile deployment:

```bash
mise run helm:skaffold:delete:cni-sidecar
```

### Delete the cluster entirely

```bash
Expand Down Expand Up @@ -321,6 +358,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-cni-sidecar.yaml` | Supervisor CNI-sidecar topology overlay for Kubernetes e2e/dev; enables the OpenShell CNI DaemonSet |
| `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
16 changes: 16 additions & 0 deletions .github/actions/build-docker-image/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ runs:
INPUTS_BINARY: ${{ inputs.binary }}
INPUTS_ARCH: ${{ inputs.arch }}

# The supervisor image also carries the chained CNI plugin, which the CNI
# installer DaemonSet copies onto each node for the cni-sidecar topology.
- name: Download openshell-cni
if: ${{ inputs.component == 'supervisor' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openshell-cni-${{ inputs.triple }}
path: artifact-cni

- name: Stage openshell-cni
if: ${{ inputs.component == 'supervisor' }}
shell: bash
run: install -Dm0755 artifact-cni/openshell-cni deploy/docker/.build/prebuilt-binaries/${INPUTS_ARCH}/openshell-cni
env:
INPUTS_ARCH: ${{ inputs.arch }}

- name: Build ${{ inputs.component }} image
shell: bash
env:
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/build-sandbox-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,27 @@ jobs:
image-tag: ${{ inputs.image-tag }}
checkout-ref: ${{ inputs.checkout-ref }}
secrets: inherit

# The chained CNI plugin ships inside the supervisor image so the CNI
# installer DaemonSet can copy it onto each node.
build-cni:
strategy:
matrix:
include:
- triple: x86_64-unknown-linux-musl
runner: linux-amd64-cpu8
dev_shell: .#devShells.x86_64-linux.musl
- triple: aarch64-unknown-linux-musl
runner: linux-arm64-cpu8
dev_shell: .#devShells.aarch64-linux.musl
uses: ./.github/workflows/build-binaries.yml
with:
package: openshell-cni
binary: openshell-cni
triple: ${{ matrix.triple }}
runner: ${{ matrix.runner }}
dev-shell: ${{ matrix.dev_shell }}
cargo-version: ${{ inputs.cargo-version }}
image-tag: ${{ inputs.image-tag }}
checkout-ref: ${{ inputs.checkout-ref }}
secrets: inherit
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ mise.local.toml
# Ignore plans for now
architecture/plans

# Superpowers scratch (plans/specs) — local working docs, not versioned
docs/superpowers/

rfc.md
.worktrees
.z3-trace
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
| `crates/openshell-server/` | Gateway server | Control-plane API, sandbox lifecycle, auth boundary |
| `crates/openshell-sandbox/` | Sandbox runtime | Container supervision, policy-enforced egress routing |
| `crates/openshell-policy/` | Policy engine | Filesystem, network, process, and inference constraints |
| `crates/openshell-cni/` | Chained CNI plugin | Node-level egress bypass-prevention for the cni-sidecar topology; also sets the per-node readiness label |
| `crates/openshell-router/` | Privacy router | Privacy-aware LLM routing |
| `crates/openshell-bootstrap/` | Gateway metadata | Gateway registration metadata, auth token storage, mTLS bundle storage |
| `crates/openshell-gateway-interceptors/` | Gateway interceptors | Intercepts and transforms configured gRPC requests at the gateway routing boundary |
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

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

116 changes: 116 additions & 0 deletions architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,122 @@ already unprivileged. Sidecar pods use a shared process namespace so the
network sidecar can resolve workload process and binary identity through
`/proc/<entrypoint-pid>`.

The cni-sidecar topology keeps the sidecar runtime model and its shared-state
boundary, but removes the privileged `openshell-network-init` init container and
its `NET_ADMIN`. Instead, the privileged OpenShell CNI DaemonSet installs the
pod-network bypass-prevention rules during CNI `ADD` using nftables or iptables.
The driver annotates sandbox pods so the chained CNI plugin can read the proxy
UID and enforcement mode. The network sidecar keeps the same privilege profile
as the other sidecar topologies: in the default binary-aware mode it runs as UID
0 with `SYS_PTRACE` and `DAC_READ_SEARCH` (but no `NET_ADMIN`) to resolve
cross-UID `/proc`, and only stays non-root with no added capabilities when
`process_binary_aware_network_policy` is disabled. The agent container stays
non-root with no added Linux capabilities in either mode. Because the node CNI —
not an in-pod container — programs the firewall, no container in the sandbox pod
holds `NET_ADMIN`.

The CNI installer supports two modes. `conflist` (default) appends the
`openshell-cni` plugin to an existing CNI `.conflist` (k3s / vanilla). On
OpenShift (Multus / OVN-Kubernetes) there is no appendable `.conflist`, so
`multus-chain` writes a standalone plugin `.conf` into the Multus
`vendor-cni-chain` auxiliary-chain directory and stores plugin credentials in a
persistent `stateDir`. Neither mode modifies a CNO-managed file.

The installer patches the chained plugin at startup and then re-verifies it on a
reconcile tick, re-patching when the plugin is missing. This keeps enforcement in
place across CNI config rewrites (for example a CNO reconcile) and DaemonSet
restarts, bounding any such gap to one reconcile interval. The chained plugin
lives in the host CNI config and survives installer pod restarts, so an ordinary
DaemonSet restart or rolling update never strips enforcement: the `preStop` hook
removes it only when the owning DaemonSet is actually being deleted (helm
uninstall), and fences the node before doing so.

A per-node scheduling gate closes the cold-start race. Once the chained plugin is
installed, the installer labels its node `openshell.ai/cni-ready=true`. Each
reconcile tick fences before it repairs: the instant enforcement is not
verifiably in place it clears the label, attempts repair, and only re-marks the
node ready once the plugin is healthy again. The gateway sets a required
`nodeAffinity` on that label for every cni-sidecar sandbox pod, so a pod cannot
schedule onto a node before that node's egress enforcement is active, and a node
whose enforcement later breaks stops accepting new sandbox pods. The label is set
through a minimal cluster-scoped grant (`nodes` `get`/`patch`, plus `get` on the
installer's own DaemonSet) bound to the dedicated CNI ServiceAccount.

The persistent label cannot by itself cover a node reboot that wipes tmpfs-backed
enforcement (`multus-chain` stores the chain file under `/run`). A boot-time
`NoSchedule` taint (`openshell.ai/cni-not-ready`) **narrows** that window but does
not fully close it: operator node config applies it at boot (only node config
runs before the scheduler; a MachineConfig example ships under
`deploy/helm/openshell/examples/`), the CNI DaemonSet tolerates it, and the
installer removes it once enforcement is ready (never re-adding it, so a transient
unready does not over-repel). A small residual race remains because the taint is
applied after the kubelet starts, so the kubelet can briefly mark the node
schedulable before the taint lands; `--register-with-taints` covers the initial
join but not reboot re-registration. `conflist` mode keeps the plugin on
persistent disk and is unaffected by reboot, and is the way to avoid the window
entirely. One residual
limitation: an ungraceful DaemonSet pod deletion (no `preStop`) leaves a stale
`cni-ready=true` until the pod is rescheduled and the next reconcile tick
re-evaluates it.

The CNI installer is a **cluster singleton**. Its chained plugin enforces pods
that carry the OpenShell annotations (`openshell.ai/cni=enabled` plus the
proxy-UID and enforcement-mode annotations, set only by a gateway on its own
sandbox pods) **and** whose namespace is in the plugin's `sandboxNamespaces`
allowlist. Pods in other namespaces are passed through without a Kubernetes API
lookup, so the allowlist bounds the blast radius of the per-pod annotation read.
The allowlist is built **automatically**: each `cni-sidecar` gateway release
ships a Helm-owned marker ConfigMap (`openshell.ai/cni-registration=true`) in its
sandbox namespace, and the installer's reconcile aggregates every marker's
namespace (via `list-sandbox-namespaces`) — unioned with the optional static
`cni.sandboxNamespaces` — into the plugin config. Because the marker is a normal
Helm resource, uninstalling a release or changing its `sandboxNamespace` removes
the marker. Deregistration is **drain-gated and monotonic**: the reconcile also
unions in every namespace that still contains an OpenShell-managed sandbox pod
(`openshell.ai/managed-by=openshell`), so removing a marker does not drop
enforcement while sandboxes are still running (which would fail-open their
recreated pods) — the namespace is pruned only once it is drained (no marker and
no sandbox pods). An additional `cni.external`
release is discovered within one reconcile, with no manual allowlist edit. To
eliminate the discovery-window race, the installer publishes on each node the CSV
of namespaces it currently enforces (`openshell.ai/cni-sandbox-namespaces`
annotation, via `set-node-coverage`), and every gateway runs a `wait-coverage`
init container that blocks until each enforcement-ready node acknowledges the
gateway's namespace — so a newly-registered release does not serve sandboxes until
enforcement is confirmed cluster-wide. The installer resources use a fixed
release-independent name, the plugin config carries a fixed `openshell` owner, and
a `configVersion` (over the aggregated allowlist and config) binds readiness so a
stale-version entry is repaired before the node is re-marked ready. Install the
singleton (`cni.enabled=true`) in one release per cluster; additional releases set
`cni.enabled=false` + `cni.external=true`. To decouple node enforcement from any
gateway's lifecycle — the recommended layout for multi-gateway clusters — install
the singleton as its own release with `cni.only=true`, which renders only the
DaemonSet, its RBAC, and (on OpenShift) its SCC, and run every gateway as
`cni.external=true`; uninstalling a gateway then never strips enforcement. The
installer treats any `openshell-cni`
chained entry as its own and upgrades it in place (the conflist patch preserves
all other plugins). The `pods get` and `configmaps list` grants are cluster-scoped
so the one installer can discover marker ConfigMaps and read sandbox pods in any
allowlisted namespace.

Because one node plugin serves every gateway on the node — which may span
OpenShell versions — the CNI-to-sandbox contract is versioned. The gateway stamps
`openshell.ai/cni-contract-version` on each cni-sidecar pod, and the plugin fails
closed (refuses CNI `ADD`) when a pod requires a version it does not implement,
rather than installing rules that may not match. The plugin accepts any version
at or below its own (backward compatible within a major) and treats an absent
annotation as a pre-versioning gateway. This turns version skew into a safe
scheduling failure instead of silent under-enforcement.

On OpenShift, binary-aware network policy also requires a purpose-built
SecurityContextConstraints for sandbox pods: the network sidecar runs as UID 0
with `SYS_PTRACE` and `DAC_READ_SEARCH` to inspect cross-UID `/proc`, which
`restricted-v2` forbids. `sandboxServiceAccount.openshift.binaryAwareSCC` creates
a minimal SCC (the `restricted-v2` baseline plus only those two capabilities, UID
0, and the `image` volume type) and binds it to the sandbox ServiceAccount.
Disabling `processBinaryAwareNetworkPolicy` drops the capability requirement and
lets the stock `restricted-v2` SCC apply.

## Images

The gateway image and Helm chart are built from this repository. Sandbox images
Expand Down
41 changes: 41 additions & 0 deletions crates/openshell-cni/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
load("@crates//:defs.bzl", "aliases", "all_crate_deps")
load("@rules_rs//rs:rust_binary.bzl", "rust_binary")
load("@rules_rs//rs:rust_library.bzl", "rust_library")
load("@rules_rs//rs:rust_test.bzl", "rust_test")
load("@rules_rust//rust:defs.bzl", "rustfmt_test")

rust_library(
name = "openshell-cni",
srcs = glob(
["src/**/*.rs"],
exclude = ["src/main.rs"],
),
aliases = aliases(),
visibility = ["//visibility:public"],
deps = all_crate_deps(normal = True),
)

rust_binary(
name = "openshell-cni_bin",
srcs = ["src/main.rs"],
aliases = aliases(),
binary_name = "openshell-cni",
visibility = ["//visibility:public"],
deps = all_crate_deps(normal = True) + [":openshell-cni"],
)

rust_test(
name = "openshell-cni_test",
crate = ":openshell-cni",
deps = all_crate_deps(normal_dev = True),
)

rustfmt_test(
name = "rustfmt_test",
targets = [
":openshell-cni",
":openshell-cni_bin",
":openshell-cni_test",
],
visibility = ["//crates:__pkg__"],
)
29 changes: 29 additions & 0 deletions crates/openshell-cni/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "openshell-cni"
description = "OpenShell chained CNI plugin for Kubernetes sidecar network enforcement"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
base64 = { workspace = true }
miette = { workspace = true }
reqwest = { workspace = true, features = ["blocking"] }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yml = { workspace = true }
tempfile = "3"

[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"

[dev-dependencies]
tempfile = "3"

[lints]
workspace = true
Loading
Loading