Make control identity mode umask-independent
Set and validate the publication identity through its retained descriptor so restrictive process masks cannot weaken or remove the required mode.
This commit is contained in:
parent
c3a76ec8c4
commit
18afbeaa91
2 changed files with 181 additions and 32 deletions
|
|
@ -28,7 +28,7 @@ use rcgen::{
|
||||||
DistinguishedName, DnType, DnValue, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
|
DistinguishedName, DnType, DnValue, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
|
||||||
PKCS_ED25519,
|
PKCS_ED25519,
|
||||||
};
|
};
|
||||||
use rustix::fs::{Mode, OFlags, openat};
|
use rustix::fs::{Mode, OFlags, fchmod, openat};
|
||||||
use rustix::net::sockopt::socket_peercred;
|
use rustix::net::sockopt::socket_peercred;
|
||||||
use rustls::pki_types::{
|
use rustls::pki_types::{
|
||||||
CertificateDer, CertificateSigningRequestDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName,
|
CertificateDer, CertificateSigningRequestDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName,
|
||||||
|
|
@ -630,8 +630,12 @@ impl ControlPaths {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_identity(&self, bytes: &[u8]) -> Result<(File, FileIdentity), ControlError> {
|
fn create_identity(
|
||||||
self.validate_root(self.root_file.metadata()?.uid())?;
|
&self,
|
||||||
|
bytes: &[u8],
|
||||||
|
uid: u32,
|
||||||
|
) -> Result<(File, FileIdentity), ControlError> {
|
||||||
|
self.validate_root(uid)?;
|
||||||
let fd = openat(
|
let fd = openat(
|
||||||
self.root_file.as_fd(),
|
self.root_file.as_fd(),
|
||||||
PUBLICATION_IDENTITY_NAME,
|
PUBLICATION_IDENTITY_NAME,
|
||||||
|
|
@ -640,15 +644,51 @@ impl ControlPaths {
|
||||||
)
|
)
|
||||||
.map_err(io::Error::from)?;
|
.map_err(io::Error::from)?;
|
||||||
let mut file = File::from(fd);
|
let mut file = File::from(fd);
|
||||||
file.write_all(bytes)
|
let identity = FileIdentity::from_metadata(&file.metadata()?);
|
||||||
.map_err(|_| ControlError::InvalidPublication("identity write failed"))?;
|
let result = (|| {
|
||||||
file.sync_all()
|
fchmod(file.as_fd(), Mode::RUSR | Mode::WUSR).map_err(io::Error::from)?;
|
||||||
.map_err(|_| ControlError::InvalidPublication("identity sync failed"))?;
|
file.write_all(bytes)
|
||||||
self.root_file
|
.map_err(|_| ControlError::InvalidPublication("identity write failed"))?;
|
||||||
.sync_all()
|
file.sync_all()
|
||||||
.map_err(|_| ControlError::InvalidPublication("directory sync failed"))?;
|
.map_err(|_| ControlError::InvalidPublication("identity sync failed"))?;
|
||||||
let metadata = file.metadata()?;
|
self.root_file
|
||||||
Ok((file, FileIdentity::from_metadata(&metadata)))
|
.sync_all()
|
||||||
|
.map_err(|_| ControlError::InvalidPublication("directory sync failed"))?;
|
||||||
|
self.validate_identity(&file, identity, uid, bytes.len())
|
||||||
|
})();
|
||||||
|
if let Err(error) = result {
|
||||||
|
remove_if_identity_matches(&self.identity, identity);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok((file, identity))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_identity(
|
||||||
|
&self,
|
||||||
|
file: &File,
|
||||||
|
expected: FileIdentity,
|
||||||
|
uid: u32,
|
||||||
|
expected_length: usize,
|
||||||
|
) -> Result<(), ControlError> {
|
||||||
|
self.validate_root(uid)?;
|
||||||
|
let descriptor = file.metadata()?;
|
||||||
|
let current = std::fs::symlink_metadata(&self.identity)?;
|
||||||
|
if FileIdentity::from_metadata(&descriptor) != expected
|
||||||
|
|| FileIdentity::from_metadata(¤t) != expected
|
||||||
|
|| !descriptor.file_type().is_file()
|
||||||
|
|| !current.file_type().is_file()
|
||||||
|
|| descriptor.uid() != uid
|
||||||
|
|| current.uid() != uid
|
||||||
|
|| descriptor.mode() & 0o7777 != 0o600
|
||||||
|
|| current.mode() & 0o7777 != 0o600
|
||||||
|
|| usize::try_from(descriptor.len()).ok() != Some(expected_length)
|
||||||
|
|| current.len() != descriptor.len()
|
||||||
|
{
|
||||||
|
return Err(ControlError::InvalidPublication(
|
||||||
|
"publication identity changed type, ownership, mode, or content length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,7 +732,8 @@ impl PublishedControlFace {
|
||||||
let publication_identity = anchor.publication_identity();
|
let publication_identity = anchor.publication_identity();
|
||||||
let publication_bytes = encode(&publication_identity)?;
|
let publication_bytes = encode(&publication_identity)?;
|
||||||
let server_config = Arc::new(anchor.server_config()?);
|
let server_config = Arc::new(anchor.server_config()?);
|
||||||
let (identity_file, identity_file_identity) = paths.create_identity(&publication_bytes)?;
|
let (identity_file, identity_file_identity) =
|
||||||
|
paths.create_identity(&publication_bytes, uid)?;
|
||||||
let listener = match std::os::unix::net::UnixListener::bind(&paths.socket) {
|
let listener = match std::os::unix::net::UnixListener::bind(&paths.socket) {
|
||||||
Ok(listener) => listener,
|
Ok(listener) => listener,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -730,25 +771,16 @@ impl PublishedControlFace {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let identity_unchanged = identity_file
|
if let Err(error) = paths.validate_identity(
|
||||||
.metadata()
|
&identity_file,
|
||||||
.map(|metadata| FileIdentity::from_metadata(&metadata) == identity_file_identity);
|
identity_file_identity,
|
||||||
match identity_unchanged {
|
uid,
|
||||||
Ok(true) => {}
|
publication_bytes.len(),
|
||||||
Ok(false) => {
|
) {
|
||||||
drop(listener);
|
drop(listener);
|
||||||
let _ = std::fs::remove_file(&paths.socket);
|
let _ = std::fs::remove_file(&paths.socket);
|
||||||
let _ = std::fs::remove_file(&paths.identity);
|
remove_if_identity_matches(&paths.identity, identity_file_identity);
|
||||||
return Err(ControlError::InvalidPublication(
|
return Err(error);
|
||||||
"publication identity changed during creation",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
drop(listener);
|
|
||||||
let _ = std::fs::remove_file(&paths.socket);
|
|
||||||
let _ = std::fs::remove_file(&paths.identity);
|
|
||||||
return Err(error.into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
listener,
|
listener,
|
||||||
|
|
|
||||||
117
crates/lazaret-host-coordinator/tests/control_umask.rs
Normal file
117
crates/lazaret-host-coordinator/tests/control_umask.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use lazaret_core::identity::{
|
||||||
|
ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, HostPath, NetworkSidecarId,
|
||||||
|
SandboxId,
|
||||||
|
};
|
||||||
|
use lazaret_core::records::{Domain, DurableDecision, Generation, GenerationIdentity};
|
||||||
|
use lazaret_host_coordinator::control::{PublishedControlFace, TrustAnchor};
|
||||||
|
use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
||||||
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
|
use rustix::fs::Mode;
|
||||||
|
use rustix::process::{getuid, umask};
|
||||||
|
|
||||||
|
struct TestDirectory(PathBuf);
|
||||||
|
|
||||||
|
impl TestDirectory {
|
||||||
|
fn new() -> Self {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"lazaret-control-restrictive-umask-{}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
std::fs::create_dir(&path).unwrap();
|
||||||
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self, name: &str) -> PathBuf {
|
||||||
|
self.0.join(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestDirectory {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
std::fs::remove_dir_all(&self.0).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UmaskGuard(Mode);
|
||||||
|
|
||||||
|
impl Drop for UmaskGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
umask(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(value: u8) -> [u8; 32] {
|
||||||
|
[value; 32]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generation_identity() -> GenerationIdentity {
|
||||||
|
GenerationIdentity {
|
||||||
|
agent_profile: "trusted".into(),
|
||||||
|
domain_id: DomainId::new(bytes(1)),
|
||||||
|
generation_id: GenerationId::new(7),
|
||||||
|
configuration_fingerprint: ConfigurationFingerprint::new(bytes(2)),
|
||||||
|
coordinator_id: CoordinatorId::new(bytes(17)),
|
||||||
|
sandbox_id: SandboxId::new(bytes(27)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publication_identity_mode_is_exact_under_restrictive_umask() {
|
||||||
|
let directory = TestDirectory::new();
|
||||||
|
let identity = generation_identity();
|
||||||
|
let lock = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(directory.path("domain.lock"))
|
||||||
|
.unwrap();
|
||||||
|
let lock = try_exclusive_flock(lock).unwrap();
|
||||||
|
let domain = Domain {
|
||||||
|
domain_id: identity.domain_id,
|
||||||
|
profile_name: identity.agent_profile.clone(),
|
||||||
|
canonical_workspace: HostPath::new(b"/workspace/project".to_vec()),
|
||||||
|
};
|
||||||
|
let store = Arc::new(
|
||||||
|
HostCoordinatorDomainStore::open(lock, &directory.path("domain.redb"), &domain).unwrap(),
|
||||||
|
);
|
||||||
|
for durable_decision in [
|
||||||
|
DurableDecision::None,
|
||||||
|
DurableDecision::Preparing,
|
||||||
|
DurableDecision::Published,
|
||||||
|
] {
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(Generation {
|
||||||
|
domain_id: identity.domain_id,
|
||||||
|
generation_id: identity.generation_id,
|
||||||
|
configuration_fingerprint: identity.configuration_fingerprint,
|
||||||
|
coordinator_id: identity.coordinator_id,
|
||||||
|
network_sidecar_id: NetworkSidecarId::new(bytes(4)),
|
||||||
|
sandbox_id: identity.sandbox_id,
|
||||||
|
durable_decision,
|
||||||
|
})])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _umask = UmaskGuard(umask(Mode::from_raw_mode(0o777)));
|
||||||
|
let face = PublishedControlFace::publish(
|
||||||
|
&directory.0,
|
||||||
|
getuid().as_raw(),
|
||||||
|
TrustAnchor::create(identity).unwrap(),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
face.identity_path(),
|
||||||
|
directory.path("protocol-identity.postcard")
|
||||||
|
);
|
||||||
|
let metadata = std::fs::symlink_metadata(face.identity_path()).unwrap();
|
||||||
|
assert!(metadata.file_type().is_file());
|
||||||
|
assert_eq!(metadata.uid(), getuid().as_raw());
|
||||||
|
assert_eq!(metadata.mode() & 0o7777, 0o600);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue