You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(sandbox): auto-detect TLS and terminate unconditionally for credential injection (#544)
* feat(sandbox): auto-detect TLS and terminate unconditionally for credential injection
Closes#533
The proxy now auto-detects TLS by peeking the first bytes of each
connection. When TLS is detected, it terminates unconditionally —
enabling credential injection and optional L7 inspection without
requiring explicit 'tls: terminate' in the policy.
Copy file name to clipboardExpand all lines: architecture/gateway-security.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -420,7 +420,7 @@ This section defines the primary attacker profiles, what the current design prot
420
420
421
421
Separate from the cluster mTLS infrastructure, each sandbox has an independent TLS capability for inspecting outbound HTTPS traffic. This is documented here for completeness because it involves a distinct, per-sandbox PKI.
422
422
423
-
When a sandbox policy configures `tls: terminate` on an endpoint, the sandbox proxy performs TLS man-in-the-middle inspection:
423
+
The sandbox proxy automatically detects and terminates TLS on outbound HTTPS connections by peeking the first bytes of each tunnel. This enables credential injection and L7 inspection without requiring explicit policy configuration. The proxy performs TLS man-in-the-middle inspection:
424
424
425
425
1. **Ephemeral sandbox CA**: a per-sandbox CA (`CN=OpenShell Sandbox CA, O=OpenShell`) is generated at sandbox startup. This CA is completely independent of the cluster mTLS CA.
426
426
2. **Trust injection**: the sandbox CA is written to the sandbox filesystem and injected via `NODE_EXTRA_CA_CERTS` and `SSL_CERT_FILE` so processes inside the sandbox trust it.
Copy file name to clipboardExpand all lines: architecture/policy-advisor.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -59,7 +59,7 @@ The `mechanistic_mapper` module (`crates/openshell-sandbox/src/mechanistic_mappe
59
59
- Port recognition (well-known ports like 443, 5432 get a boost)
60
60
- SSRF origin (SSRF denials get lower confidence)
61
61
6. Generates security notes for private IPs, database ports, and ephemeral port ranges
62
-
7. If L7 request samples are present, generates specific L7 rules (method + path) with `protocol: rest`and `tls: terminate` (plumbed but not yet fed data — see issue #205)
62
+
7. If L7 request samples are present, generates specific L7 rules (method + path) with `protocol: rest`(TLS termination is automatic — no `tls` field needed). Plumbed but not yet fed data — see issue #205.
63
63
64
64
The mapper runs in `flush_proposals_to_gateway` after the aggregator drains. It produces `PolicyChunk` protos that are sent alongside the raw `DenialSummary` protos to the gateway.
`ResolvedRoute` has a custom `Debug` implementation in `crates/openshell-router/src/config.rs` that redacts the `api_key` field, printing `[REDACTED]` instead of the actual value. This prevents key leakage in log output and debug traces.
878
893
879
-
### Post-decision: L7 dispatch or raw tunnel (`Allow` path)
894
+
### Post-decision: auto-TLS detection, L7 dispatch, or raw tunnel (`Allow` path)
880
895
881
-
After a CONNECT is allowed, the SSRF check passes, and the upstream TCP connection is established:
896
+
After a CONNECT is allowed, the SSRF check passes, and the upstream TCP connection is established, the proxy determines how to handle the tunnel traffic. TLS detection is automatic — the proxy peeks the first bytes of the client stream to decide.
882
897
883
898
1.**Query L7 config**: `query_l7_config()` asks the OPA engine for `matched_endpoint_config`. If the endpoint has a `protocol` field, parse it into `L7EndpointConfig`.
884
899
885
-
2.**L7 inspection** (if config present):
886
-
- Clone the OPA engine for per-tunnel evaluation (`clone_engine_for_tunnel()`)
-`TlsMode::Terminate`: MITM via `tls_terminate_client()` + `tls_connect_upstream()`, then `relay_with_inspection()`
890
-
-`TlsMode::Passthrough`: Peek first bytes on raw TCP; if `looks_like_http()` matches, run `relay_with_inspection()`; reject on protocol mismatch
900
+
2.**Check for `tls: skip`**: If the endpoint has `tls: skip`, bypass all auto-detection and relay raw bytes via `copy_bidirectional()`. This is the escape hatch for client-cert mTLS or non-standard protocols.
891
901
892
-
3.**L4-only** (no L7 config): `tokio::io::copy_bidirectional()` for a raw tunnel
902
+
3.**Peek and auto-detect**: Read up to 8 bytes from the client stream via `TcpStream::peek()`. Classify the traffic using `looks_like_tls()` (checks for TLS ClientHello record: byte 0 = `0x16`, bytes 1-2 = TLS version `0x03xx`) and `looks_like_http()` (checks for HTTP method prefix).
903
+
904
+
4.**TLS detected** (`is_tls = true`):
905
+
- Terminate TLS unconditionally via `tls_terminate_client()` + `tls_connect_upstream()`. This happens for all HTTPS endpoints, not just those with L7 config.
906
+
- If L7 config is present: clone the OPA engine (`clone_engine_for_tunnel()`), run `relay_with_inspection()` for per-request policy evaluation.
907
+
- If no L7 config: run `relay_passthrough_with_credentials()` — parses HTTP minimally to inject credentials (via `SecretResolver`) and log requests, but does not evaluate L7 OPA rules. This enables credential injection on all HTTPS endpoints without requiring `protocol` in the policy.
908
+
- If TLS state is not configured: fall back to raw `copy_bidirectional()` with a warning.
|`L7Decision`|`{ allowed, reason, matched_rule }`| Result of L7 evaluation |
@@ -943,39 +983,58 @@ Expansion happens in `expand_access_presets()` before the Rego engine loads the
943
983
**Errors** (block startup):
944
984
-`rules` and `access` both specified on same endpoint
945
985
-`protocol` specified without `rules` or `access`
946
-
-`tls: terminate` without a `protocol`
947
986
-`protocol: sql` with `enforcement: enforce` (SQL parsing not available in v1)
948
987
- Empty `rules` array (would deny all traffic)
949
988
950
989
**Warnings** (logged):
951
-
-`protocol: rest` on port 443 without `tls: terminate` (L7 rules ineffective on encrypted traffic)
990
+
-`tls: terminate` or `tls: passthrough` on any endpoint (deprecated — TLS termination is now automatic; use `tls: skip` to disable)
991
+
-`tls: skip` with L7 rules on port 443 (L7 inspection cannot work on encrypted traffic)
952
992
- Unknown HTTP method in rules
953
993
954
-
### TLS termination
994
+
### TLS termination (auto-detect)
955
995
956
996
**File:**`crates/openshell-sandbox/src/l7/tls.rs`
957
997
958
-
TLS termination enables the proxy to inspect HTTPS traffic by performing MITM decryption.
998
+
TLS termination is automatic. The proxy peeks the first bytes of every CONNECT tunnel and terminates TLS whenever a ClientHello is detected. This enables credential injection and L7 inspection on all HTTPS endpoints without requiring explicit `tls: terminate` in the policy. The `tls` field defaults to `Auto`; use `tls: skip` to opt out entirely (e.g., for client-cert mTLS to upstream).
959
999
960
1000
**Ephemeral CA lifecycle:**
961
1001
1. At sandbox startup, `SandboxCa::generate()` creates a self-signed CA (CN: "OpenShell Sandbox CA") using `rcgen`
962
1002
2. The CA cert PEM and a combined bundle (system CAs + sandbox CA) are written to `/etc/openshell-tls/`
963
1003
3. The sandbox CA cert path is set as `NODE_EXTRA_CA_CERTS` (additive for Node.js)
964
1004
4. The combined bundle is set as `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` (replaces defaults for OpenSSL, Python requests, curl)
965
1005
1006
+
**TLS auto-detection** (`looks_like_tls()`):
1007
+
- Peeks up to 8 bytes from the client stream
1008
+
- Checks for TLS ClientHello pattern: byte 0 = `0x16` (ContentType::Handshake), byte 1 = `0x03` (TLS major version), byte 2 ≤ `0x04` (minor version, covering SSL 3.0 through TLS 1.3)
1009
+
- Returns `false` for plaintext HTTP, SSH, or other binary protocols
- First request for a hostname generates a leaf cert signed by the sandbox CA via `rcgen`
969
1014
- Cache has a hard limit of 256 entries; on overflow, the entire cache is cleared (sufficient for sandbox scale)
970
1015
- Each leaf cert chain contains two certs: the leaf and the CA
971
1016
972
-
**Connection flow:**
1017
+
**Connection flow (when TLS is detected):**
973
1018
1.`tls_terminate_client()`: Accept TLS from the sandboxed client using a `ServerConfig` with the hostname-specific leaf cert. ALPN: `http/1.1`.
974
1019
2.`tls_connect_upstream()`: Connect TLS to the real upstream using a `ClientConfig` with Mozilla root CAs (`webpki_roots`). ALPN: `http/1.1`.
975
-
3. Proxy now holds plaintext on both sides and runs `relay_with_inspection()`.
1020
+
3. Proxy now holds plaintext on both sides. If L7 config is present, runs `relay_with_inspection()`. Otherwise, runs `relay_passthrough_with_credentials()` for credential injection without L7 evaluation.
976
1021
977
1022
System CA bundles are searched at well-known paths: `/etc/ssl/certs/ca-certificates.crt` (Debian/Ubuntu), `/etc/pki/tls/certs/ca-bundle.crt` (RHEL), `/etc/ssl/ca-bundle.pem` (openSUSE), `/etc/ssl/cert.pem` (Alpine/macOS).
When TLS is auto-terminated but no L7 policy (`protocol` + `access`/`rules`) is configured on the endpoint, the proxy enters a passthrough mode that still provides value: it parses HTTP requests minimally to rewrite credential placeholders (via `SecretResolver`) and logs each request for observability. This relay:
1029
+
1030
+
1. Reads each HTTP request from the client via `RestProvider::parse_request()`
1031
+
2. Logs the request method, path, host, and port at `info!()` level (tagged `"HTTP relay (credential injection)"`)
1032
+
3. Forwards the request to upstream via `relay_http_request_with_resolver()`, which rewrites headers containing `openshell:resolve:env:*` placeholders with actual provider credential values
1033
+
4. Relays the upstream response back to the client
1034
+
5. Loops for HTTP keep-alive; exits on client close or non-reusable response
1035
+
1036
+
This enables credential injection on all HTTPS endpoints automatically, without requiring the policy author to add `protocol: rest` and `access: full` just to get credentials injected.
0 commit comments