mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-13 04:51:00 +08:00
* feat(ocsf): emit full JSON records to supervisor stderr Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(ocsf): preserve console record boundaries Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --------- Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
277 lines
16 KiB
Plaintext
277 lines
16 KiB
Plaintext
---
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
title: "Sandbox Logging"
|
|
sidebar-title: "Logging"
|
|
description: "How OpenShell logs sandbox activity using standard tracing and OCSF structured events."
|
|
keywords: "Generative AI, Cybersecurity, Logging, OCSF, Observability"
|
|
---
|
|
|
|
Every OpenShell sandbox produces a log that records network connections, process lifecycle events, filesystem policy decisions, and configuration changes. The log uses two formats depending on the type of event.
|
|
|
|
## Log Formats
|
|
|
|
### Standard tracing
|
|
|
|
Internal operational events use Rust's `tracing` framework with a conventional format:
|
|
|
|
```text
|
|
2026-04-01T03:28:39.160Z INFO openshell_sandbox: Fetching sandbox policy via gRPC
|
|
2026-04-01T03:28:39.175Z INFO openshell_sandbox: Creating OPA engine from proto policy data
|
|
```
|
|
|
|
These events cover startup plumbing, gRPC communication, and internal state transitions that are useful for debugging but do not represent security-relevant decisions.
|
|
|
|
### OCSF structured events
|
|
|
|
Network, process, filesystem, configuration, and API activity events use the [Open Cybersecurity Schema Framework (OCSF)](https://ocsf.io) format. OCSF is an open standard for normalizing security telemetry across tools and platforms. OpenShell maps sandbox events to OCSF v1.8.0 event classes.
|
|
|
|
In the log file, OCSF events appear in a shorthand format with an `OCSF` level label, designed for quick human and agent scanning:
|
|
|
|
```text
|
|
2026-04-01T04:04:13.058Z INFO openshell_sandbox: Starting sandbox
|
|
2026-04-01T04:04:13.065Z OCSF CONFIG:DISCOVERY [INFO] Server returned no policy; attempting local discovery
|
|
2026-04-01T04:04:13.074Z INFO openshell_sandbox: Creating OPA engine from proto policy data
|
|
2026-04-01T04:04:13.078Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image
|
|
2026-04-01T04:04:32.118Z OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
|
|
2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api engine:opa]
|
|
2026-04-01T04:04:32.690Z OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy]
|
|
```
|
|
|
|
The `OCSF` label at column 25 distinguishes structured events from standard `INFO` tracing at the same position. Both formats appear in the same file.
|
|
|
|
When viewed through the CLI or TUI, which receive logs through gRPC, the same distinction applies:
|
|
|
|
```text
|
|
[1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
|
|
[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy]
|
|
[1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox
|
|
```
|
|
|
|
## OCSF Event Classes
|
|
|
|
OpenShell maps sandbox events to these OCSF classes:
|
|
|
|
| Shorthand prefix | OCSF class | Class UID | What it covers |
|
|
|---|---|---|---|
|
|
| `EVENT` | Base Event | 0 | Endpointless relay, proxy, and mediation failures |
|
|
| `NET:` | Network Activity | 4001 | TCP proxy CONNECT tunnels, bypass detection, DNS failures |
|
|
| `HTTP:` | HTTP Activity | 4002 | HTTP FORWARD requests, L7 enforcement decisions |
|
|
| `SSH:` | SSH Activity | 4007 | SSH handshakes, authentication, channel operations |
|
|
| `PROC:` | Process Activity | 1007 | Process start, exit, timeout, signal failures |
|
|
| `FINDING:` | Detection Finding | 2004 | Security findings (nonce replay, proxy bypass, unsafe policy) |
|
|
| `CONFIG:` | Device Config State Change | 5019 | Policy load/reload, Landlock, TLS setup, provider and settings changes |
|
|
| `LIFECYCLE:` | Application Lifecycle | 6002 | Sandbox supervisor start, SSH server ready |
|
|
|
|
## Reading the Shorthand Format
|
|
|
|
The shorthand format follows this pattern:
|
|
|
|
```text
|
|
CLASS:ACTIVITY [SEVERITY] ACTION DETAILS [CONTEXT]
|
|
```
|
|
|
|
Base Events use `EVENT [SEVERITY] MESSAGE [CONTEXT]`. Unix socket relay and
|
|
relay-control notifications, plus proxy and mediation failures with no observed
|
|
endpoint, use `EVENT` rather than a network or lifecycle event.
|
|
|
|
### Components
|
|
|
|
**Class and activity** (`NET:OPEN`, `HTTP:GET`, `PROC:LAUNCH`) identify the OCSF event class and what happened. The class name always starts at the same column position for vertical scanning.
|
|
|
|
**Severity** indicates the OCSF severity of the event:
|
|
|
|
| Tag | Meaning | When used |
|
|
|---|---|---|
|
|
| `[INFO]` | Informational | Allowed connections, successful operations |
|
|
| `[LOW]` | Low | DNS failures, operational warnings |
|
|
| `[MED]` | Medium | Denied connections, policy violations |
|
|
| `[HIGH]` | High | Security findings (nonce replay, bypass detection) |
|
|
| `[CRIT]` | Critical | Process timeout kills |
|
|
| `[FATAL]` | Fatal | Unrecoverable failures |
|
|
|
|
**Action** (`ALLOWED`, `DENIED`, `BLOCKED`) is the security control disposition. Not all events have an action; informational config events, for example, do not.
|
|
|
|
**Details** vary by event class:
|
|
|
|
- Network: `process(pid) -> host:port` with the process identity and destination
|
|
- HTTP: `METHOD url` with the HTTP method and target
|
|
- SSH: peer address and authentication type
|
|
- Process: `name(pid)` with exit code or command line
|
|
- Config: description of what changed
|
|
- Finding: quoted title with the stable finding type, optional confidence, and source-specific context attributes when available
|
|
|
|
**Context** in brackets provides structured fields such as policy provenance, source-specific attributes, and denial reasons.
|
|
|
|
### Examples
|
|
|
|
An allowed HTTPS connection:
|
|
|
|
```text
|
|
OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
|
|
```
|
|
|
|
An L7 read-only policy denying a POST:
|
|
|
|
```text
|
|
OCSF HTTP:POST [MED] DENIED POST http://api.github.com/user/repos [policy:github_api engine:opa]
|
|
```
|
|
|
|
A connection denied because no policy matched:
|
|
|
|
```text
|
|
OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy]
|
|
```
|
|
|
|
A connection denied because the destination resolves to an always-blocked address:
|
|
|
|
```text
|
|
OCSF NET:OPEN [MED] DENIED /usr/bin/curl(1618) -> 169.254.169.254:80 [policy:- engine:ssrf] [reason:resolves to always-blocked address]
|
|
```
|
|
|
|
An HTTP request to a non-default port. HTTP log URLs include the port whenever it differs from the scheme default (80 for `http`, 443 for `https`):
|
|
|
|
```text
|
|
OCSF HTTP:GET [INFO] ALLOWED GET http://api.internal.corp:8080/v1/status [policy:internal_api engine:opa]
|
|
```
|
|
|
|
A supervisor middleware HTTP event records whether it transformed the request. If the middleware also emits a finding, that remains a separate event:
|
|
|
|
```text
|
|
OCSF HTTP:POST [INFO] ALLOWED POST http://httpbin.org:443/anything [policy:httpbin engine:middleware] [failed:false transformed:true]
|
|
OCSF FINDING:CREATE [MED] "configured content matched" [type:content_guard.match count:1 middleware:prototype-content-guard]
|
|
```
|
|
|
|
WebSocket middleware emits one safe event per preflight, session-start, or client text-message decision. The event includes the policy-local config, registered implementation, sequence, byte counts, transformation flag, and validated reason code. It never includes the message payload or service-provided free-form reason:
|
|
|
|
```text
|
|
OCSF NET:OTHER [INFO] WEBSOCKET_MIDDLEWARE allow config=api-redactor implementation=openshell/regex sequence=3 input_bytes=128 replacement_bytes=96 transformed=true reason_code=-
|
|
```
|
|
|
|
Coverage events are separate from invocation decisions. `binding_not_selected` means a host-matched attachment did not advertise the WebSocket operation. `unsupported_message_type` means an active WebSocket stage encountered a binary message, which V1 passes through without inspection. Both are informational, do not apply `on_error`, and never claim the traffic was inspected:
|
|
|
|
```text
|
|
OCSF NET:OTHER [INFO] WEBSOCKET_MIDDLEWARE_COVERAGE state=binding_not_selected config=http-dlp implementation=example/http-dlp sequence=- message_type=- input_bytes=0
|
|
OCSF NET:OTHER [INFO] WEBSOCKET_MIDDLEWARE_COVERAGE state=unsupported_message_type config=api-redactor implementation=openshell/regex sequence=4 message_type=binary input_bytes=512
|
|
```
|
|
|
|
A fail-open stream error emits both a middleware failure and `openshell.middleware.websocket_stage_disabled`. The latter records that OpenShell will bypass that stage for later messages on the same connection. Waiting for saturated admission capacity also emits a detection finding without payload content. When both active capacity and the bounded wait queue are full, HTTP work is rejected before its payload is buffered, returns `503 Service Unavailable`, and emits `openshell.middleware.admission_exhausted`.
|
|
|
|
Proxy and SSH servers ready:
|
|
|
|
```text
|
|
OCSF NET:LISTEN [INFO] 10.200.0.1:3128
|
|
OCSF SSH:LISTEN [INFO]
|
|
```
|
|
|
|
An SSH connection accepted (one event per invocation, arriving over the supervisor's Unix socket, so there is no network peer address to log):
|
|
|
|
```text
|
|
OCSF SSH:OPEN [INFO] ALLOWED
|
|
```
|
|
|
|
A process launched inside the sandbox:
|
|
|
|
```text
|
|
OCSF PROC:LAUNCH [INFO] sleep(49)
|
|
```
|
|
|
|
A policy reload after a settings change:
|
|
|
|
```text
|
|
OCSF CONFIG:DETECTED [INFO] Settings poll: config change detected [old_revision:2915564174587774909 new_revision:11008534403127604466 policy_changed:true]
|
|
OCSF CONFIG:LOADED [INFO] Policy reloaded successfully [policy_hash:0cc0c2b525573c07]
|
|
```
|
|
|
|
## Denial Reasons
|
|
|
|
Denied `NET:` and `HTTP:` events carry a `[reason:...]` suffix that surfaces the decision detail from the event's `status_detail` field. The reason helps distinguish between policy misses, SSRF hardening, and L7 enforcement without inspecting the full OCSF JSONL record.
|
|
|
|
For supervisor middleware denials, `status_detail` contains a platform-owned reason derived from the policy-local middleware config name and optional validated reason code. Middleware failure details also use platform-owned error codes. OpenShell does not copy per-request service text or WebSocket message content into logs.
|
|
|
|
Common reason phrases emitted by the sandbox include:
|
|
|
|
| Reason | Meaning |
|
|
|---|---|
|
|
| `no matching policy` | OPA evaluated the request and no allow rule matched. |
|
|
| `resolves to always-blocked address` | An outbound policy endpoint resolved to loopback, link-local, or unspecified. Network policy cannot authorize these destinations, even when they appear in `allowed_ips`; sandbox-local loopback connections do not use this policy path. |
|
|
| `resolves to <ip> which is not in allowed_ips, connection rejected` | The destination resolved to an IP outside the policy's `allowed_ips` allowlist. |
|
|
| `DNS resolution failed for <host>:<port>` | The proxy could not resolve the destination. |
|
|
| `port <n> is a blocked control-plane port, connection rejected` | The destination port matches a control-plane port (etcd, Kubernetes API, kubelet) and is always blocked. |
|
|
| `request-target contains an encoded '/' (%2F)` | The L7 HTTP parser rejected an encoded slash. Configure `allow_encoded_slash: true` on a REST endpoint when the upstream requires encoded slashes. |
|
|
| `l7 deny` | An L7 policy rule denied the request. |
|
|
|
|
Invalid `allowed_ips` entries and entries that overlap always-blocked ranges are rejected at policy-load time, so they never reach the runtime denial path. The phrases above come from the proxy's per-CONNECT `allowed_ips` and SSRF checks, not from policy validation.
|
|
|
|
## Proxy Error Responses
|
|
|
|
When the HTTP CONNECT proxy denies a request or cannot reach the upstream, it returns an HTTP error response with a JSON body. Clients can parse the body to surface actionable failure details instead of treating the status code alone.
|
|
|
|
A denied CONNECT returns `403 Forbidden`:
|
|
|
|
```json
|
|
{
|
|
"error": "policy_denied",
|
|
"detail": "CONNECT api.example.com:443 not permitted by policy",
|
|
"reason": "binary '/usr/bin/node' not allowed in policy 'allow_api' (ancestors: [/usr/local/bin/claude])"
|
|
}
|
|
```
|
|
|
|
The `reason` field is included when the policy engine provides a specific denial reason (for example, which binary or rule caused the rejection). It is omitted when no additional detail is available.
|
|
|
|
An upstream that the proxy cannot reach returns `502 Bad Gateway`:
|
|
|
|
```json
|
|
{
|
|
"error": "upstream_unreachable",
|
|
"detail": "connection to api.example.com:443 failed"
|
|
}
|
|
```
|
|
|
|
The `error` field is a short machine-readable code (`policy_denied`, `middleware_denied`, `middleware_failed`, `ssrf_denied`, `upstream_unreachable`, `unsupported_l7_protocol`). The `detail` field is a human-readable explanation suitable for display in an agent transcript. The optional `reason` field, when present, provides the specific denial cause from the policy engine (for example, which binary was not allowed or which rule was missing).
|
|
|
|
For L7 REST policy denials, the body also includes structured policy fields such as `method`, `path`, `rule_missing`, and `next_steps`. When the policy advisor is enabled, the body also includes `agent_guidance`, a short plain-language instruction telling the agent to read `/etc/openshell/skills/policy_advisor.md`, propose the narrowest rule through `http://policy.local/v1/proposals`, wait for `policy_reloaded: true`, and retry. A middleware denial instead identifies the policy-local config in `middleware` and can include a validated `reason_code`. A fail-closed runtime failure uses `middleware_failed` with platform-owned text. Both middleware responses omit `rule_missing`, `next_steps`, and `agent_guidance` because no policy rule is missing.
|
|
|
|
## Filesystem Sandbox Logs
|
|
|
|
Landlock filesystem restrictions emit `CONFIG:` events at startup and whenever the sandbox has to skip a requested path.
|
|
|
|
These events originate in the sandbox runtime and appear in [workload container logs](/observability/accessing-logs#main-process-output) when the runtime's diagnostic log filter permits them. They are not included in the supervisor's JSONL file or pushed to the gateway log stream.
|
|
|
|
On startup, the probe reports the kernel's supported Landlock ABI version alongside the requested path counts:
|
|
|
|
```text
|
|
OCSF CONFIG:ENABLED [INFO] Landlock filesystem sandbox available [abi:v2 compat:BestEffort ro:4 rw:2]
|
|
OCSF CONFIG:ENABLED [INFO] Applying Landlock filesystem sandbox [abi:V2 compat:BestEffort ro:4 rw:2]
|
|
OCSF CONFIG:ENABLED [INFO] Landlock ruleset built [rules_applied:5 skipped:1]
|
|
```
|
|
|
|
When `landlock.compatibility` is `best_effort` and a requested path fails to open for reasons other than `NotFound` (for example, permission denied or a symlink loop), the sandbox continues without that path and emits a `[MED]` event so the degradation is not silent:
|
|
|
|
```text
|
|
OCSF CONFIG:OTHER [MED] Skipping inaccessible Landlock path (best-effort) [path:/opt/data error:Permission denied (os error 13)]
|
|
```
|
|
|
|
Set `landlock.compatibility` to `hard_requirement` in the policy to make these failures fatal instead of degraded.
|
|
|
|
## Log File Location
|
|
|
|
The supervisor writes logs to `/var/log/` in its own filesystem, separate from the sandbox workload:
|
|
|
|
| File | Format | Rotation |
|
|
|---|---|---|
|
|
| `openshell.YYYY-MM-DD.log` | Shorthand + standard tracing | Daily |
|
|
| `openshell-ocsf.YYYY-MM-DD.log` | OCSF JSONL when enabled | Daily |
|
|
|
|
Each supervisor file appender is configured with a three-file limit. The shorthand appender's filename prefix also matches JSONL files, so its cleanup can delete JSONL history on filesystems that expose file creation timestamps. Do not rely on independent three-file retention for each format.
|
|
|
|
File output requires a writable `/var/log` and uses non-blocking writers that can drop events under pressure. If file logging cannot start, the supervisor falls back to stderr-only output. Enabling `ocsf_json_enabled` adds full `OCSF-JSON` records to stderr even without a file sink; see [OCSF JSON collection](/observability/ocsf-json-export#output-location).
|
|
|
|
Docker and Podman use tmpfs for this directory; Kubernetes uses an `emptyDir` in the supervisor pod. Collect logs externally before rotation or teardown for durable retention. See [Accessing Logs](/observability/accessing-logs#direct-filesystem-access) for driver-specific access and storage lifetimes.
|
|
|
|
## Next Steps
|
|
|
|
- [Access logs](/observability/accessing-logs) through the CLI, TUI, or supervisor resources.
|
|
- [Enable OCSF JSON export](/observability/ocsf-json-export) for SIEM integration and compliance.
|
|
- Learn about [network policies](/how-it-works/policies/overview) that generate these events.
|