refactor(isolation): make confirmation backend-neutral

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
Drew Newberry
2026-09-17 09:46:47 -07:00
parent c554589cab
commit 7cd18e1201
11 changed files with 375 additions and 189 deletions
Generated
+1
View File
@@ -4397,6 +4397,7 @@ dependencies = [
"openshell-core",
"rustix 1.1.4",
"serde",
"serde_json",
"tokio",
]
+6 -2
View File
@@ -69,8 +69,12 @@ replacement from granting authority.
3. `openshell-supervisor` loads policy and runtime settings from the gateway,
attaches to the sandbox, and verifies the driver's generation and evidence.
4. The sandbox installs its seccomp notification broker and Landlock baseline,
then reports measured confirmation. The supervisor must accept that evidence
before it sends the launch permit.
validates its mechanism-specific audit evidence, and reports backend-neutral
enforcement properties. The supervisor must accept those properties and
their immutable session and resource binding before it sends the launch
permit. Other isolation backends may establish the same properties with
different mechanisms and retain their detailed evidence in backend-owned
audit data.
5. The sandbox starts the canonical process through its single workload
launcher. The supervisor starts SSH and registers its gateway session.
6. Exec, signaling, PTY, DNS, TCP, and loopback-forwarding operations cross the
@@ -14,6 +14,7 @@ repository.workspace = true
openshell-core = { path = "../openshell-core", default-features = false }
async-trait = "0.1"
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
[target.'cfg(unix)'.dependencies]
@@ -16,7 +16,8 @@
//!
//! Each transition consumes the prior state by value (`self: Box<Self>`).
//! Trusted backend implementations construct confirmation through a validating
//! constructor; the supervisor cannot obtain a ready boundary without evidence.
//! constructor; the supervisor cannot obtain a ready boundary without confirmed
//! backend-neutral enforcement properties.
//! The supervisor holds no `match`/downcast on concrete backends: the
//! registry is the only lookup by `backend_name`, and everything past it is a
//! `Box<dyn _>` / `Arc<dyn _>`.
@@ -385,46 +386,6 @@ pub trait BoundBoundary: Send {
async fn confirm(self: Box<Self>) -> Result<ConfirmedBoundary, BackendError>;
}
/// Capability masks measured from `/proc/<pid>/status`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityEvidence {
pub inheritable: u64,
pub permitted: u64,
pub effective: u64,
pub bounding: u64,
pub ambient: u64,
}
impl CapabilityEvidence {
/// True only when every Linux capability set is empty.
#[must_use]
pub const fn is_empty(self) -> bool {
self.inheritable == 0
&& self.permitted == 0
&& self.effective == 0
&& self.bounding == 0
&& self.ambient == 0
}
}
/// Active seccomp notification and socket-broker evidence.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "each independently measured kernel operation is reported explicitly"
)]
pub struct SeccompEvidence {
pub new_listener: bool,
pub notification_round_trip: bool,
pub id_validation: bool,
pub addfd_send: bool,
pub retained_socket_operation: bool,
pub proc_fd_identity: bool,
pub task_memory_read: bool,
pub task_memory_write: bool,
pub cancellation: bool,
}
/// Driver-owned evidence that the mandatory outer network fence is installed.
///
/// The sandbox cannot observe the Docker daemon, Kubernetes API, or VM device
@@ -512,29 +473,67 @@ impl DriverFenceEvidence {
}
}
/// Measured sandbox-owned evidence produced before agent launch.
/// A backend-neutral security property established before agent launch.
///
/// `mechanism` is diagnostic and audit metadata. It never authorizes launch;
/// the registered backend is responsible for validating its mechanism-specific
/// evidence before setting `enforced`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "confirmation preserves independently measured security results"
)]
pub struct SandboxConfirmEvidence {
pub struct EnforcedProperty {
pub enforced: bool,
pub mechanism: String,
}
impl EnforcedProperty {
#[must_use]
pub fn new(enforced: bool, mechanism: impl Into<String>) -> Self {
Self {
enforced,
mechanism: mechanism.into(),
}
}
fn validate(&self, name: &str) -> Result<(), BackendError> {
if self.enforced && !self.mechanism.trim().is_empty() {
Ok(())
} else {
Err(BackendError::Confirm(format!(
"{name} is not enforced or has no declared mechanism"
)))
}
}
}
/// Security properties every isolation backend establishes before launch.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoundaryProperties {
pub filesystem_confinement: EnforcedProperty,
pub egress_interception: EnforcedProperty,
pub request_attribution: EnforcedProperty,
pub privilege_floor: EnforcedProperty,
}
impl BoundaryProperties {
fn validate(&self) -> Result<(), BackendError> {
self.filesystem_confinement
.validate("filesystem confinement")?;
self.egress_interception.validate("egress interception")?;
self.request_attribution.validate("request attribution")?;
self.privilege_floor.validate("privilege floor")
}
}
/// Per-boundary confirmation produced before agent launch.
///
/// Common validation binds the confirmation to the admitted workload and
/// checks backend-neutral properties. `backend_audit` remains opaque to this
/// crate; the registered backend owns its schema and validates it before
/// constructing [`ConfirmedBoundary`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoundaryConfirmation {
pub generation: String,
pub identity: ResolvedWorkloadIdentity,
pub capabilities: CapabilityEvidence,
pub no_new_privileges: bool,
pub sandbox_dumpable: bool,
pub child_dumpable: bool,
pub core_limit_zero: bool,
pub native_architecture: String,
pub kernel_release: String,
pub seccomp: SeccompEvidence,
pub landlock_abi: u32,
pub landlock_allow_deny: bool,
pub udp_dns_round_trip: bool,
pub tcp_dns_round_trip: bool,
pub tcp_allow_round_trip: bool,
pub tcp_deny_round_trip: bool,
pub properties: BoundaryProperties,
pub authenticated_supervisor: bool,
pub session_id: SandboxSessionId,
pub driver_fence: DriverFenceEvidence,
@@ -542,33 +541,15 @@ pub struct SandboxConfirmEvidence {
/// Sandbox Runtime exits.
pub runtime_exit_terminates_workload: bool,
pub resource_claims: BTreeMap<String, String>,
pub backend_audit: serde_json::Value,
}
impl SandboxConfirmEvidence {
/// Validate the security-critical evidence required before launch.
impl BoundaryConfirmation {
/// Validate common security properties and immutable launch binding.
pub fn validate(&self, expected: &ResolvedWorkloadIdentity) -> Result<(), BackendError> {
self.driver_fence.validate()?;
self.properties.validate()?;
let complete = &self.identity == expected
&& self.capabilities.is_empty()
&& self.no_new_privileges
&& !self.sandbox_dumpable
&& self.child_dumpable
&& self.core_limit_zero
&& self.seccomp.new_listener
&& self.seccomp.notification_round_trip
&& self.seccomp.id_validation
&& self.seccomp.addfd_send
&& self.seccomp.retained_socket_operation
&& self.seccomp.proc_fd_identity
&& self.seccomp.task_memory_read
&& self.seccomp.task_memory_write
&& self.seccomp.cancellation
&& self.landlock_abi >= 3
&& self.landlock_allow_deny
&& self.udp_dns_round_trip
&& self.tcp_dns_round_trip
&& self.tcp_allow_round_trip
&& self.tcp_deny_round_trip
&& self.authenticated_supervisor
&& self.runtime_exit_terminates_workload
&& !self.generation.is_empty();
@@ -576,41 +557,45 @@ impl SandboxConfirmEvidence {
Ok(())
} else {
Err(BackendError::Confirm(
"sandbox confirmation evidence is incomplete or mismatched".to_string(),
"boundary confirmation is incomplete or mismatched".to_string(),
))
}
}
}
/// Ready boundary paired with the evidence measured by `confirm`.
/// Ready boundary paired with the confirmation established by `confirm`.
pub struct ConfirmedBoundary {
boundary: Box<dyn ReadyBoundary>,
evidence: SandboxConfirmEvidence,
confirmation: BoundaryConfirmation,
}
impl ConfirmedBoundary {
/// Construct confirmation after checking measured evidence against the
/// immutable identity admitted at attach time.
/// Construct confirmation after checking backend-neutral properties and
/// immutable identity binding.
///
/// Backend implementations are trusted to collect this evidence and bind
/// it to their resource. This constructor enforces the common requirements
/// without requiring those implementations to live in the interface crate.
/// Backend implementations are trusted to validate their audit evidence and
/// bind this confirmation to their resource. This constructor enforces the
/// common requirements without requiring those implementations to live in
/// the interface crate.
///
/// # Errors
///
/// Returns an error if evidence is incomplete or the identity does not match.
/// Returns an error if confirmation is incomplete or the identity does not match.
pub fn try_new(
boundary: Box<dyn ReadyBoundary>,
evidence: SandboxConfirmEvidence,
confirmation: BoundaryConfirmation,
expected: &ResolvedWorkloadIdentity,
) -> Result<Self, BackendError> {
evidence.validate(expected)?;
Ok(Self { boundary, evidence })
confirmation.validate(expected)?;
Ok(Self {
boundary,
confirmation,
})
}
/// Return the measured evidence carried by this confirmed state.
pub fn evidence(&self) -> &SandboxConfirmEvidence {
&self.evidence
/// Return the record carried by this confirmed state.
pub fn confirmation(&self) -> &BoundaryConfirmation {
&self.confirmation
}
/// Consume confirmation and advance to the sole launch-capable state.
@@ -21,8 +21,10 @@
//! `start_agent` -> Running. Nothing untrusted runs inside the boundary until it
//! is confirmed ready. This is enforced *by construction*: each transition
//! consumes the prior state by value. Trusted backends construct confirmation
//! through [`contract::ConfirmedBoundary::try_new`], which checks common evidence
//! before the supervisor can obtain a [`contract::ReadyBoundary`].
//! through [`contract::ConfirmedBoundary::try_new`], which checks common
//! enforcement properties and immutable launch binding before the supervisor
//! can obtain a [`contract::ReadyBoundary`]. Mechanism-specific evidence stays
//! owned by the backend that can interpret it.
//!
//! [`AgentSpec`] is shared between the workload definition the supervisor
//! submits and the [`contract::SandboxContext`] that `attach` binds to a
@@ -239,7 +239,7 @@ impl<K: MockKind> BoundBoundary for MockBound<K> {
async fn confirm(self: Box<Self>) -> Result<ConfirmedBoundary, BackendError> {
ConfirmedBoundary::try_new(
Box::new(MockReady::<K> { _k: PhantomData }),
confirmation_evidence(),
confirmation(),
&workload_identity(),
)
}
@@ -368,40 +368,16 @@ fn workload_identity() -> ResolvedWorkloadIdentity {
.unwrap()
}
fn confirmation_evidence() -> SandboxConfirmEvidence {
SandboxConfirmEvidence {
fn confirmation() -> BoundaryConfirmation {
BoundaryConfirmation {
generation: "generation-1".to_string(),
identity: workload_identity(),
capabilities: CapabilityEvidence {
inheritable: 0,
permitted: 0,
effective: 0,
bounding: 0,
ambient: 0,
properties: BoundaryProperties {
filesystem_confinement: EnforcedProperty::new(true, "mock-filesystem"),
egress_interception: EnforcedProperty::new(true, "mock-egress"),
request_attribution: EnforcedProperty::new(true, "mock-attribution"),
privilege_floor: EnforcedProperty::new(true, "mock-privilege-floor"),
},
no_new_privileges: true,
sandbox_dumpable: false,
child_dumpable: true,
core_limit_zero: true,
native_architecture: std::env::consts::ARCH.to_string(),
kernel_release: "test".to_string(),
seccomp: SeccompEvidence {
new_listener: true,
notification_round_trip: true,
id_validation: true,
addfd_send: true,
retained_socket_operation: true,
proc_fd_identity: true,
task_memory_read: true,
task_memory_write: true,
cancellation: true,
},
landlock_abi: 3,
landlock_allow_deny: true,
udp_dns_round_trip: true,
tcp_dns_round_trip: true,
tcp_allow_round_trip: true,
tcp_deny_round_trip: true,
authenticated_supervisor: true,
session_id: SandboxSessionId::new(),
driver_fence: DriverFenceEvidence::Vm {
@@ -410,6 +386,7 @@ fn confirmation_evidence() -> SandboxConfirmEvidence {
},
runtime_exit_terminates_workload: true,
resource_claims: BTreeMap::new(),
backend_audit: serde_json::json!({"backend": "mock"}),
}
}
@@ -458,7 +435,7 @@ async fn drive(
let _ingress = bound.network_mediation_source();
assert_eq!(bound.host_gateway_ip(), None);
let confirmed = bound.confirm().await?;
confirmed.evidence().validate(&sandbox_ctx().identity)?;
confirmed.confirmation().validate(&sandbox_ctx().identity)?;
confirmed.into_boundary().start_agent().await
}
@@ -536,12 +513,12 @@ async fn one_driver_runs_both_backends() {
}
#[test]
fn confirmation_constructor_rejects_incomplete_evidence() {
let mut evidence = confirmation_evidence();
evidence.seccomp.cancellation = false;
fn confirmation_constructor_rejects_unenforced_property() {
let mut confirmation = confirmation();
confirmation.properties.egress_interception.enforced = false;
let result = ConfirmedBoundary::try_new(
Box::new(MockReady::<Primary> { _k: PhantomData }),
evidence,
confirmation,
&workload_identity(),
);
assert!(matches!(result, Err(BackendError::Confirm(_))));
@@ -559,7 +536,7 @@ fn confirmation_constructor_rejects_another_workload_identity() {
.unwrap();
let result = ConfirmedBoundary::try_new(
Box::new(MockReady::<Primary> { _k: PhantomData }),
confirmation_evidence(),
confirmation(),
&expected,
);
assert!(matches!(result, Err(BackendError::Confirm(_))));
@@ -842,16 +819,16 @@ fn workload_identity_rejects_root_and_normalizes_groups() {
}
#[test]
fn confirmation_evidence_rejects_identity_or_posture_drift() {
fn confirmation_rejects_identity_or_property_drift() {
let expected = workload_identity();
let evidence = confirmation_evidence();
evidence.validate(&expected).unwrap();
let baseline = confirmation();
baseline.validate(&expected).unwrap();
let mut drifted = confirmation_evidence();
drifted.capabilities.effective = 1;
let mut drifted = confirmation();
drifted.properties.privilege_floor.enforced = false;
assert!(drifted.validate(&expected).is_err());
let mut unmanaged = confirmation_evidence();
let mut unmanaged = confirmation();
unmanaged.runtime_exit_terminates_workload = false;
assert!(unmanaged.validate(&expected).is_err());
@@ -863,7 +840,7 @@ fn confirmation_evidence_rejects_identity_or_posture_drift() {
"sha256:test".into(),
)
.unwrap();
assert!(evidence.validate(&different).is_err());
assert!(baseline.validate(&different).is_err());
}
// ---------------------------------------------------------------------------
@@ -23,8 +23,9 @@ use openshell_core::policy::{
use openshell_isolation_interface::AgentSpec;
use openshell_isolation_interface::contract::Sha256Digest;
use openshell_isolation_interface::contract::{
BackendDescriptor, BackendError, BinaryIdentity, BoundaryExitStatus, BoundarySignal,
DriverFenceEvidence, ExecSpec, ResolveError, SandboxConfirmEvidence,
BackendDescriptor, BackendError, BinaryIdentity, BoundaryConfirmation, BoundaryExitStatus,
BoundaryProperties, BoundarySignal, DriverFenceEvidence, EnforcedProperty, ExecSpec,
ResolveError,
};
use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose};
use serde::de::DeserializeOwned;
@@ -42,6 +43,145 @@ pub const STREAM_STDIN_CLOSED: u8 = 4;
pub const STREAM_NETWORK_DECISION: u8 = 5;
pub const MAX_STREAM_FRAME_BYTES: usize = 64 * 1024;
/// Capability masks measured from `/proc/<pid>/status` by the `OpenShell`
/// co-located runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityEvidence {
pub inheritable: u64,
pub permitted: u64,
pub effective: u64,
pub bounding: u64,
pub ambient: u64,
}
impl CapabilityEvidence {
#[must_use]
pub const fn is_empty(self) -> bool {
self.inheritable == 0
&& self.permitted == 0
&& self.effective == 0
&& self.bounding == 0
&& self.ambient == 0
}
}
/// Active seccomp notification and socket-broker measurements specific to the
/// `OpenShell` co-located runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "each independently measured kernel operation is reported explicitly"
)]
pub struct SeccompEvidence {
pub new_listener: bool,
pub notification_round_trip: bool,
pub id_validation: bool,
pub addfd_send: bool,
pub retained_socket_operation: bool,
pub proc_fd_identity: bool,
pub task_memory_read: bool,
pub task_memory_write: bool,
pub cancellation: bool,
}
/// Mechanism-specific audit evidence for the `OpenShell` co-located runtime.
///
/// This schema belongs to this backend rather than the generic isolation
/// interface. The host-side backend validates it before constructing a
/// backend-neutral `ConfirmedBoundary`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "audit evidence preserves independently measured security results"
)]
pub struct OpenShellSandboxAuditEvidence {
pub capabilities: CapabilityEvidence,
pub no_new_privileges: bool,
pub sandbox_dumpable: bool,
pub child_dumpable: bool,
pub core_limit_zero: bool,
pub native_architecture: String,
pub kernel_release: String,
pub seccomp: SeccompEvidence,
pub landlock_abi: u32,
pub landlock_allow_deny: bool,
pub udp_dns_round_trip: bool,
pub tcp_dns_round_trip: bool,
pub tcp_allow_round_trip: bool,
pub tcp_deny_round_trip: bool,
}
impl OpenShellSandboxAuditEvidence {
/// Validate the complete mechanism-specific posture required by this backend.
pub fn validate(&self) -> Result<(), BackendError> {
let complete = self.capabilities.is_empty()
&& self.no_new_privileges
&& !self.sandbox_dumpable
&& self.child_dumpable
&& self.core_limit_zero
&& !self.native_architecture.is_empty()
&& !self.kernel_release.is_empty()
&& self.seccomp.new_listener
&& self.seccomp.notification_round_trip
&& self.seccomp.id_validation
&& self.seccomp.addfd_send
&& self.seccomp.retained_socket_operation
&& self.seccomp.proc_fd_identity
&& self.seccomp.task_memory_read
&& self.seccomp.task_memory_write
&& self.seccomp.cancellation
&& self.landlock_abi >= 3
&& self.landlock_allow_deny
&& self.udp_dns_round_trip
&& self.tcp_dns_round_trip
&& self.tcp_allow_round_trip
&& self.tcp_deny_round_trip;
if complete {
Ok(())
} else {
Err(BackendError::Confirm(
"OpenShell sandbox audit evidence is incomplete".to_string(),
))
}
}
/// Project backend measurements into the common property contract.
#[must_use]
pub fn properties(&self) -> BoundaryProperties {
BoundaryProperties {
filesystem_confinement: EnforcedProperty::new(
self.landlock_abi >= 3 && self.landlock_allow_deny,
format!("landlock-v{}", self.landlock_abi),
),
egress_interception: EnforcedProperty::new(
self.seccomp.new_listener
&& self.seccomp.notification_round_trip
&& self.seccomp.addfd_send
&& self.udp_dns_round_trip
&& self.tcp_dns_round_trip
&& self.tcp_allow_round_trip
&& self.tcp_deny_round_trip,
"seccomp-notify",
),
request_attribution: EnforcedProperty::new(
self.seccomp.id_validation
&& self.seccomp.proc_fd_identity
&& self.seccomp.task_memory_read
&& self.seccomp.task_memory_write,
"seccomp-notify-procfs",
),
privilege_floor: EnforcedProperty::new(
self.capabilities.is_empty()
&& self.no_new_privileges
&& !self.sandbox_dumpable
&& self.child_dumpable
&& self.core_limit_zero,
"linux-capability-free",
),
}
}
}
/// Ephemeral identity of the supervisor process that owns one sandbox runtime.
///
/// The supervisor generates this value in memory and presents it on every
@@ -704,8 +844,9 @@ pub enum Response {
snapshot: SessionSnapshotWire,
},
Confirmed {
/// Measured capability-free posture produced before workload launch.
evidence: Box<SandboxConfirmEvidence>,
/// Backend-neutral properties and backend-owned audit evidence produced
/// before workload launch.
confirmation: Box<BoundaryConfirmation>,
},
Started {
process_id: String,
@@ -1184,6 +1325,61 @@ pub enum FrameError {
mod tests {
use super::*;
fn complete_audit_evidence() -> OpenShellSandboxAuditEvidence {
OpenShellSandboxAuditEvidence {
capabilities: CapabilityEvidence {
inheritable: 0,
permitted: 0,
effective: 0,
bounding: 0,
ambient: 0,
},
no_new_privileges: true,
sandbox_dumpable: false,
child_dumpable: true,
core_limit_zero: true,
native_architecture: "x86_64".to_string(),
kernel_release: "6.12.0".to_string(),
seccomp: SeccompEvidence {
new_listener: true,
notification_round_trip: true,
id_validation: true,
addfd_send: true,
retained_socket_operation: true,
proc_fd_identity: true,
task_memory_read: true,
task_memory_write: true,
cancellation: true,
},
landlock_abi: 6,
landlock_allow_deny: true,
udp_dns_round_trip: true,
tcp_dns_round_trip: true,
tcp_allow_round_trip: true,
tcp_deny_round_trip: true,
}
}
#[test]
fn openshell_audit_evidence_projects_backend_neutral_properties() {
let audit = complete_audit_evidence();
audit.validate().unwrap();
let properties = audit.properties();
assert!(properties.filesystem_confinement.enforced);
assert_eq!(properties.filesystem_confinement.mechanism, "landlock-v6");
assert!(properties.egress_interception.enforced);
assert!(properties.request_attribution.enforced);
assert!(properties.privilege_floor.enforced);
}
#[test]
fn openshell_audit_evidence_rejects_mechanism_failure() {
let mut audit = complete_audit_evidence();
audit.seccomp.addfd_send = false;
assert!(audit.validate().is_err());
assert!(!audit.properties().egress_interception.enforced);
}
#[test]
fn binary_identity_wire_rejects_ambiguous_or_invalid_shapes() {
for encoded in [
+29 -16
View File
@@ -293,19 +293,29 @@ impl BoundBoundary for RemoteBound {
async fn confirm(self: Box<Self>) -> Result<ConfirmedBoundary, BackendError> {
let response = self.client.call_idempotent(Request::Confirm).await?;
let Response::Confirmed { evidence } = response else {
return Err(unexpected_response("confirmed_with_evidence", &response));
let Response::Confirmed { confirmation } = response else {
return Err(unexpected_response("confirmed", &response));
};
if evidence.generation != self.generation
|| evidence.session_id != self.session_id
|| evidence.resource_claims != self.resource_claims
|| evidence.driver_fence != self.driver_fence
if confirmation.generation != self.generation
|| confirmation.session_id != self.session_id
|| confirmation.resource_claims != self.resource_claims
|| confirmation.driver_fence != self.driver_fence
{
return Err(BackendError::Confirm(
"sandbox confirmation generation, session, resource claims, or driver fence do not match runtime descriptor"
.to_string(),
));
}
let audit: crate::boundary_protocol::OpenShellSandboxAuditEvidence =
serde_json::from_value(confirmation.backend_audit.clone()).map_err(|error| {
BackendError::Confirm(format!("decode OpenShell sandbox audit evidence: {error}"))
})?;
audit.validate()?;
if confirmation.properties != audit.properties() {
return Err(BackendError::Confirm(
"sandbox confirmation properties do not match OpenShell audit evidence".to_string(),
));
}
self.client.start_credential_monitor();
ConfirmedBoundary::try_new(
Box::new(RemoteReady {
@@ -316,7 +326,7 @@ impl BoundBoundary for RemoteBound {
ca_file_paths: self.ca_file_paths,
provider_credentials: self.provider_credentials,
}),
*evidence,
*confirmation,
&self.identity,
)
}
@@ -1939,7 +1949,7 @@ mod tests {
},
},
Request::Confirm => Response::Confirmed {
evidence: Box::new(test_confirmation_evidence()),
confirmation: Box::new(test_confirmation()),
},
Request::OpenMediation if mediation_ready => Response::MediationReady,
Request::OpenMediation => Response::Error {
@@ -2437,12 +2447,9 @@ mod tests {
}
}
fn test_confirmation_evidence()
-> openshell_isolation_interface::contract::SandboxConfirmEvidence {
openshell_isolation_interface::contract::SandboxConfirmEvidence {
generation: "test-generation".to_string(),
identity: sandbox().identity,
capabilities: openshell_isolation_interface::contract::CapabilityEvidence {
fn test_confirmation() -> openshell_isolation_interface::contract::BoundaryConfirmation {
let audit = crate::boundary_protocol::OpenShellSandboxAuditEvidence {
capabilities: crate::boundary_protocol::CapabilityEvidence {
inheritable: 0,
permitted: 0,
effective: 0,
@@ -2455,7 +2462,7 @@ mod tests {
core_limit_zero: true,
native_architecture: std::env::consts::ARCH.to_string(),
kernel_release: "test".to_string(),
seccomp: openshell_isolation_interface::contract::SeccompEvidence {
seccomp: crate::boundary_protocol::SeccompEvidence {
new_listener: true,
notification_round_trip: true,
id_validation: true,
@@ -2472,11 +2479,17 @@ mod tests {
tcp_dns_round_trip: true,
tcp_allow_round_trip: true,
tcp_deny_round_trip: true,
};
openshell_isolation_interface::contract::BoundaryConfirmation {
generation: "test-generation".to_string(),
identity: sandbox().identity,
properties: audit.properties(),
authenticated_supervisor: true,
session_id: test_session_id(),
driver_fence: test_driver_fence(),
runtime_exit_terminates_workload: true,
resource_claims: std::collections::BTreeMap::new(),
backend_audit: serde_json::to_value(audit).expect("serialize audit evidence"),
}
}
@@ -2594,7 +2607,7 @@ mod tests {
.await
.expect("TLS request"),
Response::Confirmed {
evidence: Box::new(test_confirmation_evidence()),
confirmation: Box::new(test_confirmation()),
}
);
server.abort();
+27 -20
View File
@@ -36,9 +36,8 @@ mod linux {
};
use openshell_core::provider_credentials::ProviderCredentialState;
use openshell_isolation_interface::contract::{
BoundaryExec, BoundaryLoopbackConnector, BoundaryProcess, BoundaryTerminal,
CapabilityEvidence, ExecSession, LoopbackTarget, ResolvedWorkloadIdentity,
SandboxConfirmEvidence,
BoundaryConfirmation, BoundaryExec, BoundaryLoopbackConnector, BoundaryProcess,
BoundaryTerminal, ExecSession, LoopbackTarget, ResolvedWorkloadIdentity,
};
use openshell_sandbox_backend::GPU_RESOURCE_CLAIM;
use openshell_sandbox_backend::mediation::{
@@ -60,11 +59,11 @@ mod linux {
use openshell_sandbox_backend::boundary_protocol::{
AgentSpecWire, BinaryIdentityWire, BoundaryConfig, BoundaryErrorKind,
BoundaryListener as BoundaryListenerConfig, DnsQueryResultWire, ExecSpecWire,
ExitStatusWire, MediationTimingWire, OutputWindowWire, ProcessKindWire,
ProcessSnapshotWire, Request, RequestEnvelope, Response, ResponseEnvelope, STREAM_EXIT,
STREAM_NETWORK_DECISION, STREAM_STDERR, STREAM_STDIN, STREAM_STDIN_CLOSED, STREAM_STDOUT,
SandboxPolicyWire, SessionSnapshotWire, SignalWire, encode_frame, read_frame,
read_stream_frame, validate_resource_claims, write_frame, write_stream_frame,
ExitStatusWire, MediationTimingWire, OpenShellSandboxAuditEvidence, OutputWindowWire,
ProcessKindWire, ProcessSnapshotWire, Request, RequestEnvelope, Response, ResponseEnvelope,
STREAM_EXIT, STREAM_NETWORK_DECISION, STREAM_STDERR, STREAM_STDIN, STREAM_STDIN_CLOSED,
STREAM_STDOUT, SandboxPolicyWire, SessionSnapshotWire, SignalWire, encode_frame,
read_frame, read_stream_frame, validate_resource_claims, write_frame, write_stream_frame,
};
const CONTROL_IO_TIMEOUT: Duration = Duration::from_secs(30);
@@ -2220,20 +2219,20 @@ mod linux {
if let Err(error) = prepared.confirm(&self.process_runtime) {
return guest_error(BoundaryErrorKind::Process, error);
}
let evidence = match self.measure_confirmation_evidence() {
Ok(evidence) => evidence,
let confirmation = match self.measure_confirmation() {
Ok(confirmation) => confirmation,
Err(error) => return guest_error(BoundaryErrorKind::Process, error),
};
*state = RuntimeState::Ready(prepared.clone());
Response::Confirmed {
evidence: Box::new(evidence),
confirmation: Box::new(confirmation),
}
}
RuntimeState::Ready(_) | RuntimeState::Running(_) => {
self.measure_confirmation_evidence().map_or_else(
self.measure_confirmation().map_or_else(
|error| guest_error(BoundaryErrorKind::Process, error),
|evidence| Response::Confirmed {
evidence: Box::new(evidence),
|confirmation| Response::Confirmed {
confirmation: Box::new(confirmation),
},
)
}
@@ -2244,7 +2243,7 @@ mod linux {
}
}
fn measure_confirmation_evidence(&self) -> Result<SandboxConfirmEvidence, String> {
fn measure_confirmation(&self) -> Result<BoundaryConfirmation, String> {
validate_running_identity(
&self.config.workload_identity,
allows_runtime_supplementary_groups(&self.config),
@@ -2257,7 +2256,7 @@ mod linux {
}
let status = std::fs::read_to_string("/proc/self/status")
.map_err(|error| format!("read sandbox process status: {error}"))?;
let capabilities = CapabilityEvidence {
let capabilities = openshell_sandbox_backend::boundary_protocol::CapabilityEvidence {
inheritable: parse_status_hex(&status, "CapInh")?,
permitted: parse_status_hex(&status, "CapPrm")?,
effective: parse_status_hex(&status, "CapEff")?,
@@ -2278,9 +2277,7 @@ mod linux {
// SAFETY: successful getrlimit initialized the value.
let core_limit = unsafe { core_limit.assume_init() };
let (native_architecture, kernel_release) = uname_values()?;
Ok(SandboxConfirmEvidence {
generation: self.config.generation.clone(),
identity: self.config.workload_identity.clone(),
let audit = OpenShellSandboxAuditEvidence {
capabilities,
no_new_privileges,
sandbox_dumpable,
@@ -2295,11 +2292,21 @@ mod linux {
tcp_dns_round_trip: self.qualification.tcp_dns_round_trip,
tcp_allow_round_trip: self.qualification.tcp_allow_round_trip,
tcp_deny_round_trip: self.qualification.tcp_deny_round_trip,
};
audit.validate().map_err(|error| error.to_string())?;
let properties = audit.properties();
let backend_audit = serde_json::to_value(audit)
.map_err(|error| format!("encode OpenShell sandbox audit evidence: {error}"))?;
Ok(BoundaryConfirmation {
generation: self.config.generation.clone(),
identity: self.config.workload_identity.clone(),
properties,
authenticated_supervisor: true,
session_id: self.config.session_id,
driver_fence: self.config.driver_fence.clone(),
runtime_exit_terminates_workload: true,
resource_claims: self.config.resource_claims.clone(),
backend_audit,
})
}
@@ -4201,7 +4208,7 @@ mod linux {
fn test_runtime_qualification() -> crate::RuntimeQualification {
crate::RuntimeQualification {
seccomp: openshell_isolation_interface::contract::SeccompEvidence {
seccomp: openshell_sandbox_backend::boundary_protocol::SeccompEvidence {
new_listener: true,
notification_round_trip: true,
id_validation: true,
+1 -1
View File
@@ -34,7 +34,7 @@ pub mod sandbox;
reason = "qualification preserves independently exercised security results"
)]
pub struct RuntimeQualification {
pub seccomp: openshell_isolation_interface::contract::SeccompEvidence,
pub seccomp: openshell_sandbox_backend::boundary_protocol::SeccompEvidence,
pub landlock_abi: u32,
pub landlock_allow_deny: bool,
pub udp_dns_round_trip: bool,
+1 -1
View File
@@ -239,7 +239,7 @@ fn qualify_runtime() -> Result<(openshell_sandbox::RuntimeQualification, Qualifi
wait_killable_recv: notification.wait_killable_recv,
};
let qualification = openshell_sandbox::RuntimeQualification {
seccomp: openshell_isolation_interface::contract::SeccompEvidence {
seccomp: openshell_sandbox_backend::boundary_protocol::SeccompEvidence {
new_listener: notification.notification_round_trip(),
notification_round_trip: notification.notification_round_trip(),
id_validation: notification.notification_round_trip(),