diff --git a/README.md b/README.md index fd9b894ec7..b1a5caf4b7 100644 --- a/README.md +++ b/README.md @@ -73,19 +73,20 @@ Attach the providers and policy required by that workload. ### See network policy in action -Every sandbox starts with **minimal outbound access**. You open additional access with a short YAML policy that the proxy enforces at the HTTP method and path level, without restarting anything. +Every sandbox starts with **minimal outbound access**. You open additional access with a network rule that the proxy enforces at the HTTP method and path level, without restarting anything. ```bash # 1. Create a sandbox (starts with minimal outbound access) -openshell sandbox create +openshell sandbox create --name demo # 2. Inside the sandbox — blocked sandbox$ curl -sS https://api.github.com/zen curl: (56) Received HTTP code 403 from proxy after CONNECT -# 3. Back on the host — apply a read-only GitHub API policy +# 3. Back on the host — add a read-only GitHub API rule sandbox$ exit -openshell policy set demo --policy examples/sandbox-policy-quickstart/policy.yaml --wait +openshell policy update demo --rule-name github_api --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce --wait # 4. Reconnect — GET allowed, POST blocked by L7 openshell sandbox connect demo diff --git a/examples/sandbox-policy-quickstart/README.md b/examples/sandbox-policy-quickstart/README.md index dd19bc14e2..156348dddc 100644 --- a/examples/sandbox-policy-quickstart/README.md +++ b/examples/sandbox-policy-quickstart/README.md @@ -14,7 +14,7 @@ while writes are blocked — all without restarting anything. | File | Description | | ------------- | -------------------------------------------------------------------- | -| `policy.yaml` | L7 read-only policy for the GitHub REST API, scoped to `curl` | +| `policy.yaml` | Complete policy with the same rule, for `sandbox create --policy` | | `demo.sh` | Automated script that runs the full walkthrough non-interactively | ## Walkthrough @@ -58,42 +58,42 @@ exit ### 3. Check the deny log ```bash -openshell logs demo --since 5m +openshell logs demo --since 5m --source sandbox ``` You'll see a line like: ```text -action=deny dst_host=api.github.com dst_port=443 binary=/usr/bin/curl deny_reason="no matching network policy" +[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> api.github.com:443 [policy:- engine:opa] [reason:network connections not allowed by policy] ``` Every denied connection is logged with the destination, the binary that attempted it, and the reason. Nothing gets out silently. -### 4. Apply the read-only GitHub API policy - -Review the policy: +### 4. Add a read-only GitHub API rule ```bash -cat examples/sandbox-policy-quickstart/policy.yaml +openshell policy update demo \ + --rule-name github_api \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ + --wait ``` -```yaml -version: 1 - -# Default sandbox filesystem settings. -# These filesystem fields are required when using `openshell policy set` -# because it replaces the entire policy. -filesystem_policy: - include_workdir: true - read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] - read_write: [/sandbox, /tmp, /dev/null] -landlock: - compatibility: best_effort +The endpoint specification lists the host, port, access preset, protocol, +and enforcement mode. **curl may make GET, HEAD, and OPTIONS requests to +`api.github.com` over HTTPS. Everything else is denied.** `rest` tells the +proxy to terminate TLS and inspect each HTTP request, `read-only` permits +`GET`, `HEAD`, and `OPTIONS`, and `enforce` blocks every other request. +`policy update` changes only the network rules and keeps the rest of the +sandbox's policy. +The command adds a rule equivalent to this YAML in the policy's +`network_policies` section: + +```yaml network_policies: github_api: - name: github-api-readonly endpoints: - host: api.github.com port: 443 @@ -101,29 +101,18 @@ network_policies: enforcement: enforce access: read-only binaries: - - { path: /usr/bin/curl } + - path: /usr/bin/curl ``` -The top section preserves the default sandbox filesystem and Landlock -settings while omitting process identity so the active compute driver can -select it. These settings are required because `policy set` replaces the -entire policy. -The `network_policies` section is the interesting part: **curl may make -GET, HEAD, and OPTIONS requests to `api.github.com` over HTTPS. -Everything else is denied.** The proxy auto-detects and terminates TLS -to inspect each HTTP request and enforce the `read-only` access preset -at the method level. - -Apply it: +`--wait` blocks until the sandbox reports a result for the new policy +revision. No restart required — network rules reload while the sandbox runs. -```bash -openshell policy set demo \ - --policy examples/sandbox-policy-quickstart/policy.yaml \ - --wait -``` - -`--wait` blocks until the sandbox confirms the new policy is loaded. -No restart required — policies are hot-reloaded. +[`policy.yaml`](policy.yaml) contains the same rule in a complete policy. Use +it to start a new sandbox with the rule in place: +`openshell sandbox create --name demo --policy examples/sandbox-policy-quickstart/policy.yaml`. +Do not apply it to a running sandbox with `openshell policy set`, which +replaces the entire policy and is rejected if the file drops a filesystem path +the sandbox already has. ### 5. Connect and verify: GET works @@ -178,8 +167,11 @@ curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \ -d '{"title":"oops"}' ``` -```json -{"error":"policy_denied","policy":"github-api-readonly","detail":"POST /repos/octocat/hello-world/issues not permitted by policy"} +The proxy returns a `403` response with a JSON body that includes fields +like these: + +```text +{...,"error":"policy_denied",...,"policy":"github_api",...,"rule":"POST /repos/octocat/hello-world/issues",...} ``` The CONNECT request succeeded (api.github.com is allowed), but the L7 @@ -196,16 +188,17 @@ exit ### 7. Check the L7 deny log ```bash -openshell logs demo --level warn --since 5m +openshell logs demo --since 5m --source sandbox ``` ```text -l7_decision=deny dst_host=api.github.com l7_action=POST l7_target=/repos/octocat/hello-world/issues l7_deny_reason="POST /repos/octocat/hello-world/issues not permitted by policy" +[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://api.github.com:443/repos/octocat/hello-world/issues [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/repos/octocat/hello-world/issues reason=POST /repos/octocat/hello-world/issues not permitted by policy] ``` -The log captures the exact HTTP method, path, and deny reason. In -production, pipe these logs to your SIEM for a complete audit trail of -every request your agent makes. +The log captures the exact HTTP method, path, and matching rule. Policy +events are INFO-level log records regardless of their severity, so do not +filter them out with `--level warn`. In production, export these events to +your SIEM for a complete audit trail of every request your agent makes. ### 8. Clean up diff --git a/examples/sandbox-policy-quickstart/demo.sh b/examples/sandbox-policy-quickstart/demo.sh index ae368c06b6..8cc8b2145a 100755 --- a/examples/sandbox-policy-quickstart/demo.sh +++ b/examples/sandbox-policy-quickstart/demo.sh @@ -8,7 +8,7 @@ # Runs the full walkthrough non-interactively: # 1. Creates a sandbox with default-deny networking # 2. Attempts a request (denied) -# 3. Applies a read-only GitHub API policy +# 3. Adds a read-only GitHub API network rule # 4. Retries the request (allowed) # 5. Attempts a POST (blocked by L7) # 6. Shows logs and cleans up @@ -18,8 +18,6 @@ set -euo pipefail SANDBOX_NAME="policy-demo" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -POLICY_FILE="${SCRIPT_DIR}/policy.yaml" SSH_CONFIG=$(mktemp) cleanup() { @@ -52,17 +50,19 @@ run() { return "${PIPESTATUS[0]}" } +# Keep only OCSF network and HTTP policy decisions from `openshell logs`. +policy_events() { + grep -E '(NET|HTTP):[A-Z]+ .*(ALLOWED|DENIED)' +} + colorize_logs() { sed \ - -e "s/action=deny/$(printf '\033[1;31m')action=deny$(printf '\033[0m')/g" \ - -e "s/action=allow/$(printf '\033[1;32m')action=allow$(printf '\033[0m')/g" \ - -e "s/dst_host=[^ ]*/$(printf '\033[36m')&$(printf '\033[0m')/g" \ - -e "s/dst_port=[^ ]*/$(printf '\033[36m')&$(printf '\033[0m')/g" \ - -e "s/binary=[^ ]*/$(printf '\033[1m')&$(printf '\033[0m')/g" \ - -e "s/reason=[^\"]*/$(printf '\033[33m')&$(printf '\033[0m')/g" \ - -e "s/policy=[^ ]*/$(printf '\033[35m')&$(printf '\033[0m')/g" \ - -e "s/\[CONNECT\]/$(printf '\033[1m')[CONNECT]$(printf '\033[0m')/g" \ - -e "s/\[FORWARD\]/$(printf '\033[1m')[FORWARD]$(printf '\033[0m')/g" + -e "s/DENIED/$(printf '\033[1;31m')DENIED$(printf '\033[0m')/g" \ + -e "s/ALLOWED/$(printf '\033[1;32m')ALLOWED$(printf '\033[0m')/g" \ + -e "s/NET:[A-Z]*/$(printf '\033[1m')&$(printf '\033[0m')/g" \ + -e "s/HTTP:[A-Z]*/$(printf '\033[1m')&$(printf '\033[0m')/g" \ + -e "s/\[policy:[^]]*\]/$(printf '\033[35m')&$(printf '\033[0m')/g" \ + -e "s/\[reason:[^]]*\]/$(printf '\033[33m')&$(printf '\033[0m')/g" } sandbox_exec() { @@ -109,18 +109,19 @@ printf " ${RED}✗ Blocked by default-deny policy.${RESET}\n" step "3/7 Checking deny log" sleep 2 -printf " ${BOLD}\$ openshell logs ${SANDBOX_NAME} --since 1m -n 10${RESET}\n" -openshell logs "$SANDBOX_NAME" --since 1m -n 10 2>&1 \ - | grep -i 'connect\|forward\|deny\|allow' \ +printf " ${BOLD}\$ openshell logs ${SANDBOX_NAME} --since 1m --source sandbox -n 10${RESET}\n" +openshell logs "$SANDBOX_NAME" --since 1m --source sandbox -n 10 2>&1 \ + | policy_events \ | colorize_logs \ | sed 's/^/ /' # ------------------------------------------------------------------ -step "4/7 Applying read-only GitHub API policy" -printf " Policy file: %s\n\n" "$POLICY_FILE" -run openshell policy set "$SANDBOX_NAME" \ - --policy "$POLICY_FILE" \ +step "4/7 Adding a read-only GitHub API rule" +run openshell policy update "$SANDBOX_NAME" \ + --rule-name github_api \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ --wait # ------------------------------------------------------------------ @@ -149,9 +150,10 @@ printf " ${YELLOW}%s${RESET}\n" "$RESPONSE" step "7/7 Checking L7 deny log" sleep 2 -printf " ${BOLD}\$ openshell logs ${SANDBOX_NAME} --level warn --since 1m -n 10${RESET}\n" -openshell logs "$SANDBOX_NAME" --level warn --since 1m -n 10 2>&1 \ - | grep -i 'connect\|forward\|deny\|allow\|l7\|rest' \ +# Policy events are INFO-level OCSF records, so a --level warn filter hides them. +printf " ${BOLD}\$ openshell logs ${SANDBOX_NAME} --since 1m --source sandbox -n 10${RESET}\n" +openshell logs "$SANDBOX_NAME" --since 1m --source sandbox -n 10 2>&1 \ + | policy_events \ | colorize_logs \ | sed 's/^/ /' @@ -162,4 +164,4 @@ printf " What you saw:\n" printf " 1. Default deny — minimal outbound access, explicit approval required\n" printf " 2. L7 read-only — GET allowed, POST blocked at the HTTP method level\n" printf " 3. Audit trail — every request logged with method, path, and decision\n\n" -printf " The policy is %s lines of YAML.\n" "$(wc -l < "$POLICY_FILE" | tr -d ' ')" +printf " The rule took one command and no restart.\n" diff --git a/examples/sandbox-policy-quickstart/policy.yaml b/examples/sandbox-policy-quickstart/policy.yaml index a17b359ebc..c332d9d8cd 100644 --- a/examples/sandbox-policy-quickstart/policy.yaml +++ b/examples/sandbox-policy-quickstart/policy.yaml @@ -3,15 +3,20 @@ # Allow curl to read from the GitHub REST API. # POST, PUT, PATCH, and DELETE are blocked by the "read-only" preset. +# +# This is a complete policy for starting a new sandbox: +# openshell sandbox create --name demo --policy policy.yaml +# To add the same rule to a running sandbox, use `openshell policy update` as +# shown in README.md. `openshell policy set` replaces the entire policy and is +# rejected if the file drops a filesystem path the running sandbox already has. version: 1 -# Default sandbox filesystem and Landlock settings. Process identity is omitted -# so the active compute driver can select it. These fields are required when -# using `openshell policy set` because it replaces the entire policy. +# Filesystem and Landlock settings that cover the restrictive default policy. +# Process identity is omitted so the active compute driver can select it. filesystem_policy: include_workdir: true - read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_only: [/bin, /usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] read_write: [/sandbox, /tmp, /dev/null] landlock: compatibility: best_effort diff --git a/providers/pypi.yaml b/providers/pypi.yaml index 78ceaef9a5..148e498ff4 100644 --- a/providers/pypi.yaml +++ b/providers/pypi.yaml @@ -9,13 +9,19 @@ # least-privilege control that decides which processes may reach the endpoints # below, so it has to name the paths in *your* image. # -# Client binaries: python, python3, pip, uv. +# Client binaries: python, python3 (which also run pip), uv. # Reference layout: the paths below assume a particular image layout — a # virtualenv at /sandbox/.venv or /app/.venv, uv at # /usr/local/bin/uv, uv-managed interpreters under # /sandbox/.uv/python. Almost every other image differs -# (/usr/bin/python3, /usr/local/bin/pip, a venv elsewhere). -# Edit `binaries` before importing or the profile is inert. +# (/usr/bin/python3, /usr/local/bin/python3, a venv +# elsewhere). Edit `binaries` before importing or the +# profile is inert. +# OpenShell identifies a process by its executable, not its +# command line, so pip and other Python scripts match the +# interpreter that runs them. List that interpreter, never +# a script path such as .venv/bin/pip. Exact paths resolve +# symlinks; globs must match the interpreter's real path. # Credential scope: none. PyPI access here is anonymous. # Endpoint access: pypi.org and files.pythonhosted.org for packages; # downloads.python.org for interpreters; github.com, @@ -44,9 +50,7 @@ endpoints: binaries: - /sandbox/.venv/bin/python - /sandbox/.venv/bin/python3 - - /sandbox/.venv/bin/pip - /app/.venv/bin/python - /app/.venv/bin/python3 - - /app/.venv/bin/pip - /usr/local/bin/uv - /sandbox/.uv/python/** diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 98a21f162e..76ab015b69 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -35,7 +35,7 @@ Examples: - "Let /usr/bin/myapp talk to internal-svc:8080 but only for reading" This is sufficient for: -- **L4-only** policies (allow all traffic to host:port, no HTTP inspection) +- **L4-only** policies (host:port + binary checks, no method or path rules) - **Preset-based L7** policies (`read-only`, `read-write`, `full` on all paths) For this tier, default to: @@ -109,12 +109,12 @@ Ask these when the user's intent is broad and more specificity is possible: |-----------|--------------| | "Full access" / "allow everything" | "Do you actually need DELETE access, or would read-write (everything except DELETE) be enough?" | | "Allow access to api.example.com" (no method/path detail) | "Do you know which specific API paths or operations you need? If so, I can lock the policy down to just those. Otherwise I'll use a broad preset." | -| L4-only / "just pass it through" | "L4-only means the proxy won't inspect HTTP traffic at all — any method and path will be allowed. Are you sure you don't want at least read-only or read-write restriction?" | +| L4-only / "just pass it through" | "L4-only means the proxy applies no method or path rules — any method and path will be allowed. Are you sure you don't want at least read-only or read-write restriction?" | | Wildcard binary (`/usr/bin/*`) | "A wildcard binary pattern means any binary in that directory can use this policy. Can you narrow it to specific binaries?" | | Multiple hosts in one policy | "Do all of these hosts need the same access level? If some need tighter restrictions, I can split them into separate policies." | | `access: full` with `enforcement: audit` | "Full access in audit mode means nothing is actually restricted — all traffic flows through and violations are only logged. Is that intentional, or did you want to enforce restrictions?" | | `**` path glob on all rules | "Using `**` on all paths allows any URL path. Do you know the specific API path prefixes you need (e.g., `/api/v1/`)?" | -| Private/internal IP destination | "Does this service resolve to a private IP (10.x, 172.16.x, 192.168.x)? If so, you'll need `allowed_ips` to permit access — what CIDR range should be allowed?" | +| Private/internal IP destination | "Does this service resolve to a private IP (10.x, 172.16.x, 192.168.x)? An exact hostname can reach its private addresses without `allowed_ips`, but a wildcard or hostless endpoint needs it. Should I pin the endpoint to a specific CIDR range?" | ### Auto-Discovery of API Docs for Well-Known Services @@ -278,7 +278,8 @@ network_policies: - allow: method: path: "" - # Optional: allow private IP destinations (CIDR or exact IP) + # Optional: restrict resolved addresses (CIDR or exact IP). Required + # for private IPs on wildcard or hostless endpoints. # allowed_ips: # - "10.0.5.0/24" binaries: @@ -327,19 +328,24 @@ github_api: - { path: /usr/bin/curl } ``` -Deny rules support the same matching capabilities as allow rules: `method`, `path`, `command` (SQL), and `query` parameter matchers. When generating policies, prefer deny rules when the user needs broad access with a small set of blocked operations — it produces a shorter, more maintainable policy than enumerating 60+ allow rules. +Deny rules support the same matching capabilities as allow rules: `method`, `path`, and `query` parameter matchers. When generating policies, prefer deny rules when the user needs broad access with a small set of blocked operations — it produces a shorter, more maintainable policy than enumerating 60+ allow rules. ### Private IP Destinations -When the endpoint resolves to a private IP (RFC 1918), the proxy's SSRF protection blocks the connection by default. Use `allowed_ips` to selectively allow specific private IP ranges: +The proxy's SSRF protection treats private (RFC 1918) destinations differently depending on how the endpoint names its host: + +- **Exact hostname** (`host: api.internal.corp`) — the connection may reach the private addresses the hostname resolves to without `allowed_ips`. +- **Wildcard host, hostless endpoint, or policy-advisor proposal** — private resolved addresses are blocked unless `allowed_ips` covers them. + +Use `allowed_ips` to pin the addresses an endpoint may reach. When it is set, every resolved address must fall within the list, including public addresses: - **Host + allowlist**: `host` + `allowed_ips` — domain must resolve to an IP in the allowlist - **Hostless allowlist**: `allowed_ips` only (no `host`) — any domain on the port is allowed if it resolves to an IP in the allowlist -Loopback (`127.0.0.0/8`) and link-local (`169.254.0.0/16`) are **always blocked** regardless of `allowed_ips`. +Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), unspecified, and cloud metadata addresses are **always blocked** regardless of `allowed_ips`. ```yaml -# Example: Allow access to internal service at a known private IP range +# Example: Pin an internal service to a known private IP range internal_api: name: internal_api endpoints: @@ -375,7 +381,6 @@ Before presenting the policy to the user, verify correctness **and** flag breadt hostless, an IP literal, a trailing-dot name, or a malformed DNS selector - [ ] `tls` is either omitted or set to `skip`; no other value is accepted - [ ] `rules` list is not empty when present -- [ ] If `protocol: sql`, `enforcement` is not `enforce` - [ ] Every middleware config has a non-empty `middleware` name and non-empty `endpoints.include` - [ ] Middleware `order` values are unique and no selected chain exceeds 10 stages - [ ] No fail-closed middleware selector can cover a `tls: skip` endpoint @@ -405,7 +410,7 @@ Evaluate the generated policy for overly broad access and **include warnings in | Condition | Warning to show | |-----------|----------------| -| **L4-only** (no `protocol`, or `protocol: tcp`) | "This policy allows all application methods and paths without inspection. An omitted protocol uses explicit-proxy behavior. `protocol: tcp` enables policy DNS and transparent TCP only on a runtime that advertises the complete substrate (currently Docker and Podman); its hostname constrains connection routing, not application authority, so compatible shared infrastructure may expose other tenants or services. Consider `protocol: rest` with a preset if you want HTTP method-level or authority control." | +| **L4-only** (no `protocol`, or `protocol: tcp`) | "This policy allows all application methods and paths. An omitted protocol uses explicit-proxy behavior and applies no method or path rules. With default TLS handling, the proxy terminates detected TLS and checks the authority of the HTTP requests it parses, but other CONNECT payloads, such as HTTP/2 prior knowledge or non-HTTP protocols, can pass through a raw relay; `tls: skip` also bypasses termination and parsing. `protocol: tcp` enables policy DNS and transparent TCP only on a runtime that advertises the complete substrate (currently Docker and Podman); its hostname constrains connection routing, not application authority, so compatible shared infrastructure may expose other tenants or services. Consider `protocol: rest` with a preset if you want HTTP method-level or authority control." | | **`access: full`** | "This policy allows all HTTP methods (including DELETE) on all paths. If you don't need DELETE, `read-write` is safer. If you only need to read, `read-only` is the most restrictive option." | | **`access: full` + `enforcement: audit`** | "Full access in audit mode provides no actual restriction — all traffic flows through. This is effectively a monitoring-only policy." | | **`access: read-write`** when user hasn't confirmed write need | "This policy allows POST, PUT, and PATCH on all paths. If you only need to read data, `read-only` is more restrictive." | @@ -532,7 +537,7 @@ After presenting or applying the policy, ask if the user wants to: ## Quick Reference: Common Patterns -### L4-Only (no HTTP inspection) +### L4-Only (no method or path rules) ```yaml my_api: diff --git a/skills/generate-sandbox-policy/examples.md b/skills/generate-sandbox-policy/examples.md index f5475a6c2a..17b7ca4646 100644 --- a/skills/generate-sandbox-policy/examples.md +++ b/skills/generate-sandbox-policy/examples.md @@ -16,7 +16,7 @@ Examples organized by detail tier — from minimal (just host + intent) to full **User**: "Let claude talk to api.anthropic.com and statsig.anthropic.com, just let everything through." -No API docs needed. No L7 inspection. +No API docs needed. No method or path rules. ```yaml network_policies: @@ -29,7 +29,7 @@ network_policies: - { path: /usr/local/bin/claude } ``` -No `protocol`, `rules`, or `access` — this is pure L4 (host:port + binary identity check). +No `protocol`, `rules`, or `access` — the endpoint has no method or path rules, so any request to host:port from the listed binary is allowed. With default TLS handling, the proxy terminates detected TLS and rejects parsed HTTP requests whose authority does not match the endpoint, but other CONNECT payloads, such as HTTP/2 prior knowledge or non-HTTP protocols, can pass through a raw relay. `tls: skip` also bypasses TLS termination and HTTP parsing. --- @@ -551,7 +551,7 @@ network_policies: ### Analysis -- Anthropic API: L4-only (no inspection), standard claude binary +- Anthropic API: L4-only (no method or path rules), standard claude binary - Internal docs: L7 with read-only, HTTP so no TLS config needed - Two separate policies because different binaries @@ -579,7 +579,7 @@ network_policies: - { path: /usr/local/bin/claude } ``` -**Note**: The first policy has no `protocol` field — this means L4-only (host:port check, no HTTP inspection). The second policy has `protocol: rest` so every HTTP request is inspected. +**Note**: The first policy has no `protocol` field, so it applies no method or path rules. With default TLS handling, the proxy still terminates detected TLS and checks the authority of the HTTP requests it parses, but other CONNECT payloads can pass through a raw relay. The second policy has `protocol: rest`, so every HTTP request is also checked against the `read-only` preset. --- @@ -623,7 +623,7 @@ network_policies: **User**: "Allow curl to reach our internal API at api.internal.corp on port 8080. It resolves to 10.0.5.x addresses." -The user knows the service resolves to private IPs. Use `allowed_ips` to permit the specific subnet. +The user knows the service resolves to private IPs. An exact hostname can reach them without `allowed_ips`; add `allowed_ips` to pin connections to the known subnet. ```yaml network_policies: diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 5ae5703810..6c964a6682 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -532,7 +532,7 @@ In a separate terminal or as the agent: openshell logs dev --tail --source sandbox ``` -Look for log lines with `action: deny` -- these indicate blocked network requests. The logs include: +Look for `DENIED` log lines. `NET:OPEN [MED] DENIED` marks a blocked connection, and `HTTP: [MED] DENIED` marks a blocked request. Policy events are INFO-level log records, so do not add `--level warn`. The logs include: - **Destination host and port** (what was blocked) - **Binary path** (which process attempted the connection) @@ -541,17 +541,18 @@ Look for log lines with `action: deny` -- these indicate blocked network request ### Step 3: Pull the current policy ```bash -openshell policy get dev --full > current-policy.yaml +set -o pipefail +openshell policy get dev --base | sed '1,/^---$/d' > current-policy.yaml ``` -The `--full` flag includes the effective policy, including provider-composed entries. Use `--base` instead when the editable base policy is needed without provider-composed entries. Before resubmitting a `--full` result, review composed entries and prefer incremental updates or the base policy when appropriate. +`--base` returns the editable policy without provider-composed entries; OpenShell composes attached provider rules separately. The command prints revision details, a `---` line, and then the policy YAML. The `sed` expression keeps only the YAML, because `policy set` cannot parse the revision details. Use `--full` only to inspect the effective policy, not as input to `policy set`. ### Step 4: Modify the policy Edit `current-policy.yaml` to allow the blocked actions. **For policy content authoring, delegate to the `generate-sandbox-policy` skill.** That skill handles: - Network endpoint rule structure -- L4 vs REST, WebSocket, JSON-RPC, MCP, and SQL L7 policy decisions +- L4 vs REST, WebSocket, JSON-RPC, and MCP L7 policy decisions - Access presets (`read-only`, `read-write`, `full`) - TLS termination configuration - Enforcement modes (`audit` vs `enforce`) @@ -728,7 +729,7 @@ openshell sandbox connect work-session --editor vscode Monitor denied activity: ```bash -openshell logs work-session --tail --source sandbox --level warn +openshell logs work-session --tail --source sandbox ``` When denied actions appear: @@ -747,9 +748,11 @@ When denied actions appear: one. `--add-allow` and `--add-deny` require `--rule-name` and the complete binary scope through repeated `--binary` or explicit `--any-binary`. Declare every port on the endpoint in the operation, for example `api.example.com:443,8443:POST:/admin`. Use `--endpoint-path` to disambiguate endpoints within the selected rule; an explicitly empty path selects an endpoint without a path selector. The gateway rejects missing or mismatched scope before persistence. Inspect the current policy and confirm the intended affected scope; do not automatically fill declarations from current policy just to make a rejection pass. -2. Use full YAML replacement for broad changes or non-network fields, including - any change that would otherwise require restating a large existing scope: - `openshell policy get work-session --full > policy.yaml` +2. Use full YAML replacement for broad network changes or settings that + `policy update` cannot express, including any change that would otherwise + require restating a large existing scope. Filesystem, Landlock, and process + changes still require recreating the sandbox: + `openshell policy get work-session --base | sed '1,/^---$/d' > policy.yaml` Modify the policy with the `generate-sandbox-policy` skill. `openshell policy set work-session --policy policy.yaml --wait` 3. Verify with `openshell policy list work-session`.