Skip to content

Commit 44043ca

Browse files
committed
fix(policy)!: reject removed tls endpoint values
Signed-off-by: Yuedong Wu <dwcn22@outlook.com>
1 parent 29e89a2 commit 44043ca

26 files changed

Lines changed: 140 additions & 171 deletions

File tree

architecture/security-policy.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,12 @@ higher specificity rank deterministically overrides broader request-processing
242242
metadata. Equally specific overlapping endpoints must agree.
243243

244244
Endpoint `tls`, `enforcement`, and `access` use protobuf enums and retain their
245-
named YAML spellings. `protocol` remains a string so the supported protocol set
246-
can evolve, but every ingress validates it before persistence or activation.
245+
named YAML spellings. `tls` admits only an omitted value, meaning auto-detect
246+
and terminate for inspection, or `skip`; the deprecated `terminate` and
247+
`passthrough` enum members stay representable so a stored policy carrying one
248+
can be named in a diagnostic, but neither validates. `protocol` remains a
249+
string so the supported protocol set can evolve, but every ingress validates it
250+
before persistence or activation.
247251
The supervisor also refuses unknown enum numbers and protocol values
248252
defensively; an unrecognized enforcement value never falls back to audit.
249253

crates/openshell-cli/tests/provider_commands_integration.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4523,7 +4523,6 @@ endpoints:
45234523
- host: api.advanced.example
45244524
ports: [443, 8443]
45254525
protocol: rest
4526-
tls: terminate
45274526
enforcement: enforce
45284527
rules:
45294528
- allow:

crates/openshell-policy/src/l7_validate.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,16 @@ pub fn network_access_preset_to_str(value: i32) -> Option<&'static str> {
6868
}
6969
}
7070

