Skip to content

Commit 2798108

Browse files
authored
fix(ocsf): report https scheme for TLS-terminated requests (#4684)
* fix(ocsf): report https scheme for TLS-terminated requests Closes #4562 Signed-off-by: Eric Curtin <eric.curtin@docker.com> * fix(ocsf): use transport scheme on passthrough relay Signed-off-by: Eric Curtin <eric.curtin@docker.com> --------- Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent b01dd62 commit 2798108

8 files changed

Lines changed: 134 additions & 32 deletions

File tree

‎crates/openshell-supervisor-network/src/l7/middleware.rs‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ use crate::opa::PolicyGenerationGuard;
88
use miette::{Result, miette};
99
use openshell_ocsf::{
1010
ActionId, ActivityId, DetectionFindingBuilder, DispositionId, Endpoint, FindingInfo,
11-
HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, Url as OcsfUrl,
12-
ocsf_emit,
11+
HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, ocsf_emit,
1312
};
1413
use std::path::PathBuf;
1514
use tokio::io::{AsyncRead, AsyncWrite};
@@ -915,10 +914,7 @@ pub(super) fn middleware_events(
915914
} else {
916915
SeverityId::Medium
917916
})
918-
.http_request(HttpRequest::new(
919-
&req.action,
920-
OcsfUrl::new("http", &ctx.host, &req.target, ctx.port),
921-
))
917+
.http_request(HttpRequest::new(&req.action, ctx.ocsf_url(&req.target)))
922918
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
923919
.firewall_rule(&ctx.policy_name, "middleware")
924920
.unmapped("transformed", invocation.transformed)

‎crates/openshell-supervisor-network/src/l7/relay.rs‎

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ impl L7EvalContext {
102102
pub(crate) fn websocket_scheme(&self) -> &'static str {
103103
if self.is_plaintext() { "ws" } else { "wss" }
104104
}
105+
106+
/// OCSF URL for `target` on the inspected transport.
107+
pub(crate) fn ocsf_url(&self, target: &str) -> OcsfUrl {
108+
OcsfUrl::new(self.request_scheme(), &self.host, target, self.port)
109+
}
105110
}
106111

