Skip to content

feat(sandbox): emit OCSF File System Activity for audited paths - #5144

Open
vineethsai7 wants to merge 7 commits into
NVIDIA:mainfrom
vineethsai7:feat/4270-v1
Open

vineethsai7 wants to merge 7 commits into
NVIDIA:mainfrom
vineethsai7:feat/4270-v1

Conversation

@vineethsai7

@vineethsai7 vineethsai7 commented Nov 2, 2026 •

Copy link
Copy Markdown

Depends on #5143. The first 2 commits are from #5143; review only 155c68a..6efb317.

Summary

Add opt-in File System Activity events for opens of policy-named paths. This is a first slice of filesystem auditing: it reports an observed open attempt without claiming the kernel allowed or denied it.

Related Issue

Part of #4270. The issue is still state:triage-needed; this is a draft design and maintainer feedback is welcome.

Changes

  • Add filesystem_policy.audit_paths to authored policy, gateway protobuf, and the sandbox attach protocol. Empty lists preserve existing behavior.
  • Regenerate the Go protobuf binding and carry AuditPaths through the Go SDK policy type, converters, and fake client.
  • Match selected open, openat, and openat2 requests in the built-in sandbox boundary's existing seccomp notification broker. Bound the output to 100 matching events per second per sandbox and emit a counted drop record for suppressed matches.
  • Add OCSF File System Activity [1001], File object, schema validation, and shorthand formatting. Events include the requesting process, requested access type, and explicit path provenance.
  • Reject nonempty audit_paths in the MXC policy mapper, which has no Linux open listener.
  • Document the watch-list behavior and where its events appear. Add custom-image filesystem guidance for the /app/agent.py first-run case raised in the issue comments.

Design

The built-in boundary already receives open notifications for provider files. It reads the full requested path only when audit_paths is nonempty, matches that text against validated globs, emits an OCSF event, then lets the existing provider-file handler continue. This keeps the default prefix fast path and uses the same boundary in Docker, Podman, Kubernetes, and the MicroVM guest.

The notification arrives before the kernel completes the syscall. After SECCOMP_USER_NOTIF_FLAG_CONTINUE, the broker cannot see the subsequent Landlock result, so this slice emits Observed and leaves disposition and status unset. Reporting Allowed or Denied here would assert a result the broker does not know. The requested path is labeled syscall_argument; relative paths, directory file descriptors, and symlinks are not resolved for matching.

Alternatives considered

  • Supervisor middleware and gateway interceptors see network or control-plane traffic, not file opens.
  • A new host eBPF or fanotify monitor would need privileges and driver-specific path mapping, and would not see into the MicroVM guest on macOS.
  • Inferring a Landlock denial from the requested path or the workload's output would be unreliable. A separate kernel outcome source is needed for denial reporting.

Release Notes

Create a sandbox with absolute watch globs to log matching file open attempts. An empty list, the default, emits no file activity events. Watch-list changes require recreating the sandbox.

Example

policy.yaml:

version: 1
filesystem_policy:
  include_workdir: true
  read_only: [/usr, /etc]
  read_write: [/sandbox, /tmp]
  audit_paths:
    - /sandbox/.ssh/**
    - /sandbox/**/.env
openshell sandbox create --name audit-demo --policy ./policy.yaml -- sh
docker logs <sandbox-container>

The workload container log contains shorthand like:

FILE:OPEN [INFO] OBSERVED python3(42) /sandbox/project/.env [access:read path:requested]

The corresponding OCSF event has these fields (common metadata omitted):

{
  "class_uid": 1001,
  "activity_id": 14,
  "action_id": 3,
  "file": {"name": ".env", "path": "/sandbox/project/.env", "type_id": 0},
  "actor": {"process": {"name": "python3", "path": "/usr/bin/python3", "pid": 42}},
  "unmapped": {
    "requested_path": "/sandbox/project/.env",
    "path_source": "syscall_argument",
    "access_type": "read"
  }
}

action_id: 3 means Observed. A suppressed-event summary is a Base Event with unmapped.dropped_count and unmapped.reason: rate_limit. Runtime-originated events currently appear in workload container logs, not the supervisor JSONL file or gateway log stream.

Compatibility

audit_paths defaults to an empty list and is omitted from the JSON boundary wire form when empty. The protobuf additions use new field numbers. Existing policies and attach clients retain their previous behavior. A nonempty watch list adds full path reads to open notifications and has a per-sandbox output cap. The watch list is bound before workload launch; live changes are rejected.
The MXC driver rejects a nonempty watch list at sandbox creation instead of silently dropping it.

Review follow-up

  • Reject nonempty audit_paths in the MXC policy mapper. MXC has no Linux boundary open listener, so accepting the list would leave an enabled audit policy without file events. The mapper reports an error before sandbox creation; the regression covers both MXC mapping paths.
  • The 11 shared files now compose with feat(sandbox): emit OCSF process launch and exit events with parent pid #5143. Process identity and executable path are distinct optional fields; boundary protocol additions use separate field numbers. The first merge's two textual conflicts retained both sets of methods and tests; the later timestamp merge retained both protobuf regressions. No duplicate field, protocol message, or helper was present.
  • feat(sandbox): emit OCSF process launch and exit events with parent pid #5143's follow-up converts the public process start times to protobuf Timestamp; this branch merges that commit without duplicating the protocol fields.
  • A watched path is sampled from workload memory before syscall continuation. Another workload thread can change the argument before the kernel uses it. The policy documentation now states this limit.