71+
fn unknown_tls_value(tls: &str) -> Option<String> {
72+
(!matches!(tls, "" | "skip"))
73+
.then(|| format!("unknown tls value '{tls}' (expected skip or omitted)"))
74+
}
75+
7176
pub fn validate_endpoint_mode_values(tls: i32, enforcement: i32, access: i32) -> Vec<String> {
7277
let mut errors = Vec::new();
73-
if network_tls_mode_to_str(tls).is_none() {
74-
errors.push(format!("unknown tls enum value {tls}"));
78+
match network_tls_mode_to_str(tls) {
79+
Some(value) => errors.extend(unknown_tls_value(value)),
80+
None => errors.push(format!("unknown tls enum value {tls}")),
7581
}
7682
if network_enforcement_mode_to_str(enforcement).is_none() {
7783
errors.push(format!("unknown enforcement enum value {enforcement}"));
@@ -174,7 +180,7 @@ mod agent_transport_tests {
174180
#[test]
175181
fn agent_cannot_request_native_tcp_or_skip_tls_inspection() {
176182
assert!(agent_authored_transport_rejection("tcp", "").is_some());
177-
assert!(agent_authored_transport_rejection("TCP", "terminate").is_some());
183+
assert!(agent_authored_transport_rejection("TCP", "").is_some());
178184
assert!(agent_authored_transport_rejection("", "skip").is_some());
179185
assert!(agent_authored_transport_rejection("rest", "SKIP").is_some());
180186
}
@@ -185,11 +191,7 @@ mod agent_transport_tests {
185191
pub fn validate_endpoint_modes(tls: &str, enforcement: &str, access: &str) -> Vec<String> {
186192
let mut errors = Vec::new();
187193

188-
if !matches!(tls, "" | "skip" | "terminate" | "passthrough") {
189-
errors.push(format!(
190-
"unknown tls value '{tls}' (expected skip, terminate, or passthrough)"
191-
));
192-
}
194+
errors.extend(unknown_tls_value(tls));
193195
if !matches!(enforcement, "" | "enforce" | "audit") {
194196
errors.push(format!(
195197
"unknown enforcement value '{enforcement}' (expected enforce or audit)"
@@ -357,9 +359,26 @@ mod tests {
357359
assert!(errors[2].contains("unknown access value 'read-wirte'"));
358360
}
359361

362+
#[test]
363+
#[allow(deprecated)]
364+
fn endpoint_modes_reject_removed_tls_spellings() {
365+
for (spelling, value) in [
366+
("terminate", NetworkTlsMode::Terminate),
367+
("passthrough", NetworkTlsMode::Passthrough),
368+
] {
369+
let errors = validate_endpoint_modes(spelling, "", "");
370+
assert_eq!(errors.len(), 1, "{spelling}: {errors:?}");
371+
assert!(errors[0].contains(&format!("unknown tls value '{spelling}'")));
372+
373+
let errors = validate_endpoint_mode_values(value as i32, 0, 0);
374+
assert_eq!(errors.len(), 1, "{spelling}: {errors:?}");
375+
assert!(errors[0].contains(&format!("unknown tls value '{spelling}'")));
376+
}
377+
}
378+
360379
#[test]
361380
fn endpoint_modes_accept_documented_values_and_defaults() {
362-
for tls in ["", "skip", "terminate", "passthrough"] {
381+
for tls in ["", "skip"] {
363382
for enforcement in ["", "enforce", "audit"] {
364383
for access in ["", "read-only", "read-write", "full"] {
365384
assert!(validate_endpoint_modes(tls, enforcement, access).is_empty());

crates/openshell-policy/src/merge.rs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -833,9 +833,7 @@ fn endpoint_attributes_cover(loaded: &NetworkEndpoint, proposed: &NetworkEndpoin
833833
if !proposed.protocol.is_empty() && !protocols_match(&loaded.protocol, &proposed.protocol) {
834834
return false;
835835
}
836-
if proposed.tls != NetworkTlsMode::Unspecified as i32
837-
&& effective_tls(loaded.tls) != effective_tls(proposed.tls)
838-
{
836+
if proposed.tls != NetworkTlsMode::Unspecified as i32 && loaded.tls != proposed.tls {
839837
return false;
840838
}
841839
if proposed.enforcement != NetworkEnforcementMode::Unspecified as i32
@@ -940,19 +938,6 @@ fn protocols_match(left: &str, right: &str) -> bool {
940938
}
941939
}
942940

943-
#[allow(deprecated)]
944-
fn effective_tls(value: i32) -> i32 {
945-
match value {
946-
value
947-
if value == NetworkTlsMode::Terminate as i32
948-
|| value == NetworkTlsMode::Passthrough as i32 =>
949-
{
950-
NetworkTlsMode::Unspecified as i32
951-
}
952-
value => value,
953-
}
954-
}
955-
956941
fn effective_enforcement(value: i32) -> i32 {
957942
if value == NetworkEnforcementMode::Unspecified as i32 {
958943
NetworkEnforcementMode::Audit as i32
@@ -3783,7 +3768,6 @@ mod tests {
37833768
assert!(!policy_covers_rule(&loaded, &different_body));
37843769

37853770
let mut explicit_defaults = loaded_endpoint;
3786-
explicit_defaults.tls = 3; // deprecated passthrough compatibility value
37873771
explicit_defaults.enforcement = NetworkEnforcementMode::Audit as i32;
37883772
let runtime_defaults = rule_with_authorizations(
37893773
"proposed",
@@ -3792,13 +3776,15 @@ mod tests {
37923776
);
37933777
assert!(policy_covers_rule(&loaded, &runtime_defaults));
37943778

3795-
explicit_defaults.tls = 2; // deprecated terminate compatibility value
3796-
let legacy_terminate = rule_with_authorizations(
3797-
"proposed",
3798-
vec![explicit_defaults.clone()],
3799-
&["/usr/bin/client"],
3800-
);
3801-
assert!(policy_covers_rule(&loaded, &legacy_terminate));
3779+
for legacy in [2, 3] {
3780+
explicit_defaults.tls = legacy;
3781+
let legacy_rule = rule_with_authorizations(
3782+
"proposed",
3783+
vec![explicit_defaults.clone()],
3784+
&["/usr/bin/client"],
3785+
);
3786+
assert!(!policy_covers_rule(&loaded, &legacy_rule), "tls: {legacy}");
3787+
}
38023788

38033789
explicit_defaults.tls = NetworkTlsMode::Skip as i32;
38043790
let skip_tls = rule_with_authorizations(

crates/openshell-prover/src/containment.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,7 @@ fn validate_supported_endpoint_extensions(
15961596
context: &str,
15971597
endpoint: &Endpoint,
15981598
) -> Result<(), UnsupportedFeature> {
1599-
if !matches!(endpoint.tls.as_str(), "" | "terminate" | "passthrough")
1599+
if !endpoint.tls.is_empty()
16001600
|| endpoint.allow_encoded_slash
16011601
|| endpoint.websocket_credential_rewrite
16021602
|| endpoint.request_body_credential_rewrite
@@ -3184,17 +3184,20 @@ network_policies:
31843184
}
31853185

31863186
#[test]
3187-
fn deprecated_tls_spelling_does_not_change_authority() {
3188-
let boundary = parse(
3189-
"version: 1\nnetwork_policies:\n n:\n endpoints:\n - host: api.example.com\n port: 443\n protocol: rest\n tls: terminate\n enforcement: enforce\n access: read-only\n binaries: [{ path: /usr/bin/curl }]\n",
3190-
);
3191-
let candidate = parse(
3192-
"version: 1\nnetwork_policies:\n n:\n endpoints:\n - host: api.example.com\n port: 443\n protocol: rest\n enforcement: enforce\n rules:\n - allow: { method: GET, path: '/v1/**' }\n binaries: [{ path: /usr/bin/curl }]\n",
3193-
);
3194-
assert!(matches!(
3195-
check_within_boundary(&boundary, &candidate, options()),
3196-
CheckResult::Within(_)
3197-
));
3187+
fn removed_tls_spelling_is_outside_the_authority_model() {
3188+
for tls in ["terminate", "passthrough"] {
3189+
let policy = parse(&format!(
3190+
"version: 1\nnetwork_policies:\n n:\n endpoints:\n - host: api.example.com\n port: 443\n protocol: rest\n tls: {tls}\n enforcement: enforce\n access: read-only\n binaries: [{{ path: /usr/bin/curl }}]\n"
3191+
));
3192+
assert!(
3193+
matches!(
3194+
check_within_boundary(&policy, &policy, options()),
3195+
CheckResult::Unsupported(ref evidence)
3196+
if evidence.reason().contains("outside the initial model")
3197+
),
3198+
"tls: {tls}"
3199+
);
3200+
}
31983201
}
31993202

32003203
#[test]

crates/openshell-providers/src/profiles.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5516,7 +5516,6 @@ endpoints:
55165516
- host: api.example.com
55175517
ports: [443, 8443]
55185518
protocol: rest
5519-
tls: terminate
55205519
enforcement: enforce
55215520
rules:
55225521
- allow:
@@ -5565,7 +5564,7 @@ binaries:
55655564
assert_eq!(rest_ep.ports, vec![443, 8443]);
55665565
assert_eq!(
55675566
rest_ep.tls,
5568-
openshell_core::proto::NetworkTlsMode::Terminate as i32
5567+
openshell_core::proto::NetworkTlsMode::Unspecified as i32
55695568
);
55705569
assert_eq!(rest_ep.allowed_ips, vec!["10.0.0.0/24"]);
55715570
assert!(rest_ep.allow_encoded_slash);

crates/openshell-server/src/grpc/policy.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10004,14 +10004,12 @@ mod tests {
1000410004
}
1000510005
}
1000610006

10007-
#[allow(deprecated)]
1000810007
fn l7_scope_policy() -> ProtoSandboxPolicy {
1000910008
let endpoint = NetworkEndpoint {
1001010009
host: "api.example.com".to_string(),
1001110010
port: 443,
1001210011
ports: vec![443, 8443],
1001310012
protocol: "rest".to_string(),
10014-
tls: openshell_core::proto::NetworkTlsMode::Terminate as i32,
1001510013
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
1001610014
..Default::default()
1001710015
};
@@ -10967,7 +10965,6 @@ mod tests {
1096710965
let mut policy = test_policy_with_rule("aws", host);
1096810966
let endpoint = &mut policy.network_policies.get_mut("aws").unwrap().endpoints[0];
1096910967
endpoint.protocol = "rest".to_string();
10970-
endpoint.tls = 2;
1097110968
endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32;
1097210969
endpoint.credential_signing = "sigv4".to_string();
1097310970
endpoint.signing_service = "s3".to_string();
@@ -13032,7 +13029,6 @@ mod tests {
1303213029
.endpoints[0];
1303313030
bound_endpoint.protocol = "rest".to_string();
1303413031
bound_endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32;
13035-
bound_endpoint.tls = 2;
1303613032
openshell_policy::ensure_sandbox_process_identity(&mut policy);
1303713033
state
1303813034
.store

crates/openshell-supervisor-network/src/l7/mod.rs

Lines changed: 37 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -307,30 +307,6 @@ pub fn parse_l7_config(val: &regorus::Value) -> Option<L7EndpointConfig> {
307307

308308
let tls = match tls_value.as_str() {
309309
"skip" => TlsMode::Skip,
310-
"terminate" => {
311-
let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
312-
.activity(openshell_ocsf::ActivityId::Other)
313-
.severity(openshell_ocsf::SeverityId::Medium)
314-
.message(
315-
"'tls: terminate' is deprecated; TLS termination is now automatic. \
316-
Use 'tls: skip' to explicitly disable. This field will be removed in a future version.",
317-
)
318-
.build();
319-
openshell_ocsf::ocsf_emit!(event);
320-
TlsMode::Auto
321-
}
322-
"passthrough" => {
323-
let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
324-
.activity(openshell_ocsf::ActivityId::Other)
325-
.severity(openshell_ocsf::SeverityId::Medium)
326-
.message(
327-
"'tls: passthrough' is deprecated; TLS termination is now automatic. \
328-
Use 'tls: skip' to explicitly disable. This field will be removed in a future version.",
329-
)
330-
.build();
331-
openshell_ocsf::ocsf_emit!(event);
332-
TlsMode::Auto
333-
}
334310
"" => TlsMode::Auto,
335311
_ => unreachable!("endpoint modes were validated above"),
336312
};
@@ -470,7 +446,6 @@ pub fn endpoint_path_matches(pattern: &str, path: &str) -> bool {
470446
pub fn parse_tls_mode(val: &regorus::Value) -> TlsMode {
471447
match get_object_str(val, "tls").as_deref() {
472448
Some("skip") => TlsMode::Skip,
473-
// "terminate" and "passthrough" are deprecated aliases (logged by parse_l7_config); fall through to Auto.
474449
_ => TlsMode::Auto,
475450
}
476451
}
@@ -1273,6 +1248,12 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
12731248
);
12741249
let loc = format!("{name}.endpoints[{i}]");
12751250

1251+
errors.extend(
1252+
validate_endpoint_modes(tls, enforcement, access)
1253+
.into_iter()
1254+
.map(|reason| format!("{loc}: {reason}")),
1255+
);
1256+
12761257
if protocol == "mcp" {
12771258
if host.trim().is_empty() {
12781259
errors.push(format!(
@@ -1489,13 +1470,6 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
14891470
}
14901471
}
14911472

1492-
// Deprecated tls values: warn but don't error
1493-
if tls == "terminate" || tls == "passthrough" {
1494-
warnings.push(format!(
1495-
"{loc}: 'tls: {tls}' is deprecated; TLS termination is now automatic. Use 'tls: skip' to disable."
1496-
));
1497-
}
1498-
14991473
// tls: skip with L7 on port 443 won't work
15001474
if tls == "skip" && !protocol.is_empty() && ports.contains(&443) {
15011475
warnings.push(format!(
@@ -1510,10 +1484,6 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
15101484
));
15111485
}
15121486

1513-
// port 443 + rest + tls: skip — L7 won't work (already handled above)
1514-
// The old warning about missing `tls: terminate` is no longer needed
1515-
// because TLS termination is now automatic.
1516-
15171487
// Per-rule deny_rules validation (semantic checks handled by
15181488
// shared validator above).
15191489
if has_deny_rules {
@@ -1953,12 +1923,11 @@ mod tests {
19531923
#[test]
19541924
fn parse_l7_config_rest_enforce() {
19551925
let val = regorus::Value::from_json_str(
1956-
r#"{"protocol": "rest", "tls": "terminate", "enforcement": "enforce", "host": "api.example.com", "port": 443}"#,
1926+
r#"{"protocol": "rest", "enforcement": "enforce", "host": "api.example.com", "port": 443}"#,
19571927
)
19581928
.unwrap();
19591929
let config = parse_l7_config(&val).unwrap();
19601930
assert_eq!(config.protocol, L7Protocol::Rest);
1961-
// "terminate" is deprecated and treated as Auto.
19621931
assert_eq!(config.tls, TlsMode::Auto);
19631932
assert_eq!(config.enforcement, EnforcementMode::Enforce);
19641933
}
@@ -3421,30 +3390,37 @@ mod tests {
34213390
}
34223391

34233392
#[test]
3424-
fn validate_tls_terminate_deprecated_warning() {
3425-
let data = serde_json::json!({
3426-
"network_policies": {
3427-
"test": {
3428-
"endpoints": [{
3429-
"host": "api.example.com",
3430-
"port": 443,
3431-
"tls": "terminate",
3432-
"protocol": "rest",
3433-
"access": "full"
3434-
}],
3435-
"binaries": []
3393+
fn validate_rejects_unknown_endpoint_modes_without_warning() {
3394+
for (field, value) in [
3395+
("tls", "terminate"),
3396+
("tls", "passthrough"),
3397+
("enforcement", "enforcee"),
3398+
("access", "read_only"),
3399+
] {
3400+
let mut endpoint = serde_json::json!({
3401+
"host": "api.example.com",
3402+
"port": 443,
3403+
"protocol": "rest",
3404+
"access": "full"
3405+
});
3406+
endpoint[field] = value.into();
3407+
let data = serde_json::json!({
3408+
"network_policies": {
3409+
"test": { "endpoints": [endpoint], "binaries": [] }
34363410
}
3437-
}
3438-
});
3439-
let (errors, warnings) = validate_l7_policies(&data);
3440-
assert!(
3441-
errors.is_empty(),
3442-
"deprecated tls should not error: {errors:?}"
3443-
);
3444-
assert!(
3445-
warnings.iter().any(|w| w.contains("deprecated")),
3446-
"should warn about deprecated tls: {warnings:?}"
3447-
);
3411+
});
3412+
3413+
let (errors, warnings) = validate_l7_policies(&data);
3414+
assert!(
3415+
errors.iter().any(|e| e.contains("test.endpoints[0]")
3416+
&& e.contains(&format!("unknown {field} value '{value}'"))),
3417+
"{field}: {value} should be rejected: {errors:?}"
3418+
);
3419+
assert!(
3420+
!warnings.iter().any(|w| w.contains("deprecated")),
3421+
"{field}: {value} should not warn: {warnings:?}"
3422+
);
3423+
}
34483424
}
34493425

34503426
#[test]

0 commit comments

Comments
 (0)