Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benches/throughput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn create_test_policies() -> Vec<Authorization> {
scope: ztunnel::rbac::RbacScope::Global,
namespace: "default".into(),
rules: rules.clone(),
dry_run: false,
});
}

Expand Down
1 change: 1 addition & 0 deletions examples/localhost.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ policies:
name: deny-9999
namespace: default
scope: Namespace
dryRun: false
services:
- name: local
namespace: default
Expand Down
4 changes: 4 additions & 0 deletions proto/authorization.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ message Authorization {
// take place.
// Rules are OR-ed.
repeated Rule rules = 5;

// Whether or not this is a dry run policy.
// Dry run policies are not enforced, but their matches are logged
bool dry_run = 6;
}

message Rule {
Expand Down
1 change: 1 addition & 0 deletions src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ mod tests {
}],
}],
}],
dry_run: false,
// ..Default::default() // intentionally don't default. we want all fields populated
};

Expand Down
1 change: 1 addition & 0 deletions src/proxy/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ mod tests {
scope: Scope::Global as i32,
namespace: auth_namespace.into(),
rules: vec![],
dry_run: false,
};
let mut auth_xds_name = String::with_capacity(1 + auth_namespace.len() + auth_name.len());
auth_xds_name.push_str(auth_namespace);
Expand Down
3 changes: 3 additions & 0 deletions src/rbac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub struct Authorization {
pub scope: RbacScope,
pub action: RbacAction,
pub rules: Vec<Vec<Vec<RbacMatch>>>,
pub dry_run: bool,
}

#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Serialize)]
Expand Down Expand Up @@ -360,6 +361,7 @@ impl TryFrom<XdsRbac> for Authorization {
scope: RbacScope::from(xds::istio::security::Scope::try_from(resource.scope)?),
action: RbacAction::from(xds::istio::security::Action::try_from(resource.action)?),
rules,
dry_run: resource.dry_run,
})
}
}
Expand Down Expand Up @@ -483,6 +485,7 @@ mod tests {
scope: RbacScope::Global,
action: RbacAction::Allow,
rules,
dry_run: false,
}
}

Expand Down
142 changes: 141 additions & 1 deletion src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ impl DemandProxyState {
let workload = wl.authorization_policies.iter();

// Aggregate all of them based on type
let (allow, deny): (Vec<_>, Vec<_>) = ns
let (all_allow, all_deny): (Vec<_>, Vec<_>) = ns
.iter()
.chain(global.iter())
.chain(workload)
Expand All @@ -562,6 +562,11 @@ impl DemandProxyState {
})
.partition(|p| p.action == rbac::RbacAction::Allow);

let (deny, deny_dry_run): (Vec<&Authorization>, Vec<&Authorization>) =
all_deny.iter().partition(|p| !p.dry_run);
let (allow, allow_dry_run): (Vec<&Authorization>, Vec<&Authorization>) =
all_allow.iter().partition(|p| !p.dry_run);

