Require explicit canonical network readiness targets

This commit is contained in:
ms2k 2026-07-16 20:41:13 +09:00
commit fbd332c31f
8 changed files with 339 additions and 12 deletions

View file

@ -12,8 +12,10 @@ use sha2::{Digest, Sha256};
use crate::identity::{ConfigurationFingerprint, DeviceId, DomainId, HostPath, InodeId, MountId}; use crate::identity::{ConfigurationFingerprint, DeviceId, DomainId, HostPath, InodeId, MountId};
use crate::records::{ use crate::records::{
ConfigurationFingerprintRecordV1, EnvironmentReference, MailboxRequest, ProfileConfigurationV1, ConfigurationFingerprintRecordV1, EnvironmentReference, MailboxRequest,
ResolvedAutoApprovalRule, ResolvedVolume, TRUSTED_HOST_CONFIGURATION_SCHEMA_ID, NetworkProbeTargetConfigurationV1, NetworkReadinessConfigurationV1, ProfileConfigurationV1,
ResolvedAutoApprovalRule, ResolvedDnsProbeTargetV1, ResolvedNetworkProbeTargetV1,
ResolvedNetworkReadinessV1, ResolvedVolume, TRUSTED_HOST_CONFIGURATION_SCHEMA_ID,
TrustedHostConfigurationV1, VolumeAccess, VolumeKind, TrustedHostConfigurationV1, VolumeAccess, VolumeKind,
}; };
@ -656,6 +658,11 @@ impl ResolvedDomainConfiguration {
&self.effective_deny_prefixes &self.effective_deny_prefixes
} }
#[must_use]
pub const fn network_readiness(&self) -> &ResolvedNetworkReadinessV1 {
&self.fingerprint_record.network_readiness
}
#[must_use] #[must_use]
pub const fn ephemeral_environment(&self) -> &BTreeMap<String, EnvironmentValue> { pub const fn ephemeral_environment(&self) -> &BTreeMap<String, EnvironmentValue> {
&self.ephemeral_environment &self.ephemeral_environment
@ -843,6 +850,11 @@ pub fn load_trusted_configuration(
)?; )?;
let (effective_allow_prefixes, effective_deny_prefixes) = let (effective_allow_prefixes, effective_deny_prefixes) =
resolve_network(&configuration, selected)?; 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 environment_references = resolve_environment_references(selected)?;
let mut volumes = Vec::with_capacity(selected.volumes.len()); let mut volumes = Vec::with_capacity(selected.volumes.len());
@ -1045,6 +1057,7 @@ pub fn load_trusted_configuration(
.iter() .iter()
.map(|prefix| (prefix.address, prefix.prefix_length)) .map(|prefix| (prefix.address, prefix.prefix_length))
.collect(), .collect(),
network_readiness,
escalation_enabled: selected.escalation.enabled, escalation_enabled: selected.escalation.enabled,
auto_approval_rules: resolved_auto_approval_records, auto_approval_rules: resolved_auto_approval_records,
}; };
@ -1134,6 +1147,34 @@ pub fn derive_configuration_fingerprint(
.iter() .iter()
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())), .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_bool(&mut preimage, record.escalation_enabled);
push_sorted_list( push_sorted_list(
&mut preimage, &mut preimage,
@ -1739,6 +1780,173 @@ fn resolve_network(
Ok((allow.into_iter().collect(), deny.into_iter().collect())) Ok((allow.into_iter().collect(), deny.into_iter().collect()))
} }
const MAX_READINESS_TARGETS: usize = 32;
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)
}
fn parse_ipv4_prefix(value: &str) -> Result<Ipv4Prefix, ConfigurationError> { fn parse_ipv4_prefix(value: &str) -> Result<Ipv4Prefix, ConfigurationError> {
let (address, prefix) = value.split_once('/').ok_or_else(|| { let (address, prefix) = value.split_once('/').ok_or_else(|| {
ConfigurationError::new( ConfigurationError::new(

View file

@ -22,6 +22,31 @@ pub struct NetworkConfigurationV1 {
pub default_deny: Vec<String>, 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)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum VolumeKind { pub enum VolumeKind {
@ -81,6 +106,8 @@ pub struct ProfileConfigurationV1 {
#[serde(default)] #[serde(default)]
pub deny_egress: Vec<String>, pub deny_egress: Vec<String>,
#[serde(default)] #[serde(default)]
pub readiness: NetworkReadinessConfigurationV1,
#[serde(default)]
pub env: BTreeMap<String, String>, pub env: BTreeMap<String, String>,
#[serde(default)] #[serde(default)]
pub volumes: Vec<VolumeConfigurationV1>, pub volumes: Vec<VolumeConfigurationV1>,
@ -194,6 +221,26 @@ pub struct ConfigurationFingerprintRecordV1 {
pub volumes: Vec<ResolvedVolume>, pub volumes: Vec<ResolvedVolume>,
pub effective_allow_prefixes: Vec<([u8; 4], u8)>, pub effective_allow_prefixes: Vec<([u8; 4], u8)>,
pub effective_deny_prefixes: Vec<([u8; 4], u8)>, pub effective_deny_prefixes: Vec<([u8; 4], u8)>,
pub network_readiness: ResolvedNetworkReadinessV1,
pub escalation_enabled: bool, pub escalation_enabled: bool,
pub auto_approval_rules: Vec<ResolvedAutoApprovalRule>, 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,
}

View file

@ -13,13 +13,14 @@ use super::{
PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact, PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact,
PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion, PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion,
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity, ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity,
PublicationOwnership, ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream, PublicationOwnership, ResolvedAutoApprovalRule, ResolvedNetworkReadinessV1, ResolvedVolume,
SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit, Resolver, ResultStream, SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding,
SessionExitOutcome, SessionRegistrationRequest, SessionRegistrationResponse, SessionClose, SessionExit, SessionExitOutcome, SessionRegistrationRequest,
SessionRequestCapability, SessionTerminal, SessionTerminalEvent, SessionTerminalMessage, SessionRegistrationResponse, SessionRequestCapability, SessionTerminal, SessionTerminalEvent,
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership, SessionTerminalMessage, SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership,
StorageCatalog, StoreMetadata, TerminalInput, TerminalOutput, TerminalResize, TerminalSignal, StateAnchorOwnership, StorageCatalog, StoreMetadata, TerminalInput, TerminalOutput,
TerminalSignalKind, TlsCredential, TlsRole, TrustManifest, TrustedHostConfigurationV1, TerminalResize, TerminalSignal, TerminalSignalKind, TlsCredential, TlsRole, TrustManifest,
TrustedHostConfigurationV1,
}; };
pub trait SchemaType { pub trait SchemaType {
@ -133,6 +134,7 @@ schema_types!(
"EnvironmentReference" => EnvironmentReference, "EnvironmentReference" => EnvironmentReference,
"ResolvedVolume" => ResolvedVolume, "ResolvedVolume" => ResolvedVolume,
"ResolvedAutoApprovalRule" => ResolvedAutoApprovalRule, "ResolvedAutoApprovalRule" => ResolvedAutoApprovalRule,
"ResolvedNetworkReadinessV1" => ResolvedNetworkReadinessV1,
"DomainStore" => DomainStore, "DomainStore" => DomainStore,
"StorageCatalog" => StorageCatalog, "StorageCatalog" => StorageCatalog,
"PrepareStageState" => PrepareStageState; "PrepareStageState" => PrepareStageState;

View file

@ -13,6 +13,7 @@ use lazaret_core::configuration::{
use lazaret_core::identity::HostPath; use lazaret_core::identity::HostPath;
use lazaret_core::records::{ use lazaret_core::records::{
ConfigurationFingerprintRecordV1, EnvironmentReference, ResolvedAutoApprovalRule, ConfigurationFingerprintRecordV1, EnvironmentReference, ResolvedAutoApprovalRule,
ResolvedDnsProbeTargetV1, ResolvedNetworkProbeTargetV1, ResolvedNetworkReadinessV1,
ResolvedVolume, VolumeAccess, VolumeKind, ResolvedVolume, VolumeAccess, VolumeKind,
}; };
use static_assertions::assert_not_impl_any; use static_assertions::assert_not_impl_any;
@ -107,6 +108,11 @@ volumes = [
{{ host = "{{profile_dir}}/agent-state", container = "/root/.local/state/agent", kind = "directory", access = "read-write", create = true }}, {{ 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] [profiles.default.escalation]
enabled = false enabled = false
"#, "#,
@ -698,6 +704,20 @@ fn digest_vectors_are_framed_and_order_independent() {
}], }],
effective_allow_prefixes: vec![([8, 8, 8, 8], 32), ([1, 1, 1, 1], 32)], effective_allow_prefixes: vec![([8, 8, 8, 8], 32), ([1, 1, 1, 1], 32)],
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)], 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, escalation_enabled: true,
auto_approval_rules: vec![ResolvedAutoApprovalRule { auto_approval_rules: vec![ResolvedAutoApprovalRule {
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()), canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
@ -709,9 +729,9 @@ fn digest_vectors_are_framed_and_order_independent() {
assert_eq!( assert_eq!(
first.as_bytes(), first.as_bytes(),
&[ &[
0xd3, 0xba, 0xef, 0x91, 0x21, 0x0f, 0x98, 0x31, 0x6c, 0xea, 0x34, 0xfb, 0x41, 0xbd, 0x22, 0x57, 0x11, 0xe7, 0xc9, 0x11, 0xba, 0x8e, 0x08, 0x11, 0xae, 0x56, 0x5b, 0x7d,
0x6c, 0xa5, 0x07, 0xf0, 0xb8, 0x9b, 0x90, 0x77, 0xaf, 0x0a, 0x0e, 0xd0, 0x6a, 0xd6, 0x89, 0xb0, 0x73, 0x48, 0xaa, 0x73, 0x87, 0x19, 0x5e, 0xcd, 0xf8, 0xfa, 0xd1, 0x48,
0x46, 0xc7, 0x2d, 0xbf, 0xc1, 0x5b, 0x90, 0x57,
] ]
); );
record.persistent_dirs.reverse(); record.persistent_dirs.reverse();

View file

@ -175,6 +175,7 @@ fn trusted_host_configuration_postcard_dispatch_rejects_unknown_schema() {
persistent_dirs: vec![], persistent_dirs: vec![],
allow_egress: vec![], allow_egress: vec![],
deny_egress: vec![], deny_egress: vec![],
readiness: NetworkReadinessConfigurationV1::default(),
env: BTreeMap::new(), env: BTreeMap::new(),
volumes: vec![], volumes: vec![],
escalation: EscalationConfigurationV1::default(), escalation: EscalationConfigurationV1::default(),

View file

@ -514,6 +514,20 @@ image = "example.invalid/agent:v1"
}], }],
effective_allow_prefixes: vec![([1, 1, 1, 1], 32)], effective_allow_prefixes: vec![([1, 1, 1, 1], 32)],
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)], 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, escalation_enabled: true,
auto_approval_rules: vec![ResolvedAutoApprovalRule { auto_approval_rules: vec![ResolvedAutoApprovalRule {
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()), canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
@ -548,6 +562,23 @@ image = "example.invalid/agent:v1"
}, },
&mut seen, &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,
);
assert_eq!(seen.len(), IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES.len()); assert_eq!(seen.len(), IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES.len());
assert!( assert!(

View file

@ -17,5 +17,10 @@ volumes = [
{ host = "{profile_dir}/agent-state", container = "/root/.local/state/agent", kind = "directory", access = "read-write", create = true }, { host = "{profile_dir}/agent-state", container = "/root/.local/state/agent", kind = "directory", access = "read-write", create = true },
] ]
[profiles.default.readiness]
denied = [{ address = "10.20.30.40", port = 443 }]
permitted = [{ address = "203.0.113.10", port = 443 }]
dns = [{ resolver = "198.51.100.53", query = "example.invalid" }]
[profiles.default.escalation] [profiles.default.escalation]
enabled = false enabled = false

View file

@ -36,9 +36,13 @@ tables, field aliases, and unknown enumeration values.
| profile | `persistent_dirs` | optional list of sandbox paths; defaults empty | | profile | `persistent_dirs` | optional list of sandbox paths; defaults empty |
| profile | `allow_egress` | optional list of IPv4 prefixes; defaults empty | | profile | `allow_egress` | optional list of IPv4 prefixes; defaults empty |
| profile | `deny_egress` | optional list of IPv4 prefixes; defaults empty | | profile | `deny_egress` | optional list of IPv4 prefixes; defaults empty |
| profile | `readiness` | required at load for the selected profile; closed denied, permitted, and DNS target table |
| profile | `env` | optional map from sandbox environment name to `env:` reference; defaults empty | | profile | `env` | optional map from sandbox environment name to `env:` reference; defaults empty |
| profile | `volumes` | optional list of volume tables; defaults empty | | profile | `volumes` | optional list of volume tables; defaults empty |
| profile | `escalation` | optional table; defaults to disabled with no rules | | profile | `escalation` | optional table; defaults to disabled with no rules |
| `readiness` | `denied` | one to 32 unique `{ address = IPv4, port = nonzero }` targets denied by effective policy |
| `readiness` | `permitted` | one to 32 unique `{ address = IPv4, port = nonzero }` targets allowed by effective policy |
| `readiness` | `dns` | one to 32 unique `{ resolver = IPv4, query = strict DNS name }` targets |
| volume | `host` | required host path | | volume | `host` | required host path |
| volume | `container` | required sandbox path | | volume | `container` | required sandbox path |
| volume | `kind` | required: `file` or `directory` | | volume | `kind` | required: `file` or `directory` |
@ -148,6 +152,7 @@ classDiagram
volumes volumes
effective_allow_prefixes effective_allow_prefixes
effective_deny_prefixes effective_deny_prefixes
network_readiness
escalation_enabled escalation_enabled
auto_approval_rules auto_approval_rules
} }
@ -174,6 +179,13 @@ classDiagram
allow_extra_args allow_extra_args
} }
class ResolvedNetworkReadinessV1 {
<<serialized>>
denied
permitted
dns
}
class ConfinedPathPlan { class ConfinedPathPlan {
<<runtime>> <<runtime>>
retained_ancestor retained_ancestor
@ -190,6 +202,7 @@ classDiagram
ConfigurationFingerprintRecordV1 --> EnvironmentReference ConfigurationFingerprintRecordV1 --> EnvironmentReference
ConfigurationFingerprintRecordV1 --> ResolvedVolume ConfigurationFingerprintRecordV1 --> ResolvedVolume
ConfigurationFingerprintRecordV1 --> ResolvedAutoApprovalRule ConfigurationFingerprintRecordV1 --> ResolvedAutoApprovalRule
ConfigurationFingerprintRecordV1 --> ResolvedNetworkReadinessV1
ResolvedDomainConfiguration --> ConfinedPathPlan ResolvedDomainConfiguration --> ConfinedPathPlan
``` ```