Files
Tony Luo 40194f935e fix(network): fail closed when credential placeholders cannot be rewritten (#2162)
* fix(network): fail closed when credential placeholders cannot be rewritten

When the credential rewriter degrades internally, the proxy forwarded the
literal `openshell:resolve:env:<NAME>` placeholder (or its provider alias
marker) to the upstream instead of the resolved secret, leaking the reserved
token on the wire and causing upstream auth failures (#2161).

Two fail-open paths are closed:

- secrets: `rewrite_http_header_block` returned the header block verbatim when
  no `SecretResolver` was available, so the fail-closed marker scan (which ran
  only on the resolved path) never saw the placeholder. It now scans the
  header region for reserved markers even with no resolver and returns
  `UnresolvedPlaceholderError` when one is present. Marker-free traffic still
  passes through unchanged.

- proxy: when TLS was detected on a CONNECT but `tls_state` was `None`
  (ephemeral CA generation or CA file write failed at startup), the handler
  fell back to a raw `copy_bidirectional` tunnel, bypassing credential
  rewrite. Inside the proxy handler `tls_state` is `None` only on CA-init
  failure (`mode != Proxy` never starts the handler, and `tls: skip` is
  handled earlier), so it now refuses the connection with a 503 and a
  High-severity denial event instead of tunneling. The two startup CA-failure
  logs are raised from Medium to High.

Tests: resolver=None with a placeholder in the request line, a header value,
and the provider-alias form now fail closed; marker-free passthrough is
unchanged; the relay integration test asserts the request is rejected before
any byte reaches upstream; and the 503 fail-closed response contract is
locked.

Signed-off-by: Tony Luo <xialuo@nvidia.com>

* fix(network): refuse CONNECT before 200 when TLS termination is unavailable

The fail-closed refusal for a terminating CONNECT with no TLS termination
state (ephemeral CA init failed) was written after the 200 Connection
Established response. Because a CONNECT client only sends its TLS
ClientHello after reading the 200, the peek-based TLS detection is
inherently post-200, so the 503 landed inside the established tunnel and
surfaced to the client as a TLS protocol error rather than a readable
status. An 'allowed CONNECT' event was also logged first.

Move the decision to a pre-200 gate: query_tls_mode resolves purely from
the policy decision + host/port (no peeked bytes), so the route's TLS
treatment is known before the tunnel is acknowledged. When TLS state is
absent and the route is not tls: skip, write the 503 as the first bytes
on the socket, emit the High-severity Denied event, and close. tls: skip
routes tunnel raw exactly as before, and no allowed-CONNECT event is
emitted on the refusal path.

The now-unreachable post-200 branch is kept as defense in depth but no
longer writes an in-tunnel 503; it fails closed by dropping the
connection instead.

Add connection-level regression tests over a real loopback socket: the
gate refuses with HTTP/1.1 503 as the first bytes for a terminating
route, and writes nothing when TLS termination is present or the route is
tls: skip.

Signed-off-by: Tony Luo <xialuo@nvidia.com>

* fix(network): order the CONNECT TLS-unavailable refusal after SSRF

Addresses the gator re-check on #2162.

Ordering: the pre-200 fail-closed refusal ran before SSRF/allowed_ips
validation, so during CA-init failure an internal-address CONNECT got a
503 tls_termination_unavailable instead of the normal 403 ssrf_denied,
weakening operator visibility in degraded state. The SSRF branches now
return validated addresses; the refusal runs after that validation (an
internal address has already been denied with 403) but still before the
upstream connect and before 200 Connection Established. effective_tls_skip
is still resolved up front since the refusal consumes it.

Tests: add connection-level regressions through the real
handle_tcp_connection, driving a CONNECT from a child /bin/bash copy so
the /proc process-identity binding resolves it against a permissive
policy (the hot-swap test's identity pattern). They assert the first
bytes are HTTP/1.1 503 for a terminating route with no TLS state, a 403
(not 503) for an internal address, and no refusal for a tls: skip route.
These are gated to Linux at runtime (evaluate_opa_tcp needs /proc); a
companion test verifies the OPA policy shape (glob allow, tls mode) on
every platform so the precondition is locked where /proc is unavailable.

rest.rs: tighten the fail-closed relay test to assert the forwarded
buffer is_empty() rather than merely lacking the placeholder/secret.

Signed-off-by: Tony Luo <xialuo@nvidia.com>

* test(network): keep the CONNECT handler test client fork-free

The handler regression tests forked cat to read the proxy reply, so the
client socket fd was inherited by a second process with a different
binary. The identity resolver correctly denies that as ambiguous
shared-socket ownership (the same invariant
resolve_process_identity_denies_fork_exec_shared_socket_ambiguity pins),
so the tests exercised the deny path instead of the allow path — and on
busy CI runners the deny-path /proc fallback scan exceeded the test
budget and looked like a hang. The client script now uses only bash
builtins (exec, printf, read -d '') so exactly one process owns the
socket, and the child is left to exit on EOF instead of being killed
mid-read.

Signed-off-by: Tony Luo <xialuo@nvidia.com>

* test(network): drive the CONNECT handler tests with an in-process client

The child-process client (even fork-free) made the handler tests
environment-sensitive: on CI runners with a busy or restricted /proc,
resolving the child's socket ownership degraded into the whole-/proc
fallback scan and a deny, which surfaced as a hang. The client is now an
in-process TcpStream and the test policy allows current_exe(), so
identity resolution binds the socket to the test process itself in the
descendant scan — the same in-process pattern the passing
resolve_process_identity tests rely on.

The tls: skip test additionally asserts that the handler emitted no
DenialEvent at any stage, so it can no longer pass vacuously on a
policy or identity deny. Refusal budgets widened to 30s as a belt for
slow runners; the refusals themselves return in milliseconds.

Signed-off-by: Tony Luo <xialuo@nvidia.com>

* test(core): pin the percent-encoded marker no-resolver fail-closed path

The no-resolver scan already catches the percent-encoded canonical
marker through its decoded pass; this regression pins it: a request
line carrying openshell%3Aresolve%3Aenv%3AKEY with no resolver must
fail closed with UnresolvedPlaceholderError { location: header }.

Signed-off-by: Tony Luo <xialuo@nvidia.com>

---------

Signed-off-by: Tony Luo <xialuo@nvidia.com>
2026-07-11 09:40:36 -07:00
..

openshell-core

Shared types, constants, configuration, and helpers used across OpenShell crates.

Object Metadata

Top-level user-facing objects use a Kubernetes-style metadata convention. The metadata shape provides:

  • Stable server-generated ID.
  • Human-readable name.
  • Creation timestamp.
  • Optional labels for filtering and automation.

Code that works with object metadata should use the traits in openshell_core::metadata instead of reaching into protobuf fields directly:

use openshell_core::{ObjectId, ObjectLabels, ObjectName};

let id = sandbox.object_id();
let name = sandbox.object_name();
let labels = sandbox.object_labels();

Trait methods must tolerate missing metadata and return safe empty values rather than panicking.

Label Rules

Labels follow Kubernetes-style key and value conventions:

  • Keys may include an optional DNS-prefix followed by /.
  • Names are limited to alphanumeric characters plus -, _, and ..
  • Values use the same character set and may be empty.
  • Selectors use comma-separated key=value pairs with AND semantics.

Validate labels at API ingress before persisting objects.

Inference Profiles

Provider inference profiles live in this crate so the gateway, sandbox, and router agree on provider defaults. Profiles define:

  • Auth header style.
  • Default upstream headers.
  • Client-supplied passthrough headers.
  • Supported inference protocol shapes.

Do not duplicate provider-specific inference behavior in callers. Add shared behavior here, then consume it from the gateway, sandbox, and router.