blob: a5debe6ebe3a4e36fdf4ec27cc94e2e5a1d23be8 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::{capability_source::CapabilitySource, component_instance::ComponentInstanceInterface},
cm_config::{
AllowlistEntry, AllowlistMatcher, CapabilityAllowlistKey, CapabilityAllowlistSource,
DebugCapabilityKey, SecurityPolicy,
},
fuchsia_zircon_status as zx,
moniker::{ExtendedMoniker, Moniker, MonikerBase},
std::sync::Arc,
thiserror::Error,
tracing::{error, warn},
};
use cm_rust::CapabilityTypeName;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Errors returned by the PolicyChecker and the ScopedPolicyChecker.
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, Error, PartialEq)]
pub enum PolicyError {
#[error("security policy disallows \"{policy}\" job policy for \"{moniker}\"")]
JobPolicyDisallowed { policy: String, moniker: Moniker },
#[error("security policy disallows \"{policy}\" child policy for \"{moniker}\"")]
ChildPolicyDisallowed { policy: String, moniker: Moniker },
#[error("security policy was unable to extract the source from the routed capability")]
InvalidCapabilitySource,
#[error("security policy disallows \"{cap}\" from \"{source_moniker}\" being used at \"{target_moniker}\"")]
CapabilityUseDisallowed {
cap: String,
source_moniker: ExtendedMoniker,
target_moniker: Moniker,
},
#[error(
"debug security policy disallows \"{cap}\" from being registered in \
environment \"{env_name}\" at \"{env_moniker}\""
)]
DebugCapabilityUseDisallowed { cap: String, env_moniker: Moniker, env_name: String },
}
impl PolicyError {
/// Convert this error into its approximate `zx::Status` equivalent.
pub fn as_zx_status(&self) -> zx::Status {
zx::Status::ACCESS_DENIED
}
}
/// Evaluates security policy globally across the entire Model and all components.
/// This is used to enforce runtime capability routing restrictions across all
/// components to prevent high privilleged capabilities from being routed to
/// components outside of the list defined in the runtime security policy.
#[derive(Clone, Debug, Default)]
pub struct GlobalPolicyChecker {
/// The security policy to apply.
policy: Arc<SecurityPolicy>,
}
impl GlobalPolicyChecker {
/// Constructs a new PolicyChecker object configured by the SecurityPolicy.
pub fn new(policy: Arc<SecurityPolicy>) -> Self {
Self { policy }
}
fn get_policy_key<'a, C>(
capability_source: &'a CapabilitySource<C>,
) -> Result<CapabilityAllowlistKey, PolicyError>
where
C: ComponentInstanceInterface,
{
Ok(match &capability_source {
CapabilitySource::Namespace { capability, .. } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentManager,
source_name: capability
.source_name()
.ok_or(PolicyError::InvalidCapabilitySource)?
.clone(),
source: CapabilityAllowlistSource::Self_,
capability: capability.type_name(),
},
CapabilitySource::Component { capability, component } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()),
source_name: capability
.source_name()
.ok_or(PolicyError::InvalidCapabilitySource)?
.clone(),
source: CapabilityAllowlistSource::Self_,
capability: capability.type_name(),
},
CapabilitySource::Builtin { capability, .. } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentManager,
source_name: capability.source_name().clone(),
source: CapabilityAllowlistSource::Self_,
capability: capability.type_name(),
},
CapabilitySource::Framework { capability, component } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()),
source_name: capability.source_name().clone(),
source: CapabilityAllowlistSource::Framework,
capability: capability.type_name(),
},
CapabilitySource::Void { capability, component } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()),
source_name: capability.source_name().clone(),
source: CapabilityAllowlistSource::Void,
capability: capability.type_name(),
},
CapabilitySource::Capability { source_capability, component } => {
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()),
source_name: source_capability
.source_name()
.ok_or(PolicyError::InvalidCapabilitySource)?
.clone(),
source: CapabilityAllowlistSource::Capability,
capability: source_capability.type_name(),
}
}
CapabilitySource::AnonymizedAggregate { capability, component, .. }
| CapabilitySource::FilteredAggregate { capability, component, .. } => {
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()),
source_name: capability.source_name().clone(),
source: CapabilityAllowlistSource::Self_,
capability: capability.type_name(),
}
}
CapabilitySource::Environment { capability, .. } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentManager,
source_name: capability
.source_name()
.ok_or(PolicyError::InvalidCapabilitySource)?
.clone(),
source: CapabilityAllowlistSource::Environment,
capability: capability.type_name(),
},
})
}
/// Returns Ok(()) if the provided capability source can be routed to the
/// given target_moniker, else a descriptive PolicyError.
pub fn can_route_capability<'a, C>(
&self,
capability_source: &'a CapabilitySource<C>,
target_moniker: &'a Moniker,
) -> Result<(), PolicyError>
where
C: ComponentInstanceInterface,
{
let policy_key = Self::get_policy_key(capability_source).map_err(|e| {
error!("Security policy could not generate a policy key for `{}`", capability_source);
e
})?;
match self.policy.capability_policy.get(&policy_key) {
Some(entries) => {
let parts = target_moniker
.path()
.clone()
.into_iter()
.map(|c| AllowlistMatcher::Exact(c))
.collect();
let entry = AllowlistEntry { matchers: parts };
// Use the HashSet to find any exact matches quickly.
if entries.contains(&entry) {
return Ok(());
}
// Otherwise linear search for any non-exact matches.
if entries.iter().any(|entry| entry.matches(&target_moniker)) {
Ok(())
} else {
warn!(
"Security policy prevented `{}` from `{}` being routed to `{}`.",
policy_key.source_name, policy_key.source_moniker, target_moniker
);
Err(PolicyError::CapabilityUseDisallowed {
cap: policy_key.source_name.to_string(),
source_moniker: policy_key.source_moniker.to_owned(),
target_moniker: target_moniker.to_owned(),
})
}
}
None => Ok(()),
}
}
/// Returns Ok(()) if the provided debug capability source is allowed to be routed from given
/// environment.
pub fn can_register_debug_capability<'a>(
&self,
capability_type: CapabilityTypeName,
name: &'a cm_types::Name,
env_moniker: &'a Moniker,
env_name: &'a cm_types::Name,
) -> Result<(), PolicyError> {
let debug_key = DebugCapabilityKey {
name: name.clone(),
source: CapabilityAllowlistSource::Self_,
capability: capability_type,
env_name: env_name.clone(),
};
let route_allowed = match self.policy.debug_capability_policy.get(&debug_key) {
None => false,
Some(allowlist_set) => allowlist_set.iter().any(|entry| entry.matches(env_moniker)),
};
if route_allowed {
return Ok(());
}
warn!(
"Debug security policy prevented `{}` from being registered to environment `{}` in `{}`.",
debug_key.name, env_name, env_moniker,
);
Err(PolicyError::DebugCapabilityUseDisallowed {
cap: debug_key.name.to_string(),
env_moniker: env_moniker.to_owned(),
env_name: env_name.to_string(),
})
}
/// Returns Ok(()) if `target_moniker` is allowed to have `on_terminate=REBOOT` set.
pub fn reboot_on_terminate_allowed(&self, target_moniker: &Moniker) -> Result<(), PolicyError> {
self.policy
.child_policy
.reboot_on_terminate
.iter()
.any(|entry| entry.matches(&target_moniker))
.then(|| ())
.ok_or_else(|| PolicyError::ChildPolicyDisallowed {
policy: "reboot_on_terminate".to_owned(),
moniker: target_moniker.to_owned(),
})
}
}
/// Evaluates security policy relative to a specific Component (based on that Component's
/// Moniker).
#[derive(Clone)]
pub struct ScopedPolicyChecker {
/// The security policy to apply.
policy: Arc<SecurityPolicy>,
/// The moniker of the component that policy will be evaluated for.
pub scope: Moniker,
}
impl ScopedPolicyChecker {
pub fn new(policy: Arc<SecurityPolicy>, scope: Moniker) -> Self {
ScopedPolicyChecker { policy, scope }
}
// This interface is super simple for now since there's only three allowlists. In the future
// we'll probably want a different interface than an individual function per policy item.
pub fn ambient_mark_vmo_exec_allowed(&self) -> Result<(), PolicyError> {
self.policy
.job_policy
.ambient_mark_vmo_exec
.iter()
.any(|entry| entry.matches(&self.scope))
.then(|| ())
.ok_or_else(|| PolicyError::JobPolicyDisallowed {
policy: "ambient_mark_vmo_exec".to_owned(),
moniker: self.scope.to_owned(),
})
}
pub fn main_process_critical_allowed(&self) -> Result<(), PolicyError> {
self.policy
.job_policy
.main_process_critical
.iter()
.any(|entry| entry.matches(&self.scope))
.then(|| ())
.ok_or_else(|| PolicyError::JobPolicyDisallowed {
policy: "main_process_critical".to_owned(),
moniker: self.scope.to_owned(),
})
}
pub fn create_raw_processes_allowed(&self) -> Result<(), PolicyError> {
self.policy
.job_policy
.create_raw_processes
.iter()
.any(|entry| entry.matches(&self.scope))
.then(|| ())
.ok_or_else(|| PolicyError::JobPolicyDisallowed {
policy: "create_raw_processes".to_owned(),
moniker: self.scope.to_owned(),
})
}
}
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::assert_matches,
cm_config::{AllowlistEntryBuilder, ChildPolicyAllowlists, JobPolicyAllowlists},
moniker::ChildName,
std::collections::HashMap,
};
#[test]
fn scoped_policy_checker_vmex() {
macro_rules! assert_vmex_allowed_matches {
($policy:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
.ambient_mark_vmo_exec_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_vmex_disallowed {
($policy:expr, $moniker:expr) => {
assert_vmex_allowed_matches!(
$policy,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
let policy = Arc::new(SecurityPolicy::default());
assert_vmex_disallowed!(policy, Moniker::root());
assert_vmex_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap());
let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap();
let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap();
let policy = Arc::new(SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
main_process_critical: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
create_raw_processes: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
},
capability_policy: HashMap::new(),
debug_capability_policy: HashMap::new(),
child_policy: ChildPolicyAllowlists {
reboot_on_terminate: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
},
});
assert_vmex_allowed_matches!(policy, allowed1, Ok(()));
assert_vmex_allowed_matches!(policy, allowed2, Ok(()));
assert_vmex_disallowed!(policy, Moniker::root());
assert_vmex_disallowed!(policy, allowed1.parent().unwrap());
assert_vmex_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
}
#[test]
fn scoped_policy_checker_create_raw_processes() {
macro_rules! assert_create_raw_processes_allowed_matches {
($policy:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
.create_raw_processes_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_create_raw_processes_disallowed {
($policy:expr, $moniker:expr) => {
assert_create_raw_processes_allowed_matches!(
$policy,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
let policy = Arc::new(SecurityPolicy::default());
assert_create_raw_processes_disallowed!(policy, Moniker::root());
assert_create_raw_processes_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap());
let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap();
let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap();
let policy = Arc::new(SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![],
main_process_critical: vec![],
create_raw_processes: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
},
capability_policy: HashMap::new(),
debug_capability_policy: HashMap::new(),
child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
});
assert_create_raw_processes_allowed_matches!(policy, allowed1, Ok(()));
assert_create_raw_processes_allowed_matches!(policy, allowed2, Ok(()));
assert_create_raw_processes_disallowed!(policy, Moniker::root());
assert_create_raw_processes_disallowed!(policy, allowed1.parent().unwrap());
assert_create_raw_processes_disallowed!(
policy,
allowed1.child(ChildName::try_from("baz").unwrap())
);
}
#[test]
fn scoped_policy_checker_main_process_critical_allowed() {
macro_rules! assert_critical_allowed_matches {
($policy:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
.main_process_critical_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_critical_disallowed {
($policy:expr, $moniker:expr) => {
assert_critical_allowed_matches!(
$policy,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
let policy = Arc::new(SecurityPolicy::default());
assert_critical_disallowed!(policy, Moniker::root());
assert_critical_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap());
let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap();
let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap();
let policy = Arc::new(SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
main_process_critical: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
create_raw_processes: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
},
capability_policy: HashMap::new(),
debug_capability_policy: HashMap::new(),
child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
});
assert_critical_allowed_matches!(policy, allowed1, Ok(()));
assert_critical_allowed_matches!(policy, allowed2, Ok(()));
assert_critical_disallowed!(policy, Moniker::root());
assert_critical_disallowed!(policy, allowed1.parent().unwrap());
assert_critical_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
}
#[test]
fn scoped_policy_checker_reboot_policy_allowed() {
macro_rules! assert_reboot_allowed_matches {
($policy:expr, $moniker:expr, $expected:pat) => {
let result = GlobalPolicyChecker::new($policy.clone())
.reboot_on_terminate_allowed(&$moniker);
assert_matches!(result, $expected);
};
}
macro_rules! assert_reboot_disallowed {
($policy:expr, $moniker:expr) => {
assert_reboot_allowed_matches!(
$policy,
$moniker,
Err(PolicyError::ChildPolicyDisallowed { .. })
);
};
}
// Empty policy and enabled.
let policy = Arc::new(SecurityPolicy::default());
assert_reboot_disallowed!(policy, Moniker::root());
assert_reboot_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap());
// Nonempty policy.
let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap();
let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap();
let policy = Arc::new(SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![],
main_process_critical: vec![],
create_raw_processes: vec![],
},
capability_policy: HashMap::new(),
debug_capability_policy: HashMap::new(),
child_policy: ChildPolicyAllowlists {
reboot_on_terminate: vec![
AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
],
},
});
assert_reboot_allowed_matches!(policy, allowed1, Ok(()));
assert_reboot_allowed_matches!(policy, allowed2, Ok(()));
assert_reboot_disallowed!(policy, Moniker::root());
assert_reboot_disallowed!(policy, allowed1.parent().unwrap());
assert_reboot_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
}
}