107112
fn request_default_port(ctx: &L7EvalContext) -> Option<u16> {
@@ -1696,10 +1701,7 @@ fn build_l7_request_event(
16961701
.action(action_id)
16971702
.disposition(disposition_id)
16981703
.severity(severity)
1699-
.http_request(HttpRequest::new(
1700-
action,
1701-
OcsfUrl::new("http", &ctx.host, redacted_target, ctx.port),
1702-
))
1704+
.http_request(HttpRequest::new(action, ctx.ocsf_url(redacted_target)))
17031705
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
17041706
.firewall_rule(&ctx.policy_name, engine_type)
17051707
.message(message)
@@ -2158,7 +2160,7 @@ where
21582160
.severity(severity)
21592161
.http_request(HttpRequest::new(
21602162
&request_info.action,
2161-
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
2163+
ctx.ocsf_url(&redacted_target),
21622164
))
21632165
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
21642166
.firewall_rule(&ctx.policy_name, "l7")
@@ -2582,7 +2584,7 @@ where
25822584
.severity(severity)
25832585
.http_request(HttpRequest::new(
25842586
&request_info.action,
2585-
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
2587+
ctx.ocsf_url(&redacted_target),
25862588
))
25872589
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
25882590
.firewall_rule(&ctx.policy_name, jsonrpc_engine_type(config.protocol))
@@ -2868,7 +2870,7 @@ where
28682870
.severity(severity)
28692871
.http_request(HttpRequest::new(
28702872
&request_info.action,
2871-
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
2873+
ctx.ocsf_url(&redacted_target),
28722874
))
28732875
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
28742876
.firewall_rule(&ctx.policy_name, "l7-graphql")
@@ -3339,7 +3341,7 @@ fn emit_transformed_body_decision(
33393341
.severity(severity)
33403342
.http_request(HttpRequest::new(
33413343
&request_info.action,
3342-
OcsfUrl::new("http", &ctx.host, &request_info.target, ctx.port),
3344+
ctx.ocsf_url(&request_info.target),
33433345
))
33443346
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
33453347
.firewall_rule(&ctx.policy_name, engine_type)
@@ -3573,7 +3575,7 @@ where
35733575
.severity(SeverityId::Informational)
35743576
.http_request(HttpRequest::new(
35753577
&req.action,
3576-
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
3578+
ctx.ocsf_url(&redacted_target),
35773579
))
35783580
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
35793581
.message(format!(
@@ -3606,7 +3608,7 @@ where
36063608
req,
36073609
client,
36083610
ctx,
3609-
"http",
3611+
ctx.request_scheme(),
36103612
openshell_supervisor_middleware::TransformedBodyPolicy::NotPolicyRelevant,
36113613
)
36123614
.await?;
@@ -3680,7 +3682,7 @@ where
36803682
let resolver = ctx.secret_resolver.as_deref();
36813683
let response_middleware = response_selection
36823684
.as_ref()
3683-
.map(|exchange| exchange.response_relay(&req_with_auth, ctx, "http"));
3685+
.map(|exchange| exchange.response_relay(&req_with_auth, ctx, ctx.request_scheme()));
36843686

36853687
// Forward request with credential rewriting and relay the response.
36863688
// relay_http_request_with_resolver handles both directions: it sends
@@ -7811,7 +7813,7 @@ network_policies:
78117813
assert_eq!(denied_json["unmapped"]["failed"], false);
78127814
assert_eq!(
78137815
denied_http.format_shorthand(),
7814-
"HTTP:POST [MED] DENIED POST http://api.example.test:443/v1/messages \
7816+
"HTTP:POST [MED] DENIED POST https://api.example.test/v1/messages \
78157817
[policy:rest_api engine:middleware] \
78167818
[failed:false transformed:false \
78177819
reason:middleware_denied:content-guard:content_match]"
@@ -7927,6 +7929,88 @@ network_policies:
79277929
.unwrap();
79287930
}
79297931

7932+
#[tokio::test]
7933+
async fn passthrough_middleware_scheme_follows_transport() {
7934+
use openshell_supervisor_middleware::{ChainRunner, MiddlewareRegistry};
7935+
7936+
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
7937+
for (port, transport, scheme) in [(8443, 443, "https"), (443, 80, "http")] {
7938+
seen.lock().unwrap().clear();
7939+
let data = format!(
7940+
r#"
7941+
network_middlewares:
7942+
scheme:
7943+
middleware: test/scheme
7944+
on_error: fail_closed
7945+
endpoints:
7946+
include: ["api.example.test"]
7947+
network_policies:
7948+
passthrough_api:
7949+
name: passthrough_api
7950+
endpoints:
7951+
- host: api.example.test
7952+
port: {port}
7953+
binaries:
7954+
- {{ path: /usr/bin/curl }}
7955+
"#
7956+
);
7957+
let engine = Arc::new(OpaEngine::from_strings(TEST_POLICY, &data).unwrap());
7958+
let registry = MiddlewareRegistry::connect_services(
7959+
vec![Arc::new(SchemeRecorder(seen.clone()))],
7960+
Vec::new(),
7961+
)
7962+
.await
7963+
.expect("connect named middleware services");
7964+
engine.set_middleware_runner_for_tests(ChainRunner::from_registry(registry));
7965+
let generation_guard = engine
7966+
.generation_guard(engine.current_generation())
7967+
.unwrap();
7968+
let ctx = L7EvalContext {
7969+
host: "api.example.test".into(),
7970+
port,
7971+
request_default_port: Some(transport),
7972+
policy_name: "passthrough_api".into(),
7973+
binary_path: "/usr/bin/curl".into(),
7974+
..Default::default()
7975+
};
7976+
7977+
let (mut app, mut relay_client) = tokio::io::duplex(8192);
7978+
let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192);
7979+
let relay = tokio::spawn(async move {
7980+
relay_passthrough_with_credentials(
7981+
&mut relay_client,
7982+
&mut relay_upstream,
7983+
&ctx,
7984+
&generation_guard,
7985+
Some(engine.as_ref()),
7986+
)
7987+
.await
7988+
});
7989+
7990+
let request = format!(
7991+
"GET /v1/messages HTTP/1.1\r\nHost: api.example.test:{port}\r\nConnection: close\r\n\r\n"
7992+
);
7993+
app.write_all(request.as_bytes()).await.unwrap();
7994+
let mut upstream_request = [0u8; 1024];
7995+
tokio::time::timeout(
7996+
std::time::Duration::from_secs(1),
7997+
upstream.read(&mut upstream_request),
7998+
)
7999+
.await
8000+
.expect("request should reach upstream")
8001+
.unwrap();
8002+
assert_eq!(*seen.lock().unwrap(), [scheme]);
8003+
8004+
drop(app);
8005+
drop(upstream);
8006+
tokio::time::timeout(std::time::Duration::from_secs(1), relay)
8007+
.await
8008+
.expect("relay should finish")
8009+
.unwrap()
8010+
.ok();
8011+
}
8012+
}
8013+
79308014
#[tokio::test]
79318015
async fn websocket_upgrade_request_is_inspected_and_denied() {
79328016
// The WebSocket upgrade handshake is an HTTP request the hook can inspect
@@ -9020,6 +9104,7 @@ network_policies:
90209104
let ctx = L7EvalContext {
90219105
host: "gateway.example.test".into(),
90229106
port: 443,
9107+
request_default_port: Some(443),
90239108
policy_name: "route_api".into(),
90249109
..Default::default()
90259110
};
@@ -9038,10 +9123,34 @@ network_policies:
90389123
assert_eq!(event.base().severity, SeverityId::Medium);
90399124
assert_eq!(
90409125
event.format_shorthand(),
9041-
"HTTP:GET [MED] DENIED GET http://gateway.example.test:443/other [policy:route_api engine:l7] [reason:L7_REQUEST deny GET gateway.example.test:443/other reason=no L7 endpoint path matched request]"
9126+
"HTTP:GET [MED] DENIED GET https://gateway.example.test/other [policy:route_api engine:l7] [reason:L7_REQUEST deny GET gateway.example.test:443/other reason=no L7 endpoint path matched request]"
90429127
);
90439128
}
90449129

9130+
#[test]
9131+
fn request_event_url_scheme_follows_transport() {
9132+
for (port, transport, scheme, url) in [
9133+
(80, 80, "http", "http://api.example.test/v1"),
9134+
(443, 443, "https", "https://api.example.test/v1"),
9135+
(8443, 443, "https", "https://api.example.test:8443/v1"),
9136+
(443, 80, "http", "http://api.example.test:443/v1"),
9137+
] {
9138+
let ctx = L7EvalContext {
9139+
host: "api.example.test".into(),
9140+
port,
9141+
request_default_port: Some(transport),
9142+
policy_name: "api".into(),
9143+
..Default::default()
9144+
};
9145+
9146+
let event = build_l7_request_event(&ctx, "GET", "/v1", "allow", "l7", "ok", None);
9147+
9148+
let json = event.to_json().unwrap();
9149+
assert_eq!(json["http_request"]["url"]["scheme"], scheme);
9150+
assert!(event.format_shorthand().contains(url), "{url}");
9151+
}
9152+
}
9153+
90459154
#[tokio::test]
90469155
async fn route_selected_unmatched_path_emits_denied_policy_activity() {
90479156
let engine = OpaEngine::from_strings(TEST_POLICY, ENCODED_SLASH_SCOPING_POLICY).unwrap();

‎crates/openshell-supervisor-network/src/l7/token_grant_injection.rs‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use openshell_core::token_grant_failure::{
2222
};
2323
use openshell_ocsf::{
2424
ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId,
25-
StatusId, Url as OcsfUrl, ctx::ctx as ocsf_ctx, ocsf_emit,
25+
StatusId, ctx::ctx as ocsf_ctx, ocsf_emit,
2626
};
2727
use tracing::warn;
2828

@@ -345,10 +345,7 @@ fn report_refused_grant(
345345
.disposition(DispositionId::Blocked)
346346
.severity(SeverityId::Medium)
347347
.status(StatusId::Failure)
348-
.http_request(HttpRequest::new(
349-
method,
350-
OcsfUrl::new("http", &ctx.host, request_path, ctx.port),
351-
))
348+
.http_request(HttpRequest::new(method, ctx.ocsf_url(request_path)))
352349
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
353350
.message(format!(
354351
"Token grant failed for {credential} to {}:{}",
@@ -407,7 +404,7 @@ async fn inject_selected(
407404
.severity(SeverityId::Informational)
408405
.http_request(HttpRequest::new(
409406
&req.action,
410-
OcsfUrl::new("http", &ctx.host, request_path(&req.target), ctx.port),
407+
ctx.ocsf_url(request_path(&req.target)),
411408
))
412409
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
413410
.message(format!(

‎docs/observability/accessing-logs.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The CLI receives logs from the gateway over gRPC. Each line includes a timestamp
2020

2121
```text
2222
[1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
23-
[1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api]
23+
[1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET https://api.github.com/zen [policy:github_api]
2424
[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa]
2525
[1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox
2626
```

‎docs/observability/logging.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ In the log file, OCSF events appear in a shorthand format with an `OCSF` level l
3434
2026-04-01T04:04:13.074Z INFO openshell_sandbox: Creating OPA engine from proto policy data
3535
2026-04-01T04:04:13.078Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image
3636
2026-04-01T04:04:32.118Z OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
37-
2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api engine:opa]
37+
2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET https://api.github.com/zen [policy:github_api engine:opa]
3838
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]
3939
```
4040

@@ -122,7 +122,7 @@ OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:git
122122
An L7 read-only policy denying a POST:
123123

124124
```text
125-
OCSF HTTP:POST [MED] DENIED POST http://api.github.com/user/repos [policy:github_api engine:opa]
125+
OCSF HTTP:POST [MED] DENIED POST https://api.github.com/user/repos [policy:github_api engine:opa]
126126
```
127127

128128
A connection denied because no policy matched:
@@ -146,7 +146,7 @@ OCSF HTTP:GET [INFO] ALLOWED GET http://api.internal.corp:8080/v1/status [policy
146146
A supervisor middleware HTTP event records whether it transformed the request. If the middleware also emits a finding, that remains a separate event:
147147

148148
```text
149-
OCSF HTTP:POST [INFO] ALLOWED POST http://httpbin.org:443/anything [policy:httpbin engine:middleware] [failed:false transformed:true]
149+
OCSF HTTP:POST [INFO] ALLOWED POST https://httpbin.org/anything [policy:httpbin engine:middleware] [failed:false transformed:true]
150150
OCSF FINDING:CREATE [MED] "configured content matched" [type:content_guard.match count:1 middleware:prototype-content-guard]
151151
```
152152

‎docs/tutorials/first-network-policy.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ openshell logs demo --since 5m --source sandbox
174174
```
175175

176176
```text
177-
[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]
177+
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST https://api.github.com/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]
178178
```
179179

180180
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. Refer to [Logging](/observability/logging) for the event format.

‎docs/tutorials/github-push-access.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ openshell logs github-demo --since 5m --source sandbox
114114
You should see a denial for a request resembling this one:
115115

116116
```text
117-
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://github.com:443/<org>/<repo>.git/git-receive-pack [policy:_provider_my_github engine:l7] [reason:L7_REQUEST deny POST github.com:443/<org>/<repo>.git/git-receive-pack reason=POST /<org>/<repo>.git/git-receive-pack not permitted by policy]
117+
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST https://github.com/<org>/<repo>.git/git-receive-pack [policy:_provider_my_github engine:l7] [reason:L7_REQUEST deny POST github.com:443/<org>/<repo>.git/git-receive-pack reason=POST /<org>/<repo>.git/git-receive-pack not permitted by policy]
118118
```
119119

120120
`_provider_my_github` is the rule that OpenShell composes from the attached

‎examples/sandbox-policy-quickstart/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ openshell logs demo --since 5m --source sandbox
192192
```
193193

194194
```text
195-
[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]
195+
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST https://api.github.com/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]
196196
```
197197

198198
The log captures the exact HTTP method, path, and matching rule. Policy

0 commit comments

Comments
 (0)