Testing

  • Checks appropriate to the affected code and behavior pass: root and conformance formatting; clippy --all-targets -- -D warnings on affected Rust crates; affected Rust unit suites and live seccomp integration test; Go SDK tests and gofmt; pinned protobuf regeneration and buf lint; docs navigation and Markdown lint; Fern check (zero errors, three warnings in untouched content); locked metadata for both affected example workspaces.
  • Final post-review gate: cargo fmt --all -- --check, clippy with --all-targets -- -D warnings, and serial cargo test for all eleven affected Rust crates. The sandbox suite passed 293/293 and the server suite passed 2605/2605.
  • MXC rejection regression failed before its fix and passes now. The server time-field regression failed before feat(sandbox): emit OCSF process launch and exit events with parent pid #5143 switched to protobuf Timestamp and passes on the merged head.
  • Unit tests added/updated (policy parsing and validation, boundary compatibility, OCSF schema and round trip, live seccomp open observation, access flags, rate cap, live-update rejection, Go SDK round trip and copy isolation)
  • E2E tests added/updated (no designated driver test host in this task)

One existing socket lifecycle test failed during a parallel sandbox suite run on the shared builder; it passed when rerun alone and in the serial sandbox suite. The final serial sandbox suite passed 293/293. No driver E2E run was available under this task's host restrictions.

Follow-ups

  • Add trustworthy Landlock denial outcomes, including read, write, execute, create, and remove, with deduplication and counts. The /app/agent.py startup denial from the issue comments remains undiagnosed by an event in this slice.
  • Carry runtime file events into supervisor JSONL and gateway log streams with sandbox identity, and verify the path and actor contract in each built-in driver.
  • Consider resolved-path information and matching for relative paths and directory file descriptors while preserving the raw requested path.
  • Evaluate a kernel-produced path and outcome source for tamper resistant file access attribution.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

Manual verification

Result: PASS — On the same ARM64 stack host, upstream main rejected filesystem_policy.audit_paths and emitted no file event for a broker-mediated open. PR head accepted the watch list and emitted one OCSF File System Activity event for the watched open, with Observed action and no asserted outcome. Clearing the watch list stopped further file events.

Environment

  • Ubuntu 24.04.5 ARM64 stack host (dc-ubuntu-eh), kernel 7.0.0-1014-aws. Runs used one disposable unprivileged account with a private HOME. The host has no Docker or Podman, so this is a live seccomp-broker proof, not a driver end-to-end run. No existing service or default port was changed.
  • Baseline: fetched upstream main cc0642fa8955be9db480937193693968ada92a49. After: PR head 6efb3176d5cc7d5520c5b8fe3aac6eec7d2e7262. Both release test binaries were cross-built through dcb up-os-4270 with nice -n 15 cargo build --release --target aarch64-unknown-linux-musl -p openshell-sandbox --tests, Zig 0.14.0 as linker, and RUSTFLAGS='-C link-self-contained=no'. Main built with --locked; PR head required an unlocked build because its Cargo.lock omits the new openshell-ocsf dependency of openshell-sandbox-backend (the unlocked build added just that lock entry). Temporary probe assertions and two ARM64 test-only libc portability adjustments were removed after the run; the checked-out PR worktree is clean.

Steps

  1. In each checkout, build only the openshell-sandbox test binary with the command above and copy it to the disposable account on the stack host.
  2. Run the main test network_broker::tests::proof_5144_before_open_has_no_file_activity and the PR test network_broker::tests::watched_open_emits_observed_file_activity_and_continues, each with --exact --nocapture --test-threads=1.
  3. Each test starts the real kernel seccomp notification listener and broker, then opens a temporary file from the filtered workload launcher. The main probe tries to parse an authored audit_paths policy. The PR probe parses a policy naming the temporary watched file, opens watched and unwatched files, checks the captured OCSF event, clears the watch list, and opens the watched file again.

Before: upstream main

The file read succeeded, but main rejected the audit policy and emitted no File System Activity event:

PF5144_BEFORE policy_audit_paths=rejected open_read=ok file_activity_events=0
test result: ok. 1 passed; 0 failed

After: PR head

The watched read succeeded. Exactly one matching event had class_uid=1001, Open activity_id=14, Observed action_id=3, the requested file path, read access, a process PID, and no status or disposition. The unwatched open emitted no file event. After configuring an empty watch list, reopening the watched file emitted no new event:

PF5144_AFTER policy_audit_paths=accepted file_activity_events=1 class_uid=1001 activity_id=14 action_id=3 file_name="watched.env" path_matches_watch=true path_source="syscall_argument" access_type="read" actor_pid_present=true status_unset=true disposition_unset=true
PF5144_AFTER shorthand=FILE:OPEN [INFO] OBSERVED pr-arm-test(<pid>) <watched-path> [access:read path:requested]
PF5144_AFTER empty_audit_paths=no_new_events total_file_activity_events=1
test result: ok. 1 passed; 0 failed

Not covered

  • Sandbox creation and policy transport through gateway, supervisor, and Docker/Podman/Kubernetes/MicroVM drivers; workload-container log delivery was not observed because no container runtime is installed on this host.
  • The 100-events-per-second cap and counted drop record, other open syscall variants, MXC rejection, and Landlock outcomes were not exercised in this small live run. This event intentionally reports an observed syscall argument rather than a kernel allow/deny result.

Cleanup

The disposable account, private HOME, and staging directory were deleted. Readback found zero identity entries, proof processes, or proof listeners, and no proof HOME or staging directory. The dev and build worktrees were restored to the PR head without a commit or push.

Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
@copy-pr-bot

copy-pr-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
Signed-off-by: Vineeth Sai Narajala <vnarajal@cisco.com>
@vineethsai7

Copy link
Copy Markdown
Author

I have read the DCO document and I hereby sign the DCO.

@vineethsai7
vineethsai7 marked this pull request as ready for review November 3, 2026 04:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-11-03T04:55:49.222322Z
We couldn’t complete this review. Comment @codex review to try again.
6efb317 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant