Repository navigation
feat(gvisor): export runtime-verified container starts #1069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
matthyx
merged 2 commits into
kubescape:main
from
dakshhhhh16:gvisor-verified-start-integration
Nov 3, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| //go:build linux | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "time" | ||
|
|
||
| "github.com/kubescape/go-logger" | ||
| "github.com/kubescape/node-agent/pkg/config" | ||
| "github.com/kubescape/node-agent/pkg/gvisor" | ||
| gvcri "github.com/kubescape/node-agent/pkg/gvisor/cri" | ||
| "github.com/kubescape/node-agent/pkg/otelsetup" | ||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| coreclient "k8s.io/client-go/kubernetes/typed/core/v1" | ||
| ) | ||
|
|
||
| // startGVisorCollection is independent of the host PID/mount-namespace inventory. | ||
| // In particular it does not inject a sandbox start into an exec/profile path. | ||
| func startGVisorCollection(ctx context.Context, cfg config.Config, runtimeEndpoint string, core coreclient.CoreV1Interface, logsReady bool) (func(), error) { | ||
| if !cfg.GVisorStart.Enabled { | ||
| return func() {}, nil | ||
| } | ||
| if err := cfg.GVisorStart.Validate(); err != nil { | ||
| return nil, err | ||
| } | ||
| if !logsReady { | ||
| return nil, errors.New("gVisor observations require an initialized OTLP log exporter") | ||
| } | ||
| if cfg.GVisorStart.RuntimeEndpoint != "" { | ||
| runtimeEndpoint = cfg.GVisorStart.RuntimeEndpoint | ||
| } | ||
| inventory, err := gvcri.New(runtimeEndpoint, cfg.GVisorStart.RuntimeHandler) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| checkCtx, cancelCheck := context.WithTimeout(ctx, 2*time.Second) | ||
| err = inventory.Check(checkCtx) | ||
| cancelCheck() | ||
| if err != nil { | ||
| inventory.Close() | ||
| return nil, err | ||
| } | ||
| sourceCtx, cancel := context.WithCancel(ctx) | ||
| done := make(chan struct{}) | ||
| lookup := func(ctx context.Context, id string) (gvisor.Identity, error) { | ||
| identity, err := inventory.Resolve(ctx, id) | ||
| if err != nil { | ||
| return gvisor.Identity{}, err | ||
| } | ||
| pod, err := core.Pods(identity.Namespace).Get(ctx, identity.PodName, metav1.GetOptions{}) | ||
| if err != nil || !gVisorPodMatches(identity, pod, cfg) { | ||
| return gvisor.Identity{}, errors.New("gVisor Pod identity is unresolved or excluded") | ||
| } | ||
| return identity, nil | ||
| } | ||
| go func() { | ||
| defer close(done) | ||
| if err := gvisor.RunIntegration(sourceCtx, cfg.GVisorStart, cfg.NodeName, lookup, otelsetup.EmitRuntimeStart); err != nil { | ||
| // Never log CRI bodies or Sentry bytes. A source failure is degraded | ||
| // telemetry, not evidence that no sandbox started. | ||
| logger.L().Warning("gVisor start source stopped; runtime visibility is degraded") | ||
| } | ||
| }() | ||
| return func() { | ||
| cancel() | ||
| <-done | ||
| _ = inventory.Close() | ||
| }, nil | ||
| } | ||
|
|
||
| func gVisorPodMatches(identity gvisor.Identity, pod *corev1.Pod, cfg config.Config) bool { | ||
| if pod == nil || pod.Namespace != identity.Namespace || pod.Name != identity.PodName || | ||
| string(pod.UID) != identity.PodUID || pod.Spec.NodeName != cfg.NodeName || pod.DeletionTimestamp != nil || | ||
| cfg.IgnoreContainer(pod.Namespace, pod.Name, pod.Labels) { | ||
| return false | ||
| } | ||
| for _, c := range pod.Spec.Containers { | ||
| if c.Name == identity.ContainerName { | ||
| return true | ||
| } | ||
| } | ||
| for _, c := range pod.Spec.InitContainers { | ||
| if c.Name == identity.ContainerName { | ||
| return true | ||
| } | ||
| } | ||
| for _, c := range pod.Spec.EphemeralContainers { | ||
| if c.Name == identity.ContainerName { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| //go:build linux | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/kubescape/node-agent/pkg/config" | ||
| "github.com/kubescape/node-agent/pkg/gvisor" | ||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| func TestGVisorPodIdentityAndNamespacePolicy(t *testing.T) { | ||
| identity := gvisor.Identity{Namespace: "apps", PodName: "sandbox", PodUID: "uid", ContainerName: "app"} | ||
| for _, tc := range []string{"verified", "wrong_uid", "wrong_node", "wrong_namespace", "missing_container", "excluded_namespace", "excluded_label"} { | ||
| t.Run(tc, func(t *testing.T) { | ||
| pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "apps", Name: "sandbox", UID: "uid", Labels: map[string]string{"ignore": "yes"}}, Spec: corev1.PodSpec{NodeName: "node", Containers: []corev1.Container{{Name: "app"}}}} | ||
| cfg := config.Config{NodeName: "node"} | ||
| switch tc { | ||
| case "wrong_uid": | ||
| pod.UID = "new-uid" | ||
| case "wrong_node": | ||
| pod.Spec.NodeName = "other-node" | ||
| case "wrong_namespace": | ||
| pod.Namespace = "other" | ||
| case "missing_container": | ||
| pod.Spec.Containers = nil | ||
| case "excluded_namespace": | ||
| cfg.ExcludeNamespaces = []string{"apps"} | ||
| case "excluded_label": | ||
| cfg.ExcludeLabels = map[string][]string{"ignore": {"yes"}} | ||
| } | ||
| if got := gVisorPodMatches(identity, pod, cfg); got != (tc == "verified") { | ||
| t.Fatalf("identity/policy accepted=%v", got) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestGVisorDisabledStartupDoesNoIO(t *testing.T) { | ||
| stop, err := startGVisorCollection(context.Background(), config.Config{}, "", nil, false) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| stop() | ||
| } | ||
|
|
||
| func TestGVisorRequiresLogExport(t *testing.T) { | ||
| cfg := config.Config{GVisorStart: gvisor.IntegrationConfig{Enabled: true, SocketPath: "/run/kubescape/starts.sock"}} | ||
| if _, err := startGVisorCollection(context.Background(), cfg, "", nil, false); err == nil { | ||
| t.Fatal("enabled source silently used a no-op exporter") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
docs/experiments/gvisor-start-integration-20261102/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Node-agent gVisor start integration, 2 November 2026 | ||
|
|
||
| This is a live trial of the rebuilt Kubernetes node-agent, following the | ||
| [profile comparison in #1053](https://github.com/kubescape/node-agent/pull/1053). | ||
| The receiver, CRI/Kubernetes identity join and existing OTLP log exporter run | ||
| inside the actual node-agent process. The standalone start probe is not the | ||
| producer for this evidence. | ||
|
|
||
| ## Observed results | ||
|
|
||
| | Case | Expected observations | Received | | ||
| |---|---:|---:| | ||
| | Repeated application-container starts | 20 | 20 | | ||
| | Two overlapping gVisor workloads | 2 | 2 | | ||
| | gVisor workload in an excluded namespace | 0 | 0 | | ||
| | Valid synthetic frame claiming an unknown ID | 0 | 0 | | ||
| | Ordinary-runtime control | 0 | 0 | | ||
|
|
||
| All 24 workloads (22 accepted gVisor, one excluded gVisor and one ordinary) | ||
| completed with exit code 0. Each accepted container ID, sandbox ID, Pod UID, | ||
| Pod name/namespace, node and container name matched the independently queried | ||
| CRI/Kubernetes inventory. The concurrent workloads' execution intervals overlap. | ||
| There was exactly one retained observation per accepted container, and each | ||
| reported zero sender drops. These are bounded results, not delivery guarantees. | ||
|
|
||
| See the [verification](evidence/verification.json), | ||
| [identity ledger](evidence/runtime-ledger.json), | ||
| [received observations](evidence/observations.jsonl) and | ||
| [environment](evidence/environment.json). All records are marked | ||
| `source=gvisor_trace`, `kind=container_started`, `identity_status=verified`, | ||
| with `identity_origin=local_cri_and_kubernetes`. | ||
|
|
||
| The ordinary control produced no trace input naturally; runtime-handler | ||
| rejection is additionally covered by the CRI unit tests. The unknown-ID control | ||
| sends a valid remote-sink frame but does not establish a real runtime start. | ||
| The trial does not authenticate the sender or prove application execution from | ||
| the start record. Workload completion is separate runtime evidence. | ||
|
|
||
| ## Privacy validation | ||
|
|
||
| The fixture puts the harmless marker `GVS_INTEGRATION_CANARY_2026` in both | ||
| argv and the OCI working directory. A mounted empty directory supplies that | ||
| working directory. The marker in fixture source is intentional. | ||
|
|
||
| The collector scans the complete incoming OTLP request for the literal marker | ||
| **before** filtering retained fields. It records a violation flag without | ||
| copying the offending payload. Every retained observation has | ||
| `wire_canary_found=false` and `unexpected_attribute_count=0`. A collector | ||
| regression test injects an extra sensitive attribute and proves that filtering | ||
| cannot falsely clear the wire check. The node-agent's ordinary logs were also | ||
| scanned locally and contained no literal marker; those raw logs are not included. | ||
| This checks the literal synthetic marker and the fixed export contract, not | ||
| every possible encoding of arbitrary credentials. | ||
|
|
||
| The published run repeats an initial successful 20-plus-two trial with the | ||
| stronger pre-filter wire check. The earlier run remains in the VM for audit; | ||
| only the final run is used in the table above. | ||
|
|
||
| ## Environment | ||
|
|
||
| - Dedicated Ubuntu ARM64 Multipass VM, four vCPUs, 6 GiB RAM, kernel | ||
| 6.8.0-147-generic. | ||
| - K3s v1.37.1+k3s1, local CRI endpoint | ||
| `/run/k3s/containerd/containerd.sock`, runsc release-20261019.0 with systrap. | ||
| - Node-agent built from this PR's working tree on base `0ad6adb2` using the | ||
| repository's Go 1.27.0 toolchain and pinned dependencies, with CGO disabled. | ||
| The actual binary SHA-256 is retained in the environment summary. | ||
| - The rebuilt binary was mounted into the existing official v0.3.293 test | ||
| image and selected as its command. This is not a published image release. | ||
| - BusyBox is pinned by image-index digest in the fixture. Runtime image IDs | ||
| are recorded per workload. | ||
| - Runtime detection, application-profile learning and host sensing were | ||
| disabled for this source/export trial. The earlier eBPF comparison remains | ||
| separate evidence; no guest-syscall comparison is inferred here. | ||
|
|
||
| The test retains the earlier experimental storage/cluster setup, including | ||
| its documented root storage workaround. It is not a production chart recipe. | ||
| No managed GKE or x86_64 claim is made. | ||
|
|
||
| ## Reproduction | ||
|
|
||
| Use only the dedicated self-managed test VM and preserve the original | ||
| DaemonSet, ConfigMap and runsc runtime settings privately before changing them. | ||
| Do not export kubeconfig, service-account tokens, cloud credentials or raw OCI | ||
| specifications into a PR. | ||
|
|
||
| 1. Build `./cmd` and `./docs/experiments/gvisor-start-integration-20261102/otlp-capture` | ||
| for Linux ARM64 with Go 1.27.0. Install the test binary into the experimental | ||
| node-agent Pod using a read-only hostPath mount and command override. Keep | ||
| the existing image's gadget data and host mounts. Record the binary hash. | ||
| 2. Run the capture utility on a trusted VM-local address with a new output | ||
| file. Set `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` on node-agent to that collector. | ||
| The plaintext transport used for this isolated local experiment is not a | ||
| public collector deployment. | ||
| 3. Enable the [source configuration](../../features/gvisor-start-source.md), | ||
| with the K3s CRI endpoint, `runtimeHandler=runsc` and | ||
| `/run/kubescape/gvisor-events.sock`. Keep namespace selection limited to | ||
| `gvisor-comparison`. Create the root-owned socket directory with mode 0700. | ||
| 4. For newly created gVisor Pods only, configure runsc's pod-init configuration | ||
| with the existing `container/start` remote sink pointing to that same | ||
| socket. Do not overwrite another monitor's Default session. This experiment | ||
| owns its runtime settings and newly created sandboxes. | ||
| 5. Put the fixtures and binaries in | ||
| `/home/ubuntu/gvisor-trial/integration-20261102`. Start the collector with | ||
| `--output` pointing to `observations.jsonl` there. Wait for agent startup | ||
| and a successful version-1 seqpacket handshake, then run `trial.py --output` | ||
| with the `evidence` subdirectory as its new output path. It performs 20 | ||
| repeated starts and the unknown-ID/ordinary controls. | ||
| 6. Run `parallel-trial.py`, then `verify.py` before deleting Pods. Verification | ||
| checks every export against retained Pod and CRI identities, exact counts, | ||
| concurrent execution overlap, the pre-filter privacy flags and local agent | ||
| log scan. Checks remain active under `python -O` and `PYTHONOPTIMIZE`; | ||
| overlap and privacy verdicts are derived from the checked inputs. An error | ||
| is a failed experiment, not evidence of no starts. | ||
| 7. Export only the sanitized observation/verification files. Restore the | ||
| original ConfigMap, DaemonSet and runsc settings, remove only the experiment | ||
| workloads, allow the OTEL shutdown flush, then stop the collector and VM. | ||
| Do not delete the broader `kubescape` namespace. | ||
|
|
||
| The fixtures use fixed trial names and refuse to overwrite result directories. | ||
| Use fresh names/directories or explicitly archive and clean up a previous | ||
| experiment before repeating. Kubernetes/CRI errors and OCI specifications can | ||
| contain the marker; the fixtures do not retain those raw responses as evidence. | ||
|
|
||
| ## Validation and remaining limits | ||
|
|
||
| The Linux ARM64 node-agent built successfully. Focused Linux tests passed for | ||
| receiver/integration behavior, CRI verification, Kubernetes UID/node/filter | ||
| checks, opt-in config and the distinct OTEL record contract. Linux-targeted | ||
| vet passed for the changed packages. Portable CRI and OTEL tests also passed | ||
| with race detection on macOS. The capture privacy regression and the existing | ||
| 22 Python gVisor-tool tests passed. This was not the full repository suite or | ||
| a Linux race run. | ||
|
|
||
| The previously unexplained 19/20 start run is not retrospectively explained. | ||
| This source remains experimental and disabled by default. It can miss very | ||
| short-lived containers during identity rechecks, discard queued starts on | ||
| cancellation, or lose records through the bounded OTEL batch exporter. Detailed | ||
| health metrics, fair buffering, reconnect/recovery policy, overhead benchmarks, | ||
| stop semantics and network signals remain follow-up work. There is no actor or | ||
| session attribution and no claim of in-sandbox behavioral enforcement. |
6 changes: 6 additions & 0 deletions
6
docs/experiments/gvisor-start-integration-20261102/evidence-sha256.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "environment.json": "c6e016c7c689f0b3e8098817324524a416e627e9231968cb1bddc7a136f3263e", | ||
| "observations.jsonl": "c74d21a207fefc2275b08fbe4752ead692545922b93f6596efdad1c595d7ed36", | ||
| "runtime-ledger.json": "68e86ff45e6715404eeff75e871fd49438444046acea04791642c12f7382848c", | ||
| "verification.json": "cd45cf843f4dc04c4b76ac7725ee9881c867690418c1e3e38c44f3b67c08ec41" | ||
| } |
23 changes: 23 additions & 0 deletions
23
docs/experiments/gvisor-start-integration-20261102/evidence/environment.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "base_revision": "0ad6adb2", | ||
| "source": "working tree of this PR; test binary mounted into released base image", | ||
| "compiler": "go1.27.0", | ||
| "target": "linux/arm64, CGO_ENABLED=0", | ||
| "node_agent_binary_sha256": "8b3125d5acaca3ef3e45339590ab7e45d9ccdd91c0bdcb521dccef2f6cc610cb", | ||
| "base_image": "quay.io/kubescape/node-agent:v0.3.293", | ||
| "base_image_id": "sha256:0f31e6cfb343bfa2e486d59fe064c25a02c70bbc3fef4321dc31923fbe090577", | ||
| "node_agent_ready": true, | ||
| "kernel": "6.8.0-147-generic", | ||
| "k3s": "k3s version v1.37.1+k3s1 (356c0254)", | ||
| "runsc": "runsc version release-20261019.0", | ||
| "platform": "systrap", | ||
| "source_config": { | ||
| "enabled": true, | ||
| "socketPath": "/run/kubescape/gvisor-events.sock", | ||
| "runtimeEndpoint": "/run/k3s/containerd/containerd.sock", | ||
| "runtimeHandler": "runsc" | ||
| }, | ||
| "application_profiles_enabled_during_trial": false, | ||
| "namespace_allowlist": "gvisor-comparison", | ||
| "otlp_destination": "local disposable VM collector" | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.