328 lines
12 KiB
Rust
328 lines
12 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::ffi::OsString;
|
|
use std::fs::{self, File};
|
|
use std::os::unix::ffi::OsStringExt;
|
|
use std::os::unix::fs::MetadataExt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use lazaret_core::configuration::{
|
|
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
|
};
|
|
use lazaret_core::identity::{
|
|
DeviceId, HostPath, IdMapDigest, InodeId, PolicyId, PreparedArtifactId, SelectedManifestDigest,
|
|
SourceIdentity,
|
|
};
|
|
use lazaret_core::records::{ArtifactState, PreparedArtifact};
|
|
use lazaret_state_anchor::{
|
|
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorLaunchAuthority,
|
|
StateAnchorLayout,
|
|
};
|
|
|
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
|
|
#[test]
|
|
fn replaced_runtime_root_is_refused_before_runtime_file_write() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let runtime_root = PathBuf::from(OsString::from_vec(
|
|
configuration
|
|
.runtime_domain_root()
|
|
.canonical_label()
|
|
.as_bytes()
|
|
.to_vec(),
|
|
));
|
|
let moved_root = runtime_root.with_extension("retained");
|
|
fs::rename(&runtime_root, &moved_root).unwrap();
|
|
let replacement_files = runtime_root.join("state-anchor/runtime-files");
|
|
fs::create_dir_all(&replacement_files).unwrap();
|
|
fs::write(replacement_files.join("hostname"), b"replacement\n").unwrap();
|
|
|
|
assert!(layout.revalidate().is_err());
|
|
assert!(
|
|
layout
|
|
.replace_protected_runtime_files(&ProtectedRuntimeFiles {
|
|
resolv_conf: b"nameserver 192.0.2.1\n",
|
|
hosts: b"127.0.0.1 localhost\n",
|
|
hostname: b"changed\n",
|
|
})
|
|
.is_err()
|
|
);
|
|
assert_eq!(
|
|
fs::read(replacement_files.join("hostname")).unwrap(),
|
|
b"replacement\n"
|
|
);
|
|
assert!(!replacement_files.join("resolv.conf").exists());
|
|
assert!(!replacement_files.join("hosts").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn inherited_runtime_authority_accepts_only_the_exact_prepared_lower() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let lower = tree.path.join("prepared-lower");
|
|
fs::create_dir(&lower).unwrap();
|
|
let metadata = lower.metadata().unwrap();
|
|
let artifact = PreparedArtifact {
|
|
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
|
source_identity: SourceIdentity::new(b"fixture".to_vec()),
|
|
selected_manifest_digest: SelectedManifestDigest::new([9; 32]),
|
|
policy_id: PolicyId::new([10; 32]),
|
|
id_map_digest: IdMapDigest::new([11; 32]),
|
|
path: HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()),
|
|
device: DeviceId::new(metadata.dev()),
|
|
inode: InodeId::new(metadata.ino()),
|
|
state: ArtifactState::Available,
|
|
};
|
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
|
let expected = launch.expected_lower();
|
|
RuntimeStateAnchorAuthority::from_inherited(
|
|
launch.lower_fd(),
|
|
launch.lower_label(),
|
|
launch.persistent_root_fd(),
|
|
launch.persistent_anchor_fd(),
|
|
launch.persistent_anchor_label(),
|
|
launch.runtime_root_fd(),
|
|
launch.runtime_anchor_fd(),
|
|
launch.runtime_anchor_label(),
|
|
expected,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
RuntimeStateAnchorAuthority::from_inherited(
|
|
launch.lower_fd(),
|
|
launch.lower_label(),
|
|
launch.persistent_root_fd(),
|
|
launch.persistent_anchor_fd(),
|
|
launch.persistent_anchor_label(),
|
|
launch.runtime_root_fd(),
|
|
launch.runtime_anchor_fd(),
|
|
launch.runtime_anchor_label(),
|
|
(expected.0, expected.1 + 1),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn retained_prepared_lower_authority_refuses_a_replacement_descriptor() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let lower = tree.path.join("prepared-lower");
|
|
let replacement = tree.path.join("replacement-lower");
|
|
fs::create_dir(&lower).unwrap();
|
|
fs::create_dir(&replacement).unwrap();
|
|
let metadata = lower.metadata().unwrap();
|
|
let artifact = PreparedArtifact {
|
|
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
|
source_identity: SourceIdentity::new(b"fixture".to_vec()),
|
|
selected_manifest_digest: SelectedManifestDigest::new([9; 32]),
|
|
policy_id: PolicyId::new([10; 32]),
|
|
id_map_digest: IdMapDigest::new([11; 32]),
|
|
path: HostPath::new(b"artifacts/fixture".to_vec()),
|
|
device: DeviceId::new(metadata.dev()),
|
|
inode: InodeId::new(metadata.ino()),
|
|
state: ArtifactState::Available,
|
|
};
|
|
let exact = File::open(&lower).unwrap();
|
|
StateAnchorLaunchAuthority::capture_retained(&layout, &artifact, &lower, &exact).unwrap();
|
|
let replacement = File::open(&replacement).unwrap();
|
|
assert!(
|
|
StateAnchorLaunchAuthority::capture_retained(&layout, &artifact, &lower, &replacement)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_local_rebound_refuses_a_replaced_state_anchor() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let lower = tree.path.join("prepared-lower");
|
|
fs::create_dir(&lower).unwrap();
|
|
let artifact = prepared_artifact(&lower);
|
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
|
let anchor = launch.runtime_anchor_label().to_owned();
|
|
fs::rename(&anchor, anchor.with_extension("retained")).unwrap();
|
|
fs::create_dir(&anchor).unwrap();
|
|
|
|
assert!(
|
|
RuntimeStateAnchorAuthority::from_inherited(
|
|
launch.lower_fd(),
|
|
launch.lower_label(),
|
|
launch.persistent_root_fd(),
|
|
launch.persistent_anchor_fd(),
|
|
launch.persistent_anchor_label(),
|
|
launch.runtime_root_fd(),
|
|
launch.runtime_anchor_fd(),
|
|
launch.runtime_anchor_label(),
|
|
launch.expected_lower(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_local_rebound_refuses_a_replaced_prepared_lower() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let lower = tree.path.join("prepared-lower");
|
|
fs::create_dir(&lower).unwrap();
|
|
let artifact = prepared_artifact(&lower);
|
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
|
fs::rename(&lower, lower.with_extension("retained")).unwrap();
|
|
fs::create_dir(&lower).unwrap();
|
|
|
|
assert!(
|
|
RuntimeStateAnchorAuthority::from_inherited(
|
|
launch.lower_fd(),
|
|
launch.lower_label(),
|
|
launch.persistent_root_fd(),
|
|
launch.persistent_anchor_fd(),
|
|
launch.persistent_anchor_label(),
|
|
launch.runtime_root_fd(),
|
|
launch.runtime_anchor_fd(),
|
|
launch.runtime_anchor_label(),
|
|
launch.expected_lower(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_local_rebound_refuses_a_replaced_persistent_anchor() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let lower = tree.path.join("prepared-lower");
|
|
fs::create_dir(&lower).unwrap();
|
|
let artifact = prepared_artifact(&lower);
|
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
|
let anchor = launch.persistent_anchor_label().to_owned();
|
|
fs::rename(&anchor, anchor.with_extension("retained")).unwrap();
|
|
fs::create_dir(&anchor).unwrap();
|
|
|
|
assert!(
|
|
RuntimeStateAnchorAuthority::from_inherited(
|
|
launch.lower_fd(),
|
|
launch.lower_label(),
|
|
launch.persistent_root_fd(),
|
|
launch.persistent_anchor_fd(),
|
|
launch.persistent_anchor_label(),
|
|
launch.runtime_root_fd(),
|
|
launch.runtime_anchor_fd(),
|
|
launch.runtime_anchor_label(),
|
|
launch.expected_lower(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_local_rebound_refuses_a_state_anchor_symlink() {
|
|
let tree = TestTree::new();
|
|
let configuration = configuration(&tree);
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
let lower = tree.path.join("prepared-lower");
|
|
fs::create_dir(&lower).unwrap();
|
|
let artifact = prepared_artifact(&lower);
|
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
|
let anchor = launch.runtime_anchor_label().to_owned();
|
|
let retained = anchor.with_extension("retained");
|
|
fs::rename(&anchor, &retained).unwrap();
|
|
std::os::unix::fs::symlink(&retained, &anchor).unwrap();
|
|
|
|
assert!(
|
|
RuntimeStateAnchorAuthority::from_inherited(
|
|
launch.lower_fd(),
|
|
launch.lower_label(),
|
|
launch.persistent_root_fd(),
|
|
launch.persistent_anchor_fd(),
|
|
launch.persistent_anchor_label(),
|
|
launch.runtime_root_fd(),
|
|
launch.runtime_anchor_fd(),
|
|
launch.runtime_anchor_label(),
|
|
launch.expected_lower(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
fn prepared_artifact(lower: &Path) -> PreparedArtifact {
|
|
let metadata = lower.metadata().unwrap();
|
|
PreparedArtifact {
|
|
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
|
source_identity: SourceIdentity::new(b"fixture".to_vec()),
|
|
selected_manifest_digest: SelectedManifestDigest::new([9; 32]),
|
|
policy_id: PolicyId::new([10; 32]),
|
|
id_map_digest: IdMapDigest::new([11; 32]),
|
|
path: HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()),
|
|
device: DeviceId::new(metadata.dev()),
|
|
inode: InodeId::new(metadata.ino()),
|
|
state: ArtifactState::Available,
|
|
}
|
|
}
|
|
|
|
struct TestTree {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl TestTree {
|
|
fn new() -> Self {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"lazaret-state-anchor-authority-{}-{}",
|
|
std::process::id(),
|
|
SEQUENCE.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir(&path).unwrap();
|
|
Self { path }
|
|
}
|
|
}
|
|
|
|
impl Drop for TestTree {
|
|
fn drop(&mut self) {
|
|
fs::remove_dir_all(&self.path).unwrap();
|
|
}
|
|
}
|
|
|
|
fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomainConfiguration {
|
|
let configuration_directory = tree.path.join("configuration");
|
|
let workspace = tree.path.join("workspace");
|
|
let runtime = tree.path.join("runtime");
|
|
fs::create_dir(&configuration_directory).unwrap();
|
|
fs::create_dir(&workspace).unwrap();
|
|
fs::create_dir(&runtime).unwrap();
|
|
let configuration_path = configuration_directory.join("lazaret.toml");
|
|
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/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
|
tree.path.join("data").display()
|
|
),
|
|
)
|
|
.unwrap();
|
|
let layout = SandboxLayoutV1::from_production_layout([
|
|
"/workspace".to_owned(),
|
|
"/proc".to_owned(),
|
|
"/sys".to_owned(),
|
|
"/dev".to_owned(),
|
|
"/run/lazaret".to_owned(),
|
|
])
|
|
.unwrap();
|
|
load_trusted_configuration(ConfigurationLoadRequest {
|
|
explicit_configuration_path: Some(configuration_path),
|
|
selected_profile: "default".to_owned(),
|
|
workspace,
|
|
home: tree.path.join("home"),
|
|
xdg_config_home: None,
|
|
xdg_data_home: None,
|
|
xdg_runtime_dir: runtime,
|
|
environment: BTreeMap::<String, OsString>::new(),
|
|
sandbox_layout: layout,
|
|
})
|
|
.unwrap()
|
|
}
|