Skip to content

Commit 84752d9

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

22 files changed

Lines changed: 144 additions & 170 deletions

File tree

architecture/security-policy.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,11 @@ 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`; every other value, including the
247+
removed `terminate` and `passthrough` spellings, is rejected. `protocol`
248+
remains a string so the supported protocol set can evolve, but every ingress
249+
validates it before persistence or activation.
247250
The supervisor also refuses unknown enum numbers and protocol values
248251
defensively; an unrecognized enforcement value never falls back to audit.
249252

crates/openshell-cli/tests/provider_commands_integration.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4863,7 +4863,6 @@ endpoints:
48634863
- host: api.advanced.example
48644864
ports: [443, 8443]
48654865
protocol: rest
4866-
tls: terminate
48674866
enforcement: enforce
48684867
rules:
48694868
- allow:

crates/openshell-policy/src/l7_validate.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,17 @@ 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")).then(|| {
73+
format!("unknown tls value '{tls}'; omit the field to keep automatic TLS termination")
74+
})
75+
}
76+
7177
pub fn validate_endpoint_mode_values(tls: i32, enforcement: i32, access: i32) -> Vec<String> {
7278
let mut errors = Vec::new();
73-
if network_tls_mode_to_str(tls).is_none() {
74-
errors.push(format!("unknown tls enum value {tls}"));
79+
match network_tls_mode_to_str(tls) {
80+
Some(value) => errors.extend(unknown_tls_value(value)),
81+
None => errors.push(format!("unknown tls enum value {tls}")),
7582
}
7683
if network_enforcement_mode_to_str(enforcement).is_none() {
7784
errors.push(format!("unknown enforcement enum value {enforcement}"));
@@ -174,7 +181,7 @@ mod agent_transport_tests {
174181
#[test]
175182
fn agent_cannot_request_native_tcp_or_skip_tls_inspection() {
176183
assert!(agent_authored_transport_rejection("tcp", "").is_some());
177-
assert!(agent_authored_transport_rejection("TCP", "terminate").is_some());
184+
assert!(agent_authored_transport_rejection("TCP", "").is_some());
178185
assert!(agent_authored_transport_rejection("", "skip").is_some());
179186
assert!(agent_authored_transport_rejection("rest", "SKIP").is_some());
180187
}
@@ -185,11 +192,7 @@ mod agent_transport_tests {
185192
pub fn validate_endpoint_modes(tls: &str, enforcement: &str, access: &str) -> Vec<String> {
186193
let mut errors = Vec::new();
187194

188-
if !matches!(tls, "" | "skip" | "terminate" | "passthrough") {
189-
errors.push(format!(
190-
"unknown tls value '{tls}' (expected skip, terminate, or passthrough)"
191-
));
192-
}
195+
errors.extend(unknown_tls_value(tls));
193196
if !matches!(enforcement, "" | "enforce" | "audit") {
194197
errors.push(format!(
195198
"unknown enforcement value '{enforcement}' (expected enforce or audit)"
@@ -357,9 +360,18 @@ mod tests {
357360
assert!(errors[2].contains("unknown access value 'read-wirte'"));
358361
}
359362

363+
#[test]
364+
fn endpoint_mode_values_reject_removed_tls_enums() {
365+
for legacy in [2, 3] {
366+
let errors = validate_endpoint_mode_values(legacy, 0, 0);
367+
assert_eq!(errors.len(), 1, "tls: {legacy}");
368+
assert!(errors[0].contains("unknown tls value"));
369+
}
370+
}
371+
360372
#[test]
361373
fn endpoint_modes_accept_documented_values_and_defaults() {
362-
for tls in ["", "skip", "terminate", "passthrough"] {
374+
for tls in ["", "skip"] {
363375
for enforcement in ["", "enforce", "audit"] {
364376
for access in ["", "read-only", "read-write", "full"] {
365377
assert!(validate_endpoint_modes(tls, enforcement, access).is_empty());

crates/openshell-policy/src/merge.rs

Lines changed: 1 addition & 25 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,14 +3776,6 @@ 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));
3802-
38033779
explicit_defaults.tls = NetworkTlsMode::Skip as i32;
38043780
let skip_tls = rule_with_authorizations(
38053781
"proposed",

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: 0 additions & 5 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:
@@ -5563,10 +5562,6 @@ binaries:
55635562
let rest_ep = &proto.endpoints[1];
55645563
assert_eq!(rest_ep.port, 0);
55655564
assert_eq!(rest_ep.ports, vec![443, 8443]);
5566-
assert_eq!(
5567-
rest_ep.tls,
5568-
openshell_core::proto::NetworkTlsMode::Terminate as i32
5569-
);
55705565
assert_eq!(rest_ep.allowed_ips, vec!["10.0.0.0/24"]);
55715566
assert!(rest_ep.allow_encoded_slash);
55725567
assert!(rest_ep.allow_uninspected_credentials);

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10019,14 +10019,12 @@ mod tests {
1001910019
}
1002010020
}
1002110021

10022-
#[allow(deprecated)]
1002310022
fn l7_scope_policy() -> ProtoSandboxPolicy {
1002410023
let endpoint = NetworkEndpoint {
1002510024
host: "api.example.com".to_string(),
1002610025
port: 443,
1002710026
ports: vec![443, 8443],
1002810027
protocol: "rest".to_string(),
10029-
tls: openshell_core::proto::NetworkTlsMode::Terminate as i32,
1003010028
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
1003110029
..Default::default()
1003210030
};
@@ -10982,7 +10980,6 @@ mod tests {
1098210980
let mut policy = test_policy_with_rule("aws", host);
1098310981
let endpoint = &mut policy.network_policies.get_mut("aws").unwrap().endpoints[0];
1098410982
endpoint.protocol = "rest".to_string();
10985-
endpoint.tls = 2;
1098610983
endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32;
1098710984
endpoint.credential_signing = "sigv4".to_string();
1098810985
endpoint.signing_service = "s3".to_string();
@@ -13047,7 +13044,6 @@ mod tests {
1304713044
.endpoints[0];
1304813045
bound_endpoint.protocol = "rest".to_string();
1304913046
bound_endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32;
13050-
bound_endpoint.tls = 2;
1305113047
openshell_policy::ensure_sandbox_process_identity(&mut policy);
1305213048
state
1305313049
.store

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

Lines changed: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -307,32 +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::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
312-
.severity(openshell_ocsf::SeverityId::Medium)
313-
.status(openshell_ocsf::StatusId::Success)
314-
.state(openshell_ocsf::StateId::Other, "deprecated")
315-
.message(
316-
"'tls: terminate' is deprecated; TLS termination is now automatic. \
317-
Use 'tls: skip' to explicitly disable. This field will be removed in a future version.",
318-
)
319-
.build();
320-
openshell_ocsf::ocsf_emit!(event);
321-
TlsMode::Auto
322-
}
323-
"passthrough" => {
324-
let event = openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
325-
.severity(openshell_ocsf::SeverityId::Medium)
326-
.status(openshell_ocsf::StatusId::Success)
327-
.state(openshell_ocsf::StateId::Other, "deprecated")
328-
.message(
329-
"'tls: passthrough' is deprecated; TLS termination is now automatic. \
330-
Use 'tls: skip' to explicitly disable. This field will be removed in a future version.",
331-
)
332-
.build();
333-
openshell_ocsf::ocsf_emit!(event);
334-
TlsMode::Auto
335-
}
336310
"" => TlsMode::Auto,
337311
_ => unreachable!("endpoint modes were validated above"),
338312
};
@@ -474,7 +448,6 @@ pub fn endpoint_path_matches(pattern: &str, path: &str) -> bool {
474448
pub fn parse_tls_mode(val: &regorus::Value) -> TlsMode {
475449
match get_object_str(val, "tls").as_deref() {
476450
Some("skip") => TlsMode::Skip,
477-
// "terminate" and "passthrough" are deprecated aliases (logged by parse_l7_config); fall through to Auto.
478451
_ => TlsMode::Auto,
479452
}
480453
}
@@ -1277,6 +1250,12 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
12771250
);
12781251
let loc = format!("{name}.endpoints[{i}]");
12791252

1253+
errors.extend(
1254+
validate_endpoint_modes(tls, enforcement, access)
1255+
.into_iter()
1256+
.map(|reason| format!("{loc}: {reason}")),
1257+
);
1258+
12801259
if protocol == "mcp" {
12811260
if host.trim().is_empty() {
12821261
errors.push(format!(
@@ -1493,13 +1472,6 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
14931472
}
14941473
}
14951474

1496-
// Deprecated tls values: warn but don't error
1497-
if tls == "terminate" || tls == "passthrough" {
1498-
warnings.push(format!(
1499-
"{loc}: 'tls: {tls}' is deprecated; TLS termination is now automatic. Use 'tls: skip' to disable."
1500-
));
1501-
}
1502-
15031475
// tls: skip with L7 on port 443 won't work
15041476
if tls == "skip" && !protocol.is_empty() && ports.contains(&443) {
15051477
warnings.push(format!(
@@ -1514,10 +1486,6 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
15141486
));
15151487
}
15161488

1517-
// port 443 + rest + tls: skip — L7 won't work (already handled above)
1518-
// The old warning about missing `tls: terminate` is no longer needed
1519-
// because TLS termination is now automatic.
1520-
15211489
// Per-rule deny_rules validation (semantic checks handled by
15221490
// shared validator above).
15231491
if has_deny_rules {
@@ -1957,12 +1925,11 @@ mod tests {
19571925
#[test]
19581926
fn parse_l7_config_rest_enforce() {
19591927
let val = regorus::Value::from_json_str(
1960-
r#"{"protocol": "rest", "tls": "terminate", "enforcement": "enforce", "host": "api.example.com", "port": 443}"#,
1928+
r#"{"protocol": "rest", "enforcement": "enforce", "host": "api.example.com", "port": 443}"#,
19611929
)
19621930
.unwrap();
19631931
let config = parse_l7_config(&val).unwrap();
19641932
assert_eq!(config.protocol, L7Protocol::Rest);
1965-
// "terminate" is deprecated and treated as Auto.
19661933
assert_eq!(config.tls, TlsMode::Auto);
19671934
assert_eq!(config.enforcement, EnforcementMode::Enforce);
19681935
}
@@ -3425,30 +3392,37 @@ mod tests {
34253392
}
34263393

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

34543428
#[test]

0 commit comments

Comments
 (0)