Replace external reachability with an owned canary
This commit is contained in:
parent
0f6825c3b9
commit
31e9ab0f2d
60 changed files with 1409 additions and 1972 deletions
|
|
@ -684,7 +684,8 @@ mod tests {
|
|||
fs::write(
|
||||
&configuration_path,
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.agent]\nimage = \"example.invalid/image:v1\"\n[profiles.agent.readiness]\ndenied = [{{ address = \"10.0.0.1\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.agent]\nimage = \"example.invalid/image:v1\"
|
||||
",
|
||||
root.join("data").display()
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ impl Fixture {
|
|||
fs::write(
|
||||
root.path().join("config.toml"),
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/state:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/state:v1\"\npersistent_dirs = [\"/usr\"]
|
||||
",
|
||||
root.path().join("data").display()
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ use sha2::{Digest, Sha256};
|
|||
|
||||
use crate::identity::{ConfigurationFingerprint, DeviceId, DomainId, HostPath, InodeId, MountId};
|
||||
use crate::records::{
|
||||
ConfigurationFingerprintRecordV1, MailboxRequest, ResolvedAutoApprovalRule,
|
||||
ResolvedNetworkReadinessV1, ResolvedVolume, TRUSTED_HOST_CONFIGURATION_SCHEMA_ID,
|
||||
TrustedHostConfigurationV1, VolumeKind,
|
||||
ConfigurationFingerprintRecordV1, MailboxRequest, ResolvedAutoApprovalRule, ResolvedVolume,
|
||||
TRUSTED_HOST_CONFIGURATION_SCHEMA_ID, TrustedHostConfigurationV1, VolumeKind,
|
||||
};
|
||||
use confined_io::{io_error, open_absolute_directory, validate_executable};
|
||||
use validation::validate_sandbox_path;
|
||||
|
|
@ -456,6 +455,7 @@ fn validate_relative_consumer_path(path: &Path) -> Result<(), ConfigurationError
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EnvironmentValue(OsString);
|
||||
|
||||
impl fmt::Debug for EnvironmentValue {
|
||||
|
|
@ -656,11 +656,6 @@ impl ResolvedDomainConfiguration {
|
|||
&self.effective_deny_prefixes
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn network_readiness(&self) -> &ResolvedNetworkReadinessV1 {
|
||||
&self.fingerprint_record.network_readiness
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn ephemeral_environment(&self) -> &BTreeMap<String, EnvironmentValue> {
|
||||
&self.ephemeral_environment
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, Stat, openat2};
|
|||
|
||||
use crate::identity::HostPath;
|
||||
use crate::records::{
|
||||
EnvironmentReference, NetworkProbeTargetConfigurationV1, NetworkReadinessConfigurationV1,
|
||||
ProfileConfigurationV1, ResolvedAutoApprovalRule, ResolvedDnsProbeTargetV1,
|
||||
ResolvedNetworkProbeTargetV1, ResolvedNetworkReadinessV1, ResolvedVolume,
|
||||
EnvironmentReference, ProfileConfigurationV1, ResolvedAutoApprovalRule, ResolvedVolume,
|
||||
TrustedHostConfigurationV1, VolumeAccess, VolumeKind,
|
||||
};
|
||||
|
||||
|
|
@ -454,173 +452,6 @@ fn ipv4_prefixes_overlap(left: Ipv4Prefix, right: Ipv4Prefix) -> bool {
|
|||
u32::from_be_bytes(left.address()) & mask == u32::from_be_bytes(right.address()) & mask
|
||||
}
|
||||
|
||||
const MAX_READINESS_TARGETS: usize = 32;
|
||||
pub(super) fn resolve_network_readiness(
|
||||
readiness: &NetworkReadinessConfigurationV1,
|
||||
allow: &[Ipv4Prefix],
|
||||
deny: &[Ipv4Prefix],
|
||||
) -> Result<ResolvedNetworkReadinessV1, ConfigurationError> {
|
||||
if readiness.denied.is_empty() || readiness.permitted.is_empty() || readiness.dns.is_empty() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness requires denied, permitted, and DNS targets",
|
||||
));
|
||||
}
|
||||
if readiness.denied.len() > MAX_READINESS_TARGETS
|
||||
|| readiness.permitted.len() > MAX_READINESS_TARGETS
|
||||
|| readiness.dns.len() > MAX_READINESS_TARGETS
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness target count exceeds 32",
|
||||
));
|
||||
}
|
||||
|
||||
let denied = resolve_tcp_readiness_targets(&readiness.denied, allow, deny, false)?;
|
||||
let permitted = resolve_tcp_readiness_targets(&readiness.permitted, allow, deny, true)?;
|
||||
|
||||
let dns = readiness
|
||||
.dns
|
||||
.iter()
|
||||
.map(|target| {
|
||||
Ok(ResolvedDnsProbeTargetV1 {
|
||||
resolver: parse_probe_address(&target.resolver)?,
|
||||
query: normalize_dns_query(&target.query)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, ConfigurationError>>()?;
|
||||
if dns.len() != readiness.dns.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness contains duplicate DNS targets",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ResolvedNetworkReadinessV1 {
|
||||
denied: denied.into_iter().collect(),
|
||||
permitted: permitted.into_iter().collect(),
|
||||
dns: dns.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_tcp_readiness_targets(
|
||||
targets: &[NetworkProbeTargetConfigurationV1],
|
||||
allow: &[Ipv4Prefix],
|
||||
deny: &[Ipv4Prefix],
|
||||
expected_allowed: bool,
|
||||
) -> Result<BTreeSet<ResolvedNetworkProbeTargetV1>, ConfigurationError> {
|
||||
let resolved = targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
if target.port == 0 {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP target has port zero",
|
||||
));
|
||||
}
|
||||
let address = parse_probe_address(&target.address)?;
|
||||
if is_effectively_allowed(address, allow, deny) != expected_allowed {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP target disagrees with effective profile policy",
|
||||
));
|
||||
}
|
||||
Ok(ResolvedNetworkProbeTargetV1 {
|
||||
address,
|
||||
port: target.port,
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
if resolved.len() != targets.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness contains duplicate TCP targets",
|
||||
));
|
||||
}
|
||||
if resolved
|
||||
.iter()
|
||||
.map(|target| target.address)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len()
|
||||
!= resolved.len()
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP targets repeat an observation address",
|
||||
));
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn parse_probe_address(value: &str) -> Result<[u8; 4], ConfigurationError> {
|
||||
let address = value.parse::<std::net::Ipv4Addr>().map_err(|_| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness target address is not IPv4",
|
||||
)
|
||||
})?;
|
||||
if address.is_unspecified() || address.is_multicast() || address.is_broadcast() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness target address is not unicast",
|
||||
));
|
||||
}
|
||||
Ok(address.octets())
|
||||
}
|
||||
|
||||
fn normalize_dns_query(value: &str) -> Result<String, ConfigurationError> {
|
||||
let query = value.strip_suffix('.').unwrap_or(value);
|
||||
if query.is_empty() || query.len() > 253 || !query.is_ascii() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"DNS readiness query name is invalid",
|
||||
));
|
||||
}
|
||||
for label in query.split('.') {
|
||||
if label.is_empty()
|
||||
|| label.len() > 63
|
||||
|| label.starts_with('-')
|
||||
|| label.ends_with('-')
|
||||
|| !label
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"DNS readiness query name is invalid",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(query.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn is_effectively_allowed(address: [u8; 4], allow: &[Ipv4Prefix], deny: &[Ipv4Prefix]) -> bool {
|
||||
let best_allow = allow
|
||||
.iter()
|
||||
.filter(|prefix| prefix_contains(**prefix, address))
|
||||
.map(|prefix| prefix.prefix_length)
|
||||
.max();
|
||||
let best_deny = deny
|
||||
.iter()
|
||||
.filter(|prefix| prefix_contains(**prefix, address))
|
||||
.map(|prefix| prefix.prefix_length)
|
||||
.max();
|
||||
match (best_allow, best_deny) {
|
||||
(Some(allowed), Some(denied)) => allowed > denied,
|
||||
(Some(_) | None, None) => true,
|
||||
(None, Some(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_contains(prefix: Ipv4Prefix, address: [u8; 4]) -> bool {
|
||||
let mask = if prefix.prefix_length == 0 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << (32 - prefix.prefix_length)
|
||||
};
|
||||
u32::from_be_bytes(address) & mask == u32::from_be_bytes(prefix.address)
|
||||
}
|
||||
|
||||
pub(super) fn parse_ipv4_prefix(value: &str) -> Result<Ipv4Prefix, ConfigurationError> {
|
||||
let (address, prefix) = value.split_once('/').ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use super::confined_io::{
|
|||
host_path, io_error, is_strict_descendant, paths_intersect, read_stable_configuration,
|
||||
require_clear_of_protected, require_pairwise_disjoint, require_pairwise_non_ancestral,
|
||||
require_strict_child_of_one, resolve_environment_references, resolve_environment_values,
|
||||
resolve_network, resolve_network_readiness, validate_configuration_stat, validate_executable,
|
||||
validate_host_base, validate_persistent_destinations,
|
||||
resolve_network, validate_configuration_stat, validate_executable, validate_host_base,
|
||||
validate_persistent_destinations,
|
||||
};
|
||||
use super::validation::{
|
||||
validate_profile_name, validate_sandbox_path, validate_structural_configuration, validate_text,
|
||||
|
|
@ -153,11 +153,6 @@ pub fn load_trusted_configuration(
|
|||
)?;
|
||||
let (effective_allow_prefixes, effective_deny_prefixes) =
|
||||
resolve_network(&configuration, selected)?;
|
||||
let network_readiness = resolve_network_readiness(
|
||||
&selected.readiness,
|
||||
&effective_allow_prefixes,
|
||||
&effective_deny_prefixes,
|
||||
)?;
|
||||
let environment_references = resolve_environment_references(selected)?;
|
||||
|
||||
let mut volumes = Vec::with_capacity(selected.volumes.len());
|
||||
|
|
@ -360,7 +355,6 @@ pub fn load_trusted_configuration(
|
|||
.iter()
|
||||
.map(|prefix| (prefix.address, prefix.prefix_length))
|
||||
.collect(),
|
||||
network_readiness,
|
||||
escalation_enabled: selected.escalation.enabled,
|
||||
auto_approval_rules: resolved_auto_approval_records,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,34 +68,6 @@ pub fn derive_configuration_fingerprint(
|
|||
.iter()
|
||||
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.denied.iter().map(|target| {
|
||||
Ok([
|
||||
target.address.as_slice(),
|
||||
target.port.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat())
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.permitted.iter().map(|target| {
|
||||
Ok([
|
||||
target.address.as_slice(),
|
||||
target.port.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat())
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.dns.iter().map(|target| {
|
||||
let mut item = target.resolver.to_vec();
|
||||
push_bytes(&mut item, target.query.as_bytes())?;
|
||||
Ok(item)
|
||||
}),
|
||||
)?;
|
||||
push_bool(&mut preimage, record.escalation_enabled);
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
|
|
@ -128,6 +100,12 @@ pub(super) fn validate_structural_configuration(
|
|||
}
|
||||
for (sandbox_name, reference) in &profile.env {
|
||||
validate_environment_name(sandbox_name)?;
|
||||
if sandbox_name == "LAZARET_SESSION_ID" {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"environment name is reserved by Lazaret",
|
||||
));
|
||||
}
|
||||
let host_name = reference.strip_prefix("env:").ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::records::{
|
|||
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue,
|
||||
DERIVED_NETWORK_POLICY_SCHEMA_ID, DefaultAction, DerivedNetworkPolicy,
|
||||
DerivedNetworkPolicyFields, IpAddress, IpNetwork, NETWORK_READINESS_IDENTITY_SCHEMA_ID,
|
||||
NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation,
|
||||
NetworkCanaryObservation, NetworkObservation, NetworkReadinessAttestation,
|
||||
NetworkReadinessEvidence, NetworkReadinessIdentity, NetworkReadinessIdentityFields,
|
||||
PolicyAction, PolicyDelivery, PrefixRule, Resolver,
|
||||
};
|
||||
|
|
@ -42,19 +42,6 @@ impl CanonicalDerivedNetworkPolicy {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ReachabilityExpectation {
|
||||
pub denied_destinations: Vec<IpAddress>,
|
||||
pub permitted_destinations: Vec<IpAddress>,
|
||||
pub dns_queries: Vec<DnsQueryExpectation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct DnsQueryExpectation {
|
||||
pub resolver: IpAddress,
|
||||
pub query_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReadinessExpectation<'a> {
|
||||
pub domain_id: DomainId,
|
||||
|
|
@ -117,10 +104,9 @@ impl From<CodecError> for NetworkValidationError {
|
|||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when resolver or canonical policy validation fails.
|
||||
/// Returns an error when canonical policy validation fails.
|
||||
pub fn derive_ipv4_policy(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
resolvers: impl IntoIterator<Item = IpAddress>,
|
||||
) -> Result<CanonicalDerivedNetworkPolicy, NetworkValidationError> {
|
||||
let prefix_rules = configuration
|
||||
.effective_allow_prefixes()
|
||||
|
|
@ -163,13 +149,10 @@ pub fn derive_ipv4_policy(
|
|||
action: PolicyAction::Allow,
|
||||
}],
|
||||
prefix_rules,
|
||||
resolvers: resolvers
|
||||
.into_iter()
|
||||
.map(|address| Resolver {
|
||||
resolvers: vec![Resolver {
|
||||
family: AddressFamily::Ipv4,
|
||||
address,
|
||||
})
|
||||
.collect(),
|
||||
address: IpAddress::V4([10, 137, 54, 3]),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -243,25 +226,19 @@ pub fn readiness_fingerprint(
|
|||
)))
|
||||
}
|
||||
|
||||
/// Binds validated attestation and reachability results into readiness evidence.
|
||||
/// Binds validated attestation and owned-canary results into readiness evidence.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error for any identity, observation, policy, or probe mismatch.
|
||||
/// Returns an error for any identity, observation, policy, or canary mismatch.
|
||||
pub fn accept_network_readiness(
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
expectation: &ReadinessExpectation<'_>,
|
||||
attestation: NetworkReadinessAttestation,
|
||||
reachability: NetworkReachabilityProbe,
|
||||
expected_reachability: &ReachabilityExpectation,
|
||||
canary: NetworkCanaryObservation,
|
||||
) -> Result<NetworkReadinessEvidence, NetworkValidationError> {
|
||||
validate_attestation(policy, expectation, &attestation)?;
|
||||
validate_reachability(
|
||||
policy.policy(),
|
||||
expectation,
|
||||
&reachability,
|
||||
expected_reachability,
|
||||
)?;
|
||||
validate_canary(expectation, &canary)?;
|
||||
let identity = readiness_identity(&attestation);
|
||||
let fingerprint = readiness_fingerprint(&identity)?;
|
||||
Ok(NetworkReadinessEvidence {
|
||||
|
|
@ -271,7 +248,7 @@ pub fn accept_network_readiness(
|
|||
readiness_fingerprint: fingerprint,
|
||||
attestation,
|
||||
readiness_identity: identity,
|
||||
reachability_probe: reachability,
|
||||
canary_observation: canary,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -291,12 +268,11 @@ pub fn validate_network_attestation(
|
|||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error for any identity, projection, fingerprint, or probe mismatch.
|
||||
/// Returns an error for any identity, projection, fingerprint, or canary mismatch.
|
||||
pub fn validate_network_readiness_evidence(
|
||||
policy: &CanonicalDerivedNetworkPolicy,
|
||||
expectation: &ReadinessExpectation<'_>,
|
||||
evidence: &NetworkReadinessEvidence,
|
||||
expected_reachability: &ReachabilityExpectation,
|
||||
) -> Result<(), NetworkValidationError> {
|
||||
if evidence.domain_id != expectation.domain_id
|
||||
|| evidence.generation_id != expectation.generation_id
|
||||
|
|
@ -316,12 +292,7 @@ pub fn validate_network_readiness_evidence(
|
|||
"network readiness projection or fingerprint mismatch",
|
||||
));
|
||||
}
|
||||
validate_reachability(
|
||||
policy.policy(),
|
||||
expectation,
|
||||
&evidence.reachability_probe,
|
||||
expected_reachability,
|
||||
)
|
||||
validate_canary(expectation, &evidence.canary_observation)
|
||||
}
|
||||
|
||||
fn validate_address_families(
|
||||
|
|
@ -496,149 +467,40 @@ fn valid_observation(observation: &NetworkObservation) -> bool {
|
|||
&& observation.routes_attested
|
||||
}
|
||||
|
||||
fn validate_reachability(
|
||||
policy: &DerivedNetworkPolicy,
|
||||
fn validate_canary(
|
||||
expected: &ReadinessExpectation<'_>,
|
||||
probe: &NetworkReachabilityProbe,
|
||||
expected_results: &ReachabilityExpectation,
|
||||
canary: &NetworkCanaryObservation,
|
||||
) -> Result<(), NetworkValidationError> {
|
||||
if probe.domain_id != expected.domain_id
|
||||
|| probe.generation_id != expected.generation_id
|
||||
|| probe.enforcement_namespace != expected.enforcement_namespace
|
||||
|| expected_results.denied_destinations.is_empty()
|
||||
|| expected_results.permitted_destinations.is_empty()
|
||||
|| expected_results.dns_queries.is_empty()
|
||||
if canary.domain_id != expected.domain_id
|
||||
|| canary.generation_id != expected.generation_id
|
||||
|| canary.enforcement_namespace != expected.enforcement_namespace
|
||||
{
|
||||
return reachability_error("network probe identity or coverage mismatch");
|
||||
}
|
||||
|
||||
let denied = exact_observations(
|
||||
&expected_results.denied_destinations,
|
||||
&probe.denied_destinations,
|
||||
|item| (item.destination, item.unreachable),
|
||||
"denied destination observations differ",
|
||||
)?;
|
||||
let permitted = exact_observations(
|
||||
&expected_results.permitted_destinations,
|
||||
&probe.permitted_destinations,
|
||||
|item| (item.destination, item.reachable),
|
||||
"permitted destination observations differ",
|
||||
)?;
|
||||
if denied.iter().any(|(_, passed)| !passed) {
|
||||
return reachability_error("a configured denied destination was reachable");
|
||||
}
|
||||
if permitted.iter().any(|(_, passed)| !passed) {
|
||||
return reachability_error("a configured permitted destination was unreachable");
|
||||
}
|
||||
if expected_results
|
||||
.denied_destinations
|
||||
.iter()
|
||||
.any(|address| effective_action(policy, *address) != Some(PolicyAction::Deny))
|
||||
{
|
||||
return reachability_error("a denied probe destination is not denied by policy");
|
||||
}
|
||||
if expected_results
|
||||
.permitted_destinations
|
||||
.iter()
|
||||
.any(|address| effective_action(policy, *address) != Some(PolicyAction::Allow))
|
||||
{
|
||||
return reachability_error("a permitted probe destination is not allowed by policy");
|
||||
}
|
||||
|
||||
let expected_dns: BTreeSet<_> = expected_results.dns_queries.iter().cloned().collect();
|
||||
let actual_dns: BTreeSet<_> = probe
|
||||
.dns_observations
|
||||
.iter()
|
||||
.map(|item| DnsQueryExpectation {
|
||||
resolver: item.resolver,
|
||||
query_name: item.query_name.clone(),
|
||||
})
|
||||
.collect();
|
||||
let resolvers: BTreeSet<_> = policy
|
||||
.resolvers
|
||||
.iter()
|
||||
.map(|resolver| resolver.address)
|
||||
.collect();
|
||||
if expected_dns.len() != expected_results.dns_queries.len()
|
||||
|| actual_dns.len() != probe.dns_observations.len()
|
||||
|| actual_dns != expected_dns
|
||||
|| probe.dns_observations.iter().any(|item| {
|
||||
item.query_name.is_empty()
|
||||
|| !resolvers.contains(&item.resolver)
|
||||
|| item.resolved_addresses.is_empty()
|
||||
|| item
|
||||
.resolved_addresses
|
||||
.iter()
|
||||
.any(|address| address_family(*address) != AddressFamily::Ipv4)
|
||||
})
|
||||
{
|
||||
return reachability_error("DNS readiness observations differ or contain no IPv4 answer");
|
||||
return reachability_error("owned canary identity mismatch");
|
||||
}
|
||||
let passed = [
|
||||
canary.linux_policy_matches,
|
||||
canary.endpoints_authenticated_before_policy,
|
||||
canary.allowed_endpoint_authenticated,
|
||||
canary.denied_endpoint_unreachable,
|
||||
canary.imported_interface_matches,
|
||||
canary.tap_present,
|
||||
canary.ipv6_absent,
|
||||
canary.network_capabilities_empty,
|
||||
canary.route_mutation_refused,
|
||||
canary.cleanup_shape_matched,
|
||||
canary.responder_closed,
|
||||
canary.peer_namespace_closed,
|
||||
canary.host_interface_deleted,
|
||||
canary.allowed_endpoint_unreachable_after_cleanup,
|
||||
canary.tap_present_after_cleanup,
|
||||
canary.ipv6_unchanged_after_cleanup,
|
||||
canary.network_admin_dropped,
|
||||
];
|
||||
if passed.into_iter().all(|predicate| predicate) {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exact_observations<T, F>(
|
||||
expected: &[IpAddress],
|
||||
actual: &[T],
|
||||
project: F,
|
||||
mismatch: &'static str,
|
||||
) -> Result<BTreeSet<(IpAddress, bool)>, NetworkValidationError>
|
||||
where
|
||||
F: Fn(&T) -> (IpAddress, bool),
|
||||
{
|
||||
let expected_items: BTreeSet<_> = expected.iter().copied().collect();
|
||||
let actual_items: BTreeSet<_> = actual.iter().map(project).collect();
|
||||
let actual_addresses: BTreeSet<_> = actual_items.iter().map(|(address, _)| *address).collect();
|
||||
if expected_items.len() != expected.len()
|
||||
|| actual_items.len() != actual.len()
|
||||
|| expected_items.len() != actual_items.len()
|
||||
|| expected_items.len() != actual_addresses.len()
|
||||
|| actual_addresses != expected_items
|
||||
{
|
||||
return reachability_error(mismatch);
|
||||
} else {
|
||||
reachability_error("owned canary predicate failed")
|
||||
}
|
||||
Ok(actual_items)
|
||||
}
|
||||
|
||||
fn effective_action(policy: &DerivedNetworkPolicy, address: IpAddress) -> Option<PolicyAction> {
|
||||
let family = address_family(address);
|
||||
let disposition = policy
|
||||
.address_families
|
||||
.iter()
|
||||
.find(|item| item.family == family)?;
|
||||
if disposition.disposition != AddressFamilyDispositionValue::Enabled {
|
||||
return None;
|
||||
}
|
||||
policy
|
||||
.prefix_rules
|
||||
.iter()
|
||||
.filter(|rule| rule.family == family && network_contains(rule.network, address))
|
||||
.max_by_key(|rule| rule.network.prefix_length)
|
||||
.map(|rule| rule.action)
|
||||
.or_else(|| {
|
||||
policy
|
||||
.default_actions
|
||||
.iter()
|
||||
.find(|item| item.family == family)
|
||||
.map(|item| item.action)
|
||||
})
|
||||
}
|
||||
|
||||
fn network_contains(network: IpNetwork, address: IpAddress) -> bool {
|
||||
let prefix_length = network.prefix_length;
|
||||
let (network, address) = match (network.address, address) {
|
||||
(IpAddress::V4(network), IpAddress::V4(address)) => (network.to_vec(), address.to_vec()),
|
||||
(IpAddress::V6(network), IpAddress::V6(address)) => (network.to_vec(), address.to_vec()),
|
||||
_ => return false,
|
||||
};
|
||||
let complete = usize::from(prefix_length / 8);
|
||||
let remainder = prefix_length % 8;
|
||||
if network[..complete] != address[..complete] {
|
||||
return false;
|
||||
}
|
||||
remainder == 0
|
||||
|| network[complete] & (u8::MAX << (8 - remainder))
|
||||
== address[complete] & (u8::MAX << (8 - remainder))
|
||||
}
|
||||
|
||||
const fn address_family(address: IpAddress) -> AddressFamily {
|
||||
|
|
|
|||
|
|
@ -22,31 +22,6 @@ pub struct NetworkConfigurationV1 {
|
|||
pub default_deny: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkReadinessConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub denied: Vec<NetworkProbeTargetConfigurationV1>,
|
||||
#[serde(default)]
|
||||
pub permitted: Vec<NetworkProbeTargetConfigurationV1>,
|
||||
#[serde(default)]
|
||||
pub dns: Vec<DnsProbeTargetConfigurationV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkProbeTargetConfigurationV1 {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DnsProbeTargetConfigurationV1 {
|
||||
pub resolver: String,
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum VolumeKind {
|
||||
|
|
@ -106,8 +81,6 @@ pub struct ProfileConfigurationV1 {
|
|||
#[serde(default)]
|
||||
pub deny_egress: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub readiness: NetworkReadinessConfigurationV1,
|
||||
#[serde(default)]
|
||||
pub env: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<VolumeConfigurationV1>,
|
||||
|
|
@ -221,26 +194,6 @@ pub struct ConfigurationFingerprintRecordV1 {
|
|||
pub volumes: Vec<ResolvedVolume>,
|
||||
pub effective_allow_prefixes: Vec<([u8; 4], u8)>,
|
||||
pub effective_deny_prefixes: Vec<([u8; 4], u8)>,
|
||||
pub network_readiness: ResolvedNetworkReadinessV1,
|
||||
pub escalation_enabled: bool,
|
||||
pub auto_approval_rules: Vec<ResolvedAutoApprovalRule>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct ResolvedNetworkReadinessV1 {
|
||||
pub denied: Vec<ResolvedNetworkProbeTargetV1>,
|
||||
pub permitted: Vec<ResolvedNetworkProbeTargetV1>,
|
||||
pub dns: Vec<ResolvedDnsProbeTargetV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct ResolvedNetworkProbeTargetV1 {
|
||||
pub address: [u8; 4],
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct ResolvedDnsProbeTargetV1 {
|
||||
pub resolver: [u8; 4],
|
||||
pub query: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,33 +302,29 @@ impl<'de> Deserialize<'de> for NetworkReadinessIdentity {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct DeniedDestinationObservation {
|
||||
pub destination: IpAddress,
|
||||
pub unreachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct PermittedDestinationObservation {
|
||||
pub destination: IpAddress,
|
||||
pub reachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct DnsObservation {
|
||||
pub resolver: IpAddress,
|
||||
pub query_name: String,
|
||||
pub resolved_addresses: Vec<IpAddress>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NetworkReachabilityProbe {
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct NetworkCanaryObservation {
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub enforcement_namespace: NamespaceIdentity,
|
||||
pub denied_destinations: Vec<DeniedDestinationObservation>,
|
||||
pub permitted_destinations: Vec<PermittedDestinationObservation>,
|
||||
pub dns_observations: Vec<DnsObservation>,
|
||||
pub linux_policy_matches: bool,
|
||||
pub endpoints_authenticated_before_policy: bool,
|
||||
pub allowed_endpoint_authenticated: bool,
|
||||
pub denied_endpoint_unreachable: bool,
|
||||
pub imported_interface_matches: bool,
|
||||
pub tap_present: bool,
|
||||
pub ipv6_absent: bool,
|
||||
pub network_capabilities_empty: bool,
|
||||
pub route_mutation_refused: bool,
|
||||
pub cleanup_shape_matched: bool,
|
||||
pub responder_closed: bool,
|
||||
pub peer_namespace_closed: bool,
|
||||
pub host_interface_deleted: bool,
|
||||
pub allowed_endpoint_unreachable_after_cleanup: bool,
|
||||
pub tap_present_after_cleanup: bool,
|
||||
pub ipv6_unchanged_after_cleanup: bool,
|
||||
pub network_admin_dropped: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
|
|
@ -339,7 +335,7 @@ pub struct NetworkReadinessEvidence {
|
|||
pub readiness_fingerprint: ReadinessFingerprint,
|
||||
pub attestation: NetworkReadinessAttestation,
|
||||
pub readiness_identity: NetworkReadinessIdentity,
|
||||
pub reachability_probe: NetworkReachabilityProbe,
|
||||
pub canary_observation: NetworkCanaryObservation,
|
||||
}
|
||||
|
||||
impl NetworkReadinessEvidence {
|
||||
|
|
@ -357,10 +353,10 @@ impl NetworkReadinessEvidence {
|
|||
.is_ok_and(|canonical| canonical == readiness_fingerprint)
|
||||
&& self.domain_id == self.attestation.domain_id
|
||||
&& self.domain_id == self.readiness_identity.domain_id
|
||||
&& self.domain_id == self.reachability_probe.domain_id
|
||||
&& self.domain_id == self.canary_observation.domain_id
|
||||
&& self.generation_id == self.attestation.generation_id
|
||||
&& self.generation_id == self.readiness_identity.generation_id
|
||||
&& self.generation_id == self.reachability_probe.generation_id
|
||||
&& self.generation_id == self.canary_observation.generation_id
|
||||
&& self.network_sidecar_id == self.attestation.network_sidecar_id
|
||||
&& self.network_sidecar_id == self.readiness_identity.network_sidecar_id
|
||||
&& self.attestation.configuration_fingerprint
|
||||
|
|
@ -369,7 +365,7 @@ impl NetworkReadinessEvidence {
|
|||
&& self.readiness_identity.derived_policy_digest == derived_policy_digest
|
||||
&& self.attestation.enforcement_namespace == namespace
|
||||
&& self.readiness_identity.enforcement_namespace == namespace
|
||||
&& self.reachability_probe.enforcement_namespace == namespace
|
||||
&& self.canary_observation.enforcement_namespace == namespace
|
||||
&& self.attestation.runtime_supervisor == *runtime_supervisor
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
use super::{
|
||||
AddressFamily, AddressFamilyDisposition, ArtifactLease, ArtifactState, ClientEnrollmentRequest,
|
||||
ClientEnrollmentResponse, ConfigurationFingerprintRecordV1, ControlRequest, ControlResponse,
|
||||
DefaultAction, DeniedDestinationObservation, DerivedNetworkPolicy, DeveloperDecision,
|
||||
DeveloperDecisionKind, DnsObservation, Domain, DomainStore, DomainStoreMetadata,
|
||||
EnvironmentReference, EscalationOutcome, Generation, GenerationIdentity, GitRoutingOwnership,
|
||||
GuestProcessIdentity, HelperOwnership, HostProcessIdentity, HostProcessOwnershipSlot,
|
||||
InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionMessage,
|
||||
InteractiveSessionRequest, Mailbox, MailboxCancellation, MailboxRequest, MailboxResult,
|
||||
MailboxResultAck, MailboxResultChunk, MountIdentity, NamespaceIdentity, NetworkObservation,
|
||||
NetworkOwnership, NetworkReachabilityProbe, NetworkReadinessAttestation,
|
||||
NetworkReadinessEvidence, NetworkReadinessIdentity, NonInteractiveOutput,
|
||||
NonInteractiveSessionMessage, NonInteractiveSessionRequest, Participant, ParticipantOwnership,
|
||||
ParticipantState, PermittedDestinationObservation, PersistedHostProcessOwnership,
|
||||
PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction, PolicyDelivery, PrefixRule,
|
||||
PrepareOperation, PrepareStageState, PreparedArtifact, PreparedLowerOwnership,
|
||||
ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion, ProtocolCredential,
|
||||
ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity, PublicationOwnership,
|
||||
ResolvedAutoApprovalRule, ResolvedNetworkReadinessV1, ResolvedVolume, Resolver, ResultStream,
|
||||
DefaultAction, DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, Domain,
|
||||
DomainStore, DomainStoreMetadata, EnvironmentReference, EscalationOutcome, Generation,
|
||||
GenerationIdentity, GitRoutingOwnership, GuestProcessIdentity, HelperOwnership,
|
||||
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, Mailbox,
|
||||
MailboxCancellation, MailboxRequest, MailboxResult, MailboxResultAck, MailboxResultChunk,
|
||||
MountIdentity, NamespaceIdentity, NetworkCanaryObservation, NetworkObservation,
|
||||
NetworkOwnership, NetworkReadinessAttestation, NetworkReadinessEvidence,
|
||||
NetworkReadinessIdentity, NonInteractiveOutput, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, Participant, ParticipantOwnership, ParticipantState,
|
||||
PersistedHostProcessOwnership, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction,
|
||||
PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact,
|
||||
PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion,
|
||||
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity,
|
||||
PublicationOwnership, ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream,
|
||||
SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit,
|
||||
SessionExitOutcome, SessionRegistrationRequest, SessionRegistrationResponse,
|
||||
SessionRequestCapability, SessionTerminal, SessionTerminalEvent, SessionTerminalMessage,
|
||||
|
|
@ -136,16 +135,12 @@ schema_types!(
|
|||
"NetworkObservation" => NetworkObservation,
|
||||
"PolicyDelivery" => PolicyDelivery,
|
||||
"NetworkReadinessIdentity" => NetworkReadinessIdentity,
|
||||
"NetworkReachabilityProbe" => NetworkReachabilityProbe,
|
||||
"DeniedDestinationObservation" => DeniedDestinationObservation,
|
||||
"PermittedDestinationObservation" => PermittedDestinationObservation,
|
||||
"DnsObservation" => DnsObservation,
|
||||
"NetworkCanaryObservation" => NetworkCanaryObservation,
|
||||
"TrustedHostConfigurationV1" => TrustedHostConfigurationV1,
|
||||
"ConfigurationFingerprintRecordV1" => ConfigurationFingerprintRecordV1,
|
||||
"EnvironmentReference" => EnvironmentReference,
|
||||
"ResolvedVolume" => ResolvedVolume,
|
||||
"ResolvedAutoApprovalRule" => ResolvedAutoApprovalRule,
|
||||
"ResolvedNetworkReadinessV1" => ResolvedNetworkReadinessV1,
|
||||
"DomainStore" => DomainStore,
|
||||
"StorageCatalog" => StorageCatalog,
|
||||
"PrepareStageState" => PrepareStageState,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use lazaret_core::configuration::{
|
|||
use lazaret_core::identity::HostPath;
|
||||
use lazaret_core::records::{
|
||||
ConfigurationFingerprintRecordV1, EnvironmentReference, ResolvedAutoApprovalRule,
|
||||
ResolvedDnsProbeTargetV1, ResolvedNetworkProbeTargetV1, ResolvedNetworkReadinessV1,
|
||||
ResolvedVolume, VolumeAccess, VolumeKind,
|
||||
};
|
||||
use static_assertions::assert_not_impl_any;
|
||||
|
|
@ -113,11 +112,6 @@ volumes = [
|
|||
{{ host = "{{profile_dir}}/agent-state", container = "/root/.local/state/agent", kind = "directory", access = "read-write", create = true }},
|
||||
]
|
||||
|
||||
[profiles.default.readiness]
|
||||
denied = [{{ address = "10.2.3.4", port = 443 }}]
|
||||
permitted = [{{ address = "192.0.2.4", port = 443 }}]
|
||||
dns = [{{ resolver = "198.51.100.53", query = "example.invalid" }}]
|
||||
|
||||
[profiles.default.escalation]
|
||||
enabled = false
|
||||
"#,
|
||||
|
|
@ -283,6 +277,16 @@ allow_extra_args = true
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_cannot_claim_the_session_identity_environment_name() {
|
||||
let document = b"schema = \"lazaret-trusted-host-config-v1\"\n\
|
||||
[profiles.default]\n\
|
||||
image = \"example.invalid/default:v1\"\n\
|
||||
env = { LAZARET_SESSION_ID = \"env:HOST_SESSION_ID\" }\n";
|
||||
let error = parse_trusted_configuration(document).unwrap_err();
|
||||
assert_eq!(error.kind(), ConfigurationErrorKind::InvalidProfile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_profile_loads_into_confined_nonserializable_authority() {
|
||||
let tree = TestTree::new();
|
||||
|
|
@ -761,20 +765,6 @@ fn digest_vectors_are_framed_and_order_independent() {
|
|||
}],
|
||||
effective_allow_prefixes: vec![([8, 8, 8, 8], 32), ([1, 1, 1, 1], 32)],
|
||||
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)],
|
||||
network_readiness: ResolvedNetworkReadinessV1 {
|
||||
denied: vec![ResolvedNetworkProbeTargetV1 {
|
||||
address: [10, 0, 0, 1],
|
||||
port: 9,
|
||||
}],
|
||||
permitted: vec![ResolvedNetworkProbeTargetV1 {
|
||||
address: [1, 1, 1, 1],
|
||||
port: 443,
|
||||
}],
|
||||
dns: vec![ResolvedDnsProbeTargetV1 {
|
||||
resolver: [8, 8, 8, 8],
|
||||
query: "example.invalid".into(),
|
||||
}],
|
||||
},
|
||||
escalation_enabled: true,
|
||||
auto_approval_rules: vec![ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
|
|
@ -786,9 +776,9 @@ fn digest_vectors_are_framed_and_order_independent() {
|
|||
assert_eq!(
|
||||
first.as_bytes(),
|
||||
&[
|
||||
0x22, 0x57, 0x11, 0xe7, 0xc9, 0x11, 0xba, 0x8e, 0x08, 0x11, 0xae, 0x56, 0x5b, 0x7d,
|
||||
0x89, 0xb0, 0x73, 0x48, 0xaa, 0x73, 0x87, 0x19, 0x5e, 0xcd, 0xf8, 0xfa, 0xd1, 0x48,
|
||||
0xc1, 0x5b, 0x90, 0x57,
|
||||
0xd3, 0xba, 0xef, 0x91, 0x21, 0x0f, 0x98, 0x31, 0x6c, 0xea, 0x34, 0xfb, 0x41, 0xbd,
|
||||
0x6c, 0xa5, 0x07, 0xf0, 0xb8, 0x9b, 0x90, 0x77, 0xaf, 0x0a, 0x0e, 0xd0, 0x6a, 0xd6,
|
||||
0x46, 0xc7, 0x2d, 0xbf,
|
||||
]
|
||||
);
|
||||
record.persistent_dirs.reverse();
|
||||
|
|
|
|||
|
|
@ -6,21 +6,20 @@ use lazaret_core::identity::{
|
|||
PidfsProcessAuthorityV1, PidfsProcessIdentityV1, ProcessStartTimeTicks, ReadinessFingerprint,
|
||||
};
|
||||
use lazaret_core::network::{
|
||||
DnsQueryExpectation, NetworkValidationErrorKind, ReachabilityExpectation, ReadinessExpectation,
|
||||
accept_network_readiness, canonical_readiness_identity_bytes, derive_policy,
|
||||
readiness_fingerprint, readiness_identity, validate_network_readiness_evidence,
|
||||
NetworkValidationErrorKind, ReadinessExpectation, accept_network_readiness,
|
||||
canonical_readiness_identity_bytes, derive_policy, readiness_fingerprint, readiness_identity,
|
||||
validate_network_readiness_evidence,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction,
|
||||
DeniedDestinationObservation, DerivedNetworkPolicyFields, DnsObservation, IpAddress, IpNetwork,
|
||||
NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation,
|
||||
NetworkReadinessAttestationFields, PermittedDestinationObservation, PolicyAction,
|
||||
PolicyDelivery, PrefixRule, Resolver,
|
||||
DerivedNetworkPolicyFields, IpAddress, IpNetwork, NetworkCanaryObservation, NetworkObservation,
|
||||
NetworkReadinessAttestation, NetworkReadinessAttestationFields, PolicyAction, PolicyDelivery,
|
||||
PrefixRule, Resolver,
|
||||
};
|
||||
|
||||
type AttestationMutation = fn(&mut NetworkReadinessAttestation);
|
||||
type EvidenceMutation = fn(&mut lazaret_core::records::NetworkReadinessEvidence);
|
||||
type ProbeMutation = fn(&mut NetworkReachabilityProbe);
|
||||
type CanaryMutation = fn(&mut NetworkCanaryObservation);
|
||||
|
||||
fn repeated(value: u8) -> [u8; 32] {
|
||||
[value; 32]
|
||||
|
|
@ -107,8 +106,7 @@ struct Fixture {
|
|||
runtime_supervisor: PidfsProcessIdentityV1,
|
||||
connectivity_helper: PidfsProcessIdentityV1,
|
||||
attestation: NetworkReadinessAttestation,
|
||||
probe: NetworkReachabilityProbe,
|
||||
reachability: ReachabilityExpectation,
|
||||
canary: NetworkCanaryObservation,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
|
|
@ -135,39 +133,34 @@ impl Fixture {
|
|||
routes_attested: true,
|
||||
},
|
||||
});
|
||||
let probe = NetworkReachabilityProbe {
|
||||
let canary = NetworkCanaryObservation {
|
||||
domain_id: DomainId::new(repeated(1)),
|
||||
generation_id: GenerationId::new(2),
|
||||
enforcement_namespace: namespace(),
|
||||
denied_destinations: vec![DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 1, 2, 3]),
|
||||
unreachable: true,
|
||||
}],
|
||||
permitted_destinations: vec![PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 20, 1, 2]),
|
||||
reachable: true,
|
||||
}],
|
||||
dns_observations: vec![DnsObservation {
|
||||
resolver: IpAddress::V4([192, 0, 2, 53]),
|
||||
query_name: "sandbox.invalid".into(),
|
||||
resolved_addresses: vec![IpAddress::V4([192, 0, 2, 10])],
|
||||
}],
|
||||
};
|
||||
let reachability = ReachabilityExpectation {
|
||||
denied_destinations: vec![IpAddress::V4([10, 1, 2, 3])],
|
||||
permitted_destinations: vec![IpAddress::V4([10, 20, 1, 2])],
|
||||
dns_queries: vec![DnsQueryExpectation {
|
||||
resolver: IpAddress::V4([192, 0, 2, 53]),
|
||||
query_name: "sandbox.invalid".into(),
|
||||
}],
|
||||
linux_policy_matches: true,
|
||||
endpoints_authenticated_before_policy: true,
|
||||
allowed_endpoint_authenticated: true,
|
||||
denied_endpoint_unreachable: true,
|
||||
imported_interface_matches: true,
|
||||
tap_present: true,
|
||||
ipv6_absent: true,
|
||||
network_capabilities_empty: true,
|
||||
route_mutation_refused: true,
|
||||
cleanup_shape_matched: true,
|
||||
responder_closed: true,
|
||||
peer_namespace_closed: true,
|
||||
host_interface_deleted: true,
|
||||
allowed_endpoint_unreachable_after_cleanup: true,
|
||||
tap_present_after_cleanup: true,
|
||||
ipv6_unchanged_after_cleanup: true,
|
||||
network_admin_dropped: true,
|
||||
};
|
||||
Self {
|
||||
policy,
|
||||
runtime_supervisor,
|
||||
connectivity_helper,
|
||||
attestation,
|
||||
probe,
|
||||
reachability,
|
||||
canary,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,8 +181,7 @@ impl Fixture {
|
|||
&self.policy,
|
||||
&self.expectation(),
|
||||
self.attestation.clone(),
|
||||
self.probe.clone(),
|
||||
&self.reachability,
|
||||
self.canary.clone(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -344,8 +336,7 @@ fn every_attestation_identity_mismatch_is_refused() {
|
|||
&fixture.policy,
|
||||
&fixture.expectation(),
|
||||
attestation,
|
||||
fixture.probe.clone(),
|
||||
&fixture.reachability,
|
||||
fixture.canary.clone(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
|
|
@ -392,13 +383,13 @@ fn every_evidence_identity_mismatch_is_refused() {
|
|||
value.readiness_fingerprint = ReadinessFingerprint::new(repeated(90));
|
||||
}),
|
||||
("probe domain", |value| {
|
||||
value.reachability_probe.domain_id = DomainId::new(repeated(91));
|
||||
value.canary_observation.domain_id = DomainId::new(repeated(91));
|
||||
}),
|
||||
("probe generation", |value| {
|
||||
value.reachability_probe.generation_id = GenerationId::new(92);
|
||||
value.canary_observation.generation_id = GenerationId::new(92);
|
||||
}),
|
||||
("probe namespace", |value| {
|
||||
value.reachability_probe.enforcement_namespace.inode = InodeId::new(93);
|
||||
value.canary_observation.enforcement_namespace.inode = InodeId::new(93);
|
||||
}),
|
||||
];
|
||||
for (name, mutate) in mutations {
|
||||
|
|
@ -409,7 +400,6 @@ fn every_evidence_identity_mismatch_is_refused() {
|
|||
&fixture.policy,
|
||||
&fixture.expectation(),
|
||||
&evidence,
|
||||
&fixture.reachability,
|
||||
)
|
||||
.is_err(),
|
||||
"accepted {name}"
|
||||
|
|
@ -418,34 +408,35 @@ fn every_evidence_identity_mismatch_is_refused() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_or_failed_reachability_is_refused() {
|
||||
fn incomplete_or_failed_canary_is_refused() {
|
||||
let fixture = Fixture::new();
|
||||
let mutations: [(&str, ProbeMutation); 6] = [
|
||||
let mutations: [(&str, CanaryMutation); 6] = [
|
||||
("deny reached", |value| {
|
||||
value.denied_destinations[0].unreachable = false;
|
||||
value.denied_endpoint_unreachable = false;
|
||||
}),
|
||||
("permit unreachable", |value| {
|
||||
value.permitted_destinations[0].reachable = false;
|
||||
value.allowed_endpoint_authenticated = false;
|
||||
}),
|
||||
("missing deny", |value| value.denied_destinations.clear()),
|
||||
("missing permit", |value| {
|
||||
value.permitted_destinations.clear();
|
||||
("policy mismatch", |value| {
|
||||
value.linux_policy_matches = false;
|
||||
}),
|
||||
("missing DNS", |value| value.dns_observations.clear()),
|
||||
("empty DNS result", |value| {
|
||||
value.dns_observations[0].resolved_addresses.clear();
|
||||
("cleanup mismatch", |value| {
|
||||
value.cleanup_shape_matched = false;
|
||||
}),
|
||||
("capability retained", |value| {
|
||||
value.network_admin_dropped = false;
|
||||
}),
|
||||
("IPv6 present", |value| value.ipv6_absent = false),
|
||||
];
|
||||
for (name, mutate) in mutations {
|
||||
let mut probe = fixture.probe.clone();
|
||||
mutate(&mut probe);
|
||||
let mut canary = fixture.canary.clone();
|
||||
mutate(&mut canary);
|
||||
assert!(
|
||||
accept_network_readiness(
|
||||
&fixture.policy,
|
||||
&fixture.expectation(),
|
||||
fixture.attestation.clone(),
|
||||
probe,
|
||||
&fixture.reachability,
|
||||
canary,
|
||||
)
|
||||
.is_err(),
|
||||
"accepted {name}"
|
||||
|
|
@ -475,8 +466,7 @@ fn incomplete_or_malformed_attestation_is_refused() {
|
|||
&fixture.policy,
|
||||
&fixture.expectation(),
|
||||
attestation,
|
||||
fixture.probe.clone(),
|
||||
&fixture.reachability,
|
||||
fixture.canary.clone(),
|
||||
)
|
||||
.is_err(),
|
||||
"accepted {name}"
|
||||
|
|
|
|||
|
|
@ -387,24 +387,28 @@ fn readiness_identity() -> NetworkReadinessIdentity {
|
|||
})
|
||||
}
|
||||
|
||||
fn reachability_probe() -> NetworkReachabilityProbe {
|
||||
NetworkReachabilityProbe {
|
||||
fn canary_observation() -> NetworkCanaryObservation {
|
||||
NetworkCanaryObservation {
|
||||
domain_id: domain_id(),
|
||||
generation_id: generation_id(),
|
||||
enforcement_namespace: namespace(),
|
||||
denied_destinations: vec![DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 1, 2, 3]),
|
||||
unreachable: true,
|
||||
}],
|
||||
permitted_destinations: vec![PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([1, 1, 1, 1]),
|
||||
reachable: true,
|
||||
}],
|
||||
dns_observations: vec![DnsObservation {
|
||||
resolver: IpAddress::V4([9, 9, 9, 9]),
|
||||
query_name: "example.invalid".into(),
|
||||
resolved_addresses: vec![IpAddress::V6([1; 16])],
|
||||
}],
|
||||
linux_policy_matches: true,
|
||||
endpoints_authenticated_before_policy: true,
|
||||
allowed_endpoint_authenticated: true,
|
||||
denied_endpoint_unreachable: true,
|
||||
imported_interface_matches: true,
|
||||
tap_present: true,
|
||||
ipv6_absent: true,
|
||||
network_capabilities_empty: true,
|
||||
route_mutation_refused: true,
|
||||
cleanup_shape_matched: true,
|
||||
responder_closed: true,
|
||||
peer_namespace_closed: true,
|
||||
host_interface_deleted: true,
|
||||
allowed_endpoint_unreachable_after_cleanup: true,
|
||||
tap_present_after_cleanup: true,
|
||||
ipv6_unchanged_after_cleanup: true,
|
||||
network_admin_dropped: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -417,7 +421,7 @@ fn readiness_evidence() -> NetworkReadinessEvidence {
|
|||
readiness_fingerprint: derive_readiness_fingerprint(&readiness_identity).unwrap(),
|
||||
attestation: readiness_attestation(),
|
||||
readiness_identity,
|
||||
reachability_probe: reachability_probe(),
|
||||
canary_observation: canary_observation(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ fn trusted_host_configuration_postcard_dispatch_rejects_unknown_schema() {
|
|||
persistent_dirs: vec![],
|
||||
allow_egress: vec![],
|
||||
deny_egress: vec![],
|
||||
readiness: NetworkReadinessConfigurationV1::default(),
|
||||
env: BTreeMap::new(),
|
||||
volumes: vec![],
|
||||
escalation: EscalationConfigurationV1::default(),
|
||||
|
|
|
|||
|
|
@ -531,29 +531,7 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
verify_shape(network_observation(), &mut seen);
|
||||
verify_shape(PolicyDelivery::SealedMemfd, &mut seen);
|
||||
verify_shape(readiness_identity(), &mut seen);
|
||||
verify_shape(reachability_probe(), &mut seen);
|
||||
verify_shape(
|
||||
DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 0, 0, 1]),
|
||||
unreachable: true,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([1, 0, 0, 1]),
|
||||
reachable: true,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
DnsObservation {
|
||||
resolver: IpAddress::V4([9, 9, 9, 9]),
|
||||
query_name: "example.invalid".into(),
|
||||
resolved_addresses: vec![IpAddress::V4([203, 0, 113, 1])],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(canary_observation(), &mut seen);
|
||||
verify_shape(
|
||||
DomainStore {
|
||||
metadata: DomainStoreMetadata::v1(),
|
||||
|
|
@ -606,20 +584,6 @@ image = "example.invalid/agent:v1"
|
|||
}],
|
||||
effective_allow_prefixes: vec![([1, 1, 1, 1], 32)],
|
||||
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)],
|
||||
network_readiness: ResolvedNetworkReadinessV1 {
|
||||
denied: vec![ResolvedNetworkProbeTargetV1 {
|
||||
address: [10, 0, 0, 1],
|
||||
port: 9,
|
||||
}],
|
||||
permitted: vec![ResolvedNetworkProbeTargetV1 {
|
||||
address: [1, 1, 1, 1],
|
||||
port: 443,
|
||||
}],
|
||||
dns: vec![ResolvedDnsProbeTargetV1 {
|
||||
resolver: [8, 8, 8, 8],
|
||||
query: "example.invalid".into(),
|
||||
}],
|
||||
},
|
||||
escalation_enabled: true,
|
||||
auto_approval_rules: vec![ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
|
|
@ -654,23 +618,6 @@ image = "example.invalid/agent:v1"
|
|||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ResolvedNetworkReadinessV1 {
|
||||
denied: vec![ResolvedNetworkProbeTargetV1 {
|
||||
address: [10, 0, 0, 1],
|
||||
port: 9,
|
||||
}],
|
||||
permitted: vec![ResolvedNetworkProbeTargetV1 {
|
||||
address: [1, 1, 1, 1],
|
||||
port: 443,
|
||||
}],
|
||||
dns: vec![ResolvedDnsProbeTargetV1 {
|
||||
resolver: [8, 8, 8, 8],
|
||||
query: "example.invalid".into(),
|
||||
}],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
BootstrapWaitRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
|
|
@ -693,8 +640,8 @@ image = "example.invalid/agent:v1"
|
|||
assert_eq!(
|
||||
seen.digest(),
|
||||
[
|
||||
255, 220, 6, 56, 49, 12, 147, 182, 148, 61, 218, 228, 124, 211, 136, 128, 186, 186,
|
||||
138, 239, 246, 197, 75, 0, 75, 85, 27, 244, 21, 21, 246, 41,
|
||||
245, 169, 157, 164, 67, 211, 165, 23, 129, 62, 119, 92, 247, 248, 131, 4, 252, 42, 34,
|
||||
214, 207, 60, 126, 219, 81, 68, 159, 6, 26, 88, 233, 108,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,14 +370,14 @@ fn active_network_rejects_every_mismatched_readiness_relation() {
|
|||
}),
|
||||
),
|
||||
(
|
||||
"reachability domain",
|
||||
"canary domain",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reachability_probe
|
||||
.canary_observation
|
||||
.domain_id = other_domain;
|
||||
}),
|
||||
),
|
||||
|
|
@ -406,14 +406,14 @@ fn active_network_rejects_every_mismatched_readiness_relation() {
|
|||
}),
|
||||
),
|
||||
(
|
||||
"reachability generation",
|
||||
"canary generation",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reachability_probe
|
||||
.canary_observation
|
||||
.generation_id = other_generation;
|
||||
}),
|
||||
),
|
||||
|
|
@ -502,14 +502,14 @@ fn active_network_rejects_every_mismatched_readiness_relation() {
|
|||
}),
|
||||
),
|
||||
(
|
||||
"reachability namespace",
|
||||
"canary namespace",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reachability_probe
|
||||
.canary_observation
|
||||
.enforcement_namespace = other_namespace;
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -476,7 +476,8 @@ mod tests {
|
|||
fs::write(
|
||||
&configuration_path,
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/state:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/state:v1\"\npersistent_dirs = [\"/usr\"]
|
||||
",
|
||||
root.join("data").display()
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ use std::sync::Mutex;
|
|||
|
||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
|
||||
use lazaret_core::records::Session;
|
||||
use lazaret_core::records::{
|
||||
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
|
||||
ParticipantState, PreparedArtifact,
|
||||
};
|
||||
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||
use lazaret_state_anchor::StateAnchorLayout;
|
||||
|
|
@ -70,8 +69,6 @@ pub struct BootstrapPlan<'a> {
|
|||
pub runtime_launch: RuntimeSupervisorLaunch,
|
||||
pub sandbox_bundle: &'a SandboxBundle,
|
||||
pub network_policy: &'a CanonicalDerivedNetworkPolicy,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
pub reachability: &'a ReachabilityExpectation,
|
||||
pub container_id: ContainerId,
|
||||
pub runtime_root: PathBuf,
|
||||
pub bundle_digest: BundleDigest,
|
||||
|
|
@ -98,6 +95,7 @@ pub struct CoordinatorLifecycleAdapter<'a> {
|
|||
escalation: Option<EscalationService<'a>>,
|
||||
escalation_handle: Option<EscalationServiceHandle>,
|
||||
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
||||
awaiting_sandbox: Option<crate::runtime::AwaitingSandboxReadiness>,
|
||||
runtime: Option<CommittedSandboxRuntime>,
|
||||
state_anchor_mount: Option<MountIdentity>,
|
||||
git_routing_mount: Option<MountIdentity>,
|
||||
|
|
@ -197,6 +195,7 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
escalation: None,
|
||||
escalation_handle: None,
|
||||
awaiting_runtime: None,
|
||||
awaiting_sandbox: None,
|
||||
runtime: None,
|
||||
state_anchor_mount: None,
|
||||
git_routing_mount: None,
|
||||
|
|
@ -330,12 +329,16 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
fn start_connectivity_helper(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
self.awaiting_runtime
|
||||
let awaiting = self
|
||||
.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
|
||||
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?;
|
||||
awaiting
|
||||
.receive_helper_staged(&network)
|
||||
.map(|_| ())
|
||||
.map_err(|error| failure(format!("starting connectivity helper: {error}")))
|
||||
.map_err(|error| failure(format!("starting connectivity helper: {error}")))?;
|
||||
awaiting
|
||||
.receive_network_attestation(&network, self.plan.network_policy)
|
||||
.map_err(|error| failure(format!("attesting prepared network: {error}")))
|
||||
}
|
||||
|
||||
fn start_sidecar(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
|
|
@ -349,21 +352,16 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
fn mark_network_ready(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
let mut probe = self.plan.network_probe.clone();
|
||||
self.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
|
||||
.receive_network_readiness(
|
||||
&network,
|
||||
self.plan.network_policy,
|
||||
|namespace| {
|
||||
probe.enforcement_namespace = namespace;
|
||||
probe
|
||||
},
|
||||
self.plan.reachability,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|error| failure(format!("marking network ready: {error}")))
|
||||
let sandbox = SandboxCoordinator::new(&store);
|
||||
let pending = self
|
||||
.awaiting_sandbox
|
||||
.take()
|
||||
.ok_or_else(|| LifecycleServiceError("Sandbox readiness is absent".into()))?;
|
||||
let (runtime, _) = pending
|
||||
.commit_networked(&sandbox, &network, self.plan.network_policy)
|
||||
.map_err(|error| failure(format!("marking network ready: {error}")))?;
|
||||
self.runtime = Some(runtime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_sandbox(&mut self) -> Result<(), LifecycleServiceError> {
|
||||
|
|
@ -381,12 +379,10 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
.sandbox_bundle
|
||||
.capture_handoff_authority(self.plan.configuration)
|
||||
.map_err(|error| failure(format!("capturing sandbox handoff: {error}")))?;
|
||||
let (runtime, _) = pending
|
||||
let awaiting = pending
|
||||
.acknowledge(staged, &handoff)
|
||||
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?
|
||||
.commit_networked(&sandbox)
|
||||
.map_err(|error| failure(format!("committing sandbox readiness: {error}")))?;
|
||||
self.runtime = Some(runtime);
|
||||
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?;
|
||||
self.awaiting_sandbox = Some(awaiting);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
|
|||
Arc::clone(&store),
|
||||
InstalledProductPaths {
|
||||
escalation_client: &installed.join("lazaret-escalation-client"),
|
||||
sandbox_init: &executable,
|
||||
sandbox_init: &installed.join("lazaret-sandbox-init"),
|
||||
},
|
||||
))
|
||||
.map_err(failure)?;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ use std::sync::{Arc, Mutex};
|
|||
|
||||
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
|
||||
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
|
||||
use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy};
|
||||
use lazaret_core::network::derive_ipv4_policy;
|
||||
use lazaret_core::records::{
|
||||
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
|
||||
Generation, GenerationIdentity, HostProcessOwnershipSlot, PreparedArtifact, Session,
|
||||
};
|
||||
use lazaret_ignored_workspace::git_common_directory;
|
||||
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
|
||||
|
|
@ -21,7 +21,6 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
|
||||
build_sandbox_bundle_with_workspace,
|
||||
|
|
@ -79,9 +78,6 @@ pub struct ColdStartInputs<'a> {
|
|||
pub prepared_artifact_authority: &'a File,
|
||||
pub prepared_artifact_label: &'a Path,
|
||||
pub root_process: RootProcess,
|
||||
pub resolver_addresses: Vec<IpAddress>,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
pub reachability: ReachabilityExpectation,
|
||||
pub container_id: ContainerId,
|
||||
pub invoking_uid: u32,
|
||||
pub escalation_client: &'a File,
|
||||
|
|
@ -142,8 +138,6 @@ pub struct ColdStart<'a> {
|
|||
runtime_launch: Option<RuntimeSupervisorLaunch>,
|
||||
sandbox_bundle: SandboxBundle,
|
||||
network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
|
||||
network_probe: NetworkProbeRequest,
|
||||
reachability: ReachabilityExpectation,
|
||||
container_id: ContainerId,
|
||||
runtime_root: PathBuf,
|
||||
bundle_digest: BundleDigest,
|
||||
|
|
@ -204,7 +198,7 @@ impl<'a> ColdStart<'a> {
|
|||
staging
|
||||
.write_bundle(sandbox_bundle.specification_json())
|
||||
.map_err(|error| failure(format!("bundle staging: {error}")))?;
|
||||
let network_policy = derive_ipv4_policy(inputs.configuration, inputs.resolver_addresses)
|
||||
let network_policy = derive_ipv4_policy(inputs.configuration)
|
||||
.map_err(|error| failure(format!("network policy: {error}")))?;
|
||||
let ownership = ProcessOwnershipContext {
|
||||
domain_id: inputs.generation.domain_id,
|
||||
|
|
@ -263,8 +257,6 @@ impl<'a> ColdStart<'a> {
|
|||
runtime_launch: Some(runtime_launch),
|
||||
sandbox_bundle,
|
||||
network_policy,
|
||||
network_probe: inputs.network_probe,
|
||||
reachability: inputs.reachability,
|
||||
container_id: inputs.container_id,
|
||||
runtime_root: runtime_domain.join("runsc"),
|
||||
bundle_digest,
|
||||
|
|
@ -366,6 +358,7 @@ impl<'a> ColdStart<'a> {
|
|||
self.container_id,
|
||||
current_process_identity().map_err(failure)?,
|
||||
self.sandbox_bundle.command(),
|
||||
self.configuration.ephemeral_environment().clone(),
|
||||
self.runtime_root.clone(),
|
||||
)
|
||||
.map_err(failure)?;
|
||||
|
|
@ -383,8 +376,6 @@ impl<'a> ColdStart<'a> {
|
|||
.ok_or_else(|| ColdStartError("cold start was already consumed".into()))?,
|
||||
sandbox_bundle: &self.sandbox_bundle,
|
||||
network_policy: &self.network_policy,
|
||||
network_probe: self.network_probe.clone(),
|
||||
reachability: &self.reachability,
|
||||
container_id: self.container_id,
|
||||
runtime_root: self.runtime_root.clone(),
|
||||
bundle_digest: self.bundle_digest,
|
||||
|
|
@ -393,6 +384,7 @@ impl<'a> ColdStart<'a> {
|
|||
RunscNonInteractiveExecutor::from_run_command(
|
||||
self.sandbox_bundle.command(),
|
||||
self.container_id,
|
||||
self.configuration.ephemeral_environment().clone(),
|
||||
)
|
||||
.map_err(failure)?,
|
||||
),
|
||||
|
|
@ -414,9 +406,6 @@ fn require_inputs(inputs: &ColdStartInputs<'_>) -> Result<(), ColdStartError> {
|
|||
if inputs.generation.domain_id != inputs.configuration.domain_id()
|
||||
|| inputs.generation.configuration_fingerprint
|
||||
!= inputs.configuration.configuration_fingerprint()
|
||||
|| inputs.network_probe.domain_id != inputs.generation.domain_id
|
||||
|| inputs.network_probe.generation_id != inputs.generation.generation_id
|
||||
|| inputs.resolver_addresses.is_empty()
|
||||
|| inputs.invoking_uid != rustix::process::getuid().as_raw()
|
||||
{
|
||||
return Err(ColdStartError("cold-start inputs disagree".into()));
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue