mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
* fix(network): refuse protocol upgrades on JSON-RPC and MCP endpoints JSON-RPC and MCP rules apply to each HTTP request, but the proxy could forward a request that also carried upgrade headers. After an upstream answered 101, route selection and the forward proxy relayed the connection without inspection. Refuse any request that carries an Upgrade header on JSON-RPC-family endpoints before the L7 policy decision, in every enforcement mode. Share the check with the existing h2c refusal and call it from relay_jsonrpc as well. Record the refusal as a policy denial and answer with the unsupported_l7_protocol error, because no policy rule can allow the request. If a JSON-RPC-family endpoint still receives 101, close the connection instead of relaying raw bytes. Document the refusal and the WebSocket alternative. Signed-off-by: Shiju <shiju@nvidia.com> * docs(observability): remove duplicate protocol error definition Keep unsupported_l7_protocol in the response error-code list and retain its explanation in the policy troubleshooting table. Signed-off-by: Shiju <shiju@nvidia.com> --------- Signed-off-by: Shiju <shiju@nvidia.com>
271 lines
15 KiB
Plaintext
271 lines
15 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.
|
|
|
|
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
|
|
|
|
Inside the sandbox, logs are written to `/var/log/`:
|
|
|
|
| File | Format | Rotation |
|
|
|---|---|---|
|
|
| `openshell.YYYY-MM-DD.log` | Shorthand + standard tracing | Daily, 3 files max |
|
|
| `openshell-ocsf.YYYY-MM-DD.log` | OCSF JSONL when enabled | Daily, 3 files max |
|
|
|
|
Both files rotate daily and retain the 3 most recent files to bound disk usage.
|
|
|
|
## Next Steps
|
|
|
|
- [Access logs](/observability/accessing-logs) through the CLI, TUI, or sandbox filesystem.
|
|
- [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.
|