trace!(
allow = allow.len(),
deny = deny.len(),
Expand All @@ -570,6 +575,11 @@ impl DemandProxyState {

// Allow and deny logic follows https://istio.io/latest/docs/reference/config/security/authorization-policy/

for pol in deny_dry_run.iter() {
if pol.matches(conn) {
debug!(policy = pol.to_key().as_str(), "dry-run deny policy match");
}
}
// "If there are any DENY policies that match the request, deny the request."
for pol in deny.iter() {
if pol.matches(conn) {
Expand All @@ -582,6 +592,11 @@ impl DemandProxyState {
trace!(policy = pol.to_key().as_str(), "deny policy does not match");
}
}
for pol in allow_dry_run.iter() {
if pol.matches(conn) {
debug!(policy = pol.to_key().as_str(), "dry-run allow policy match");
}
}
// "If there are no ALLOW policies for the workload, allow the request."
if allow.is_empty() {
debug!("no allow policies, allow");
Expand Down Expand Up @@ -1454,6 +1469,17 @@ mod tests {
)
}

fn create_dry_run_wildcard_rbac_policy(action: rbac::RbacAction) -> rbac::Authorization {
rbac::Authorization {
action,
namespace: "ns1".into(),
name: "wildcard".into(),
rules: vec![vec![]],
scope: rbac::RbacScope::Namespace,
dry_run: true,
}
}

// test that we confirm with https://istio.io/latest/docs/reference/config/security/authorization-policy/.
// We don't test #1 as ztunnel doesn't support custom policies.
// 1. If there are any CUSTOM policies that match the request, evaluate and deny the request if the evaluation result is deny.
Expand All @@ -1466,6 +1492,15 @@ mod tests {
let mut state = ProxyState::new(None);
state.workloads.insert(Arc::new(create_workload(1)));
state.workloads.insert(Arc::new(create_workload(2)));
// Dry run policies should have no effect.
state.policies.insert(
"wildcard-allow".into(),
create_dry_run_wildcard_rbac_policy(rbac::RbacAction::Allow),
);
state.policies.insert(
"wildcard-deny".into(),
create_dry_run_wildcard_rbac_policy(rbac::RbacAction::Deny),
);
state.policies.insert(
"allow".into(),
rbac::Authorization {
Expand All @@ -1485,6 +1520,7 @@ mod tests {
],
],
scope: rbac::RbacScope::Namespace,
dry_run: false,
},
);
state.policies.insert(
Expand All @@ -1506,6 +1542,7 @@ mod tests {
],
],
scope: rbac::RbacScope::Namespace,
dry_run: false,
},
);

Expand Down Expand Up @@ -1567,6 +1604,109 @@ mod tests {
assert!(mock_proxy_state.assert_rbac(&ctx).await.is_ok());
}

#[tokio::test]
async fn assert_rbac_dry_run_with_real_policies() {
initialize_telemetry();
crate::telemetry::set_level(true, "debug").ok();

let mut state = ProxyState::new(None);
state.workloads.insert(Arc::new(create_workload(1)));

// Real deny policy that matches denyacct
state.policies.insert(
"real-deny".into(),
rbac::Authorization {
action: rbac::RbacAction::Deny,
namespace: "ns1".into(),
name: "real-deny".into(),
rules: vec![vec![vec![rbac::RbacMatch {
principals: vec![StringMatch::Exact(
"cluster.local/ns/default/sa/denyacct".into(),
)],
..Default::default()
}]]],
scope: rbac::RbacScope::Namespace,
dry_run: false,
},
);

// Dry-run deny policy that matches both defaultacct and denyacct
state.policies.insert(
"dry-run-deny".into(),
rbac::Authorization {
action: rbac::RbacAction::Deny,
namespace: "ns1".into(),
name: "dry-run-deny".into(),
rules: vec![
vec![vec![rbac::RbacMatch {
principals: vec![StringMatch::Exact(
"cluster.local/ns/default/sa/defaultacct".into(),
)],
..Default::default()
}]],
vec![vec![rbac::RbacMatch {
principals: vec![StringMatch::Exact(
"cluster.local/ns/default/sa/denyacct".into(),
)],
..Default::default()
}]],
],
scope: rbac::RbacScope::Namespace,
dry_run: true,
},
);

// Real allow policy that matches defaultacct
state.policies.insert(
"real-allow".into(),
rbac::Authorization {
action: rbac::RbacAction::Allow,
namespace: "ns1".into(),
name: "real-allow".into(),
rules: vec![vec![vec![rbac::RbacMatch {
principals: vec![StringMatch::Exact(
"cluster.local/ns/default/sa/defaultacct".into(),
)],
..Default::default()
}]]],
scope: rbac::RbacScope::Namespace,
dry_run: false,
},
);

// Dry-run allow policy that matches defaultacct
state.policies.insert(
"dry-run-allow".into(),
rbac::Authorization {
action: rbac::RbacAction::Allow,
namespace: "ns1".into(),
name: "dry-run-allow".into(),
rules: vec![vec![vec![rbac::RbacMatch {
principals: vec![StringMatch::Exact(
"cluster.local/ns/default/sa/defaultacct".into(),
)],
..Default::default()
}]]],
scope: rbac::RbacScope::Namespace,
dry_run: true,
},
);

let mock_proxy_state = create_state(state);

let ctx = get_rbac_context(&mock_proxy_state, 1, "defaultacct");
assert!(mock_proxy_state.assert_rbac(&ctx).await.is_ok());

crate::telemetry::testing::assert_contains(std::collections::HashMap::from([
("policy", "ns1/dry-run-deny"),
("message", "dry-run deny policy match"),
]));
crate::telemetry::testing::assert_contains(std::collections::HashMap::from([
("policy", "ns1/dry-run-allow"),
("message", "dry-run allow policy match"),
]));
}

#[tokio::test]
async fn test_load_balance() {
initialize_telemetry();
Expand Down
1 change: 1 addition & 0 deletions src/state/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ mod tests {
namespaces: vec![StringMatch::Exact("whatever".into())],
..Default::default()
}]]],
dry_run: false,
};
let policy_key = policy.to_key();
// insert this namespace-scoped policy into policystore then assert it is
Expand Down
1 change: 1 addition & 0 deletions src/xds/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ mod tests {
}],
}],
}],
dry_run: false,
};
ProtoResource {
name: format!("foo{i}"),
Expand Down
1 change: 1 addition & 0 deletions tests/namespaced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ mod namespaced {
)],
..Default::default()
}]]],
dry_run: false,
})
.await?;
let _ = manager
Expand Down