commit 40505188fa5ed2e48e5bcdd64d785679456e4ea1
parent 085cc3258f4635f45ef3c16cda74952bf1608448
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 04:27:05 +0000
service-host: authorize admin peers
- require Linux SO_PEERCRED admission before request dispatch
- bind configured admin groups to exact directory and socket modes
- keep macOS filesystem-only and other platforms explicitly unsupported
- cover process credentials, policy denial, modes, and target compilation
Diffstat:
6 files changed, 463 insertions(+), 20 deletions(-)
diff --git a/crates/service_host/src/admin/mod.rs b/crates/service_host/src/admin/mod.rs
@@ -4,6 +4,7 @@
mod client;
mod limits;
mod model;
+mod peer;
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod server;
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -25,6 +26,9 @@ pub use model::{
AdminIdentifierField, AdminMutationRequest, AdminOperationId, AdminPayloadError,
AdminSuccessResponse,
};
+pub use peer::{
+ AdminPeerAuthorizationPolicy, AdminPeerAuthorizationPolicyError, AdminPeerAuthorizationSupport,
+};
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub use server::{
ADMIN_MIN_RESPONSE_BODY_UTF8_BYTES, ADMIN_ROUTE_PARAMETER_NAME_MAX_UTF8_BYTES,
@@ -36,6 +40,7 @@ pub use server::{
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub use unix::{
- UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT, UNIX_ADMIN_OWNER_DIRECTORY_MODE, UNIX_ADMIN_OWNER_SOCKET_MODE,
- UnixAdminSocketBinding, UnixAdminSocketError, UnixAdminSocketWriterAuthority,
+ UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT, UNIX_ADMIN_GROUP_DIRECTORY_MODE, UNIX_ADMIN_GROUP_SOCKET_MODE,
+ UNIX_ADMIN_OWNER_DIRECTORY_MODE, UNIX_ADMIN_OWNER_SOCKET_MODE, UnixAdminSocketBinding,
+ UnixAdminSocketError, UnixAdminSocketWriterAuthority,
};
diff --git a/crates/service_host/src/admin/peer.rs b/crates/service_host/src/admin/peer.rs
@@ -0,0 +1,268 @@
+//! Platform-bounded local-admin peer authorization.
+
+use core::fmt;
+use std::error::Error;
+
+/// Host support level for local-admin peer authorization.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AdminPeerAuthorizationSupport {
+ /// Linux requires kernel-reported `SO_PEERCRED` credentials.
+ LinuxSoPeerCredRequired,
+ /// macOS v1 relies only on owner-restricted filesystem permissions.
+ MacOsFilesystemOwnerPermissionsOnly,
+ /// The local-admin transport is not supported on this platform in v1.
+ Unsupported,
+}
+
+impl AdminPeerAuthorizationSupport {
+ /// Returns the exact v1 authorization support for the compilation target.
+ #[must_use]
+ pub const fn current() -> Self {
+ #[cfg(target_os = "linux")]
+ {
+ Self::LinuxSoPeerCredRequired
+ }
+ #[cfg(target_os = "macos")]
+ {
+ Self::MacOsFilesystemOwnerPermissionsOnly
+ }
+ #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+ {
+ Self::Unsupported
+ }
+ }
+}
+
+/// A safe configuration failure for local-admin peer authorization.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AdminPeerAuthorizationPolicyError {
+ InvalidAdminGroupId,
+ AdminGroupUnsupported,
+}
+
+impl fmt::Display for AdminPeerAuthorizationPolicyError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("admin peer authorization policy is invalid")
+ }
+}
+
+impl Error for AdminPeerAuthorizationPolicyError {}
+
+/// One immutable policy shared by Unix-socket permissions and peer admission.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct AdminPeerAuthorizationPolicy {
+ admin_gid: Option<u32>,
+}
+
+impl AdminPeerAuthorizationPolicy {
+ /// Restricts access to the daemon's effective user identity.
+ #[must_use]
+ pub const fn owner_only() -> Self {
+ Self { admin_gid: None }
+ }
+
+ /// Allows the configured Linux admin group in addition to the daemon user.
+ pub fn with_admin_gid(admin_gid: u32) -> Result<Self, AdminPeerAuthorizationPolicyError> {
+ if AdminPeerAuthorizationSupport::current()
+ != AdminPeerAuthorizationSupport::LinuxSoPeerCredRequired
+ {
+ return Err(AdminPeerAuthorizationPolicyError::AdminGroupUnsupported);
+ }
+ if admin_gid == u32::MAX {
+ return Err(AdminPeerAuthorizationPolicyError::InvalidAdminGroupId);
+ }
+ Ok(Self {
+ admin_gid: Some(admin_gid),
+ })
+ }
+
+ /// Returns the host support contract represented by this policy.
+ #[must_use]
+ pub const fn support(self) -> AdminPeerAuthorizationSupport {
+ AdminPeerAuthorizationSupport::current()
+ }
+
+ /// Returns the configured Linux admin group, when present.
+ #[must_use]
+ pub const fn admin_gid(self) -> Option<u32> {
+ self.admin_gid
+ }
+}
+
+impl Default for AdminPeerAuthorizationPolicy {
+ fn default() -> Self {
+ Self::owner_only()
+ }
+}
+
+#[cfg(target_os = "linux")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum PeerAuthorizationFailure {
+ CredentialsUnavailable { kind: std::io::ErrorKind },
+ Denied,
+}
+
+#[cfg(target_os = "macos")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum PeerAuthorizationFailure {}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct PeerAuthorizer {
+ policy: AdminPeerAuthorizationPolicy,
+ daemon_euid: u32,
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl PeerAuthorizer {
+ pub(crate) const fn new(policy: AdminPeerAuthorizationPolicy, daemon_euid: u32) -> Self {
+ Self {
+ policy,
+ daemon_euid,
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ pub(crate) fn authorize(
+ self,
+ stream: &tokio::net::UnixStream,
+ ) -> Result<(), PeerAuthorizationFailure> {
+ self.authorize_linux_result(
+ stream
+ .peer_cred()
+ .map(|credentials| (credentials.uid(), credentials.gid()))
+ .map_err(|error| error.kind()),
+ )
+ }
+
+ #[cfg(target_os = "linux")]
+ fn authorize_linux_result(
+ self,
+ credentials: Result<(u32, u32), std::io::ErrorKind>,
+ ) -> Result<(), PeerAuthorizationFailure> {
+ let (peer_uid, peer_gid) = credentials
+ .map_err(|kind| PeerAuthorizationFailure::CredentialsUnavailable { kind })?;
+ self.authorize_linux_credentials(peer_uid, peer_gid)
+ }
+
+ #[cfg(target_os = "linux")]
+ fn authorize_linux_credentials(
+ self,
+ peer_uid: u32,
+ peer_gid: u32,
+ ) -> Result<(), PeerAuthorizationFailure> {
+ if peer_uid == self.daemon_euid || self.policy.admin_gid == Some(peer_gid) {
+ Ok(())
+ } else {
+ Err(PeerAuthorizationFailure::Denied)
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ pub(crate) const fn authorize(
+ self,
+ _stream: &tokio::net::UnixStream,
+ ) -> Result<(), PeerAuthorizationFailure> {
+ let _ = self.policy;
+ let _ = self.daemon_euid;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn support_inventory_is_exact_for_the_compilation_target() {
+ #[cfg(target_os = "linux")]
+ assert_eq!(
+ AdminPeerAuthorizationSupport::current(),
+ AdminPeerAuthorizationSupport::LinuxSoPeerCredRequired
+ );
+ #[cfg(target_os = "macos")]
+ assert_eq!(
+ AdminPeerAuthorizationSupport::current(),
+ AdminPeerAuthorizationSupport::MacOsFilesystemOwnerPermissionsOnly
+ );
+ #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+ assert_eq!(
+ AdminPeerAuthorizationSupport::current(),
+ AdminPeerAuthorizationSupport::Unsupported
+ );
+ }
+
+ #[test]
+ fn owner_policy_is_stable_and_group_policy_is_platform_bounded() {
+ let owner = AdminPeerAuthorizationPolicy::owner_only();
+ assert_eq!(owner.admin_gid(), None);
+ assert_eq!(owner.support(), AdminPeerAuthorizationSupport::current());
+ assert_eq!(
+ AdminPeerAuthorizationPolicy::with_admin_gid(u32::MAX),
+ Err(if cfg!(target_os = "linux") {
+ AdminPeerAuthorizationPolicyError::InvalidAdminGroupId
+ } else {
+ AdminPeerAuthorizationPolicyError::AdminGroupUnsupported
+ })
+ );
+ #[cfg(target_os = "linux")]
+ assert_eq!(
+ AdminPeerAuthorizationPolicy::with_admin_gid(42)
+ .expect("valid Linux admin group")
+ .admin_gid(),
+ Some(42)
+ );
+ #[cfg(not(target_os = "linux"))]
+ assert_eq!(
+ AdminPeerAuthorizationPolicy::with_admin_gid(42),
+ Err(AdminPeerAuthorizationPolicyError::AdminGroupUnsupported)
+ );
+ }
+
+ #[cfg(target_os = "linux")]
+ #[tokio::test]
+ async fn linux_process_credentials_allow_uid_or_gid_and_deny_otherwise() {
+ let (peer, observed) = tokio::net::UnixStream::pair().expect("Unix stream pair");
+ let credentials = peer.peer_cred().expect("Linux SO_PEERCRED");
+ let different_uid = different_id(credentials.uid());
+ let different_gid = different_id(credentials.gid());
+
+ PeerAuthorizer::new(
+ AdminPeerAuthorizationPolicy::owner_only(),
+ credentials.uid(),
+ )
+ .authorize(&observed)
+ .expect("matching daemon euid");
+ PeerAuthorizer::new(
+ AdminPeerAuthorizationPolicy::with_admin_gid(credentials.gid())
+ .expect("peer group policy"),
+ different_uid,
+ )
+ .authorize(&observed)
+ .expect("matching configured admin gid");
+ assert_eq!(
+ PeerAuthorizer::new(
+ AdminPeerAuthorizationPolicy::with_admin_gid(different_gid)
+ .expect("different group policy"),
+ different_uid,
+ )
+ .authorize(&observed),
+ Err(PeerAuthorizationFailure::Denied)
+ );
+ assert_eq!(
+ PeerAuthorizer::new(
+ AdminPeerAuthorizationPolicy::owner_only(),
+ credentials.uid(),
+ )
+ .authorize_linux_result(Err(std::io::ErrorKind::PermissionDenied)),
+ Err(PeerAuthorizationFailure::CredentialsUnavailable {
+ kind: std::io::ErrorKind::PermissionDenied,
+ })
+ );
+ }
+
+ #[cfg(target_os = "linux")]
+ fn different_id(id: u32) -> u32 {
+ if id == 0 { 1 } else { 0 }
+ }
+}
diff --git a/crates/service_host/src/admin/server.rs b/crates/service_host/src/admin/server.rs
@@ -754,6 +754,7 @@ impl AdminServer {
.map_err(|error| AdminServerError::ListenerRegistration { kind: error.kind() })?;
let listener = tokio::net::UnixListener::from_std(listener)
.map_err(|error| AdminServerError::ListenerRegistration { kind: error.kind() })?;
+ let peer_authorizer = binding.peer_authorizer();
let permits = Arc::new(Semaphore::new(
self.state.limits.concurrent_connections() as usize
));
@@ -777,6 +778,10 @@ impl AdminServer {
break Err(AdminServerError::Accept { kind: error.kind() });
}
};
+ if peer_authorizer.authorize(&stream).is_err() {
+ drop(stream);
+ continue;
+ }
let Ok(permit) = Arc::clone(&permits).try_acquire_owned() else {
drop(stream);
continue;
diff --git a/crates/service_host/src/admin/unix.rs b/crates/service_host/src/admin/unix.rs
@@ -11,14 +11,22 @@ use std::os::unix::net::UnixListener;
use std::path::{Path, PathBuf};
use std::time::Duration;
-use rustix::fs::{FileType, Mode, OFlags, fchmod, fstat, open, openat};
+use rustix::fs::{
+ AtFlags, FileType, Gid, Mode, OFlags, chownat, fchmod, fchown, fstat, open, openat,
+};
use rustix::process::geteuid;
+use super::peer::{AdminPeerAuthorizationPolicy, PeerAuthorizer};
+
const WRITER_LOCK_FILE_NAME: &str = ".radroots-admin-writer.lock";
/// Final owner-only mode for the runtime directory.
pub const UNIX_ADMIN_OWNER_DIRECTORY_MODE: u32 = 0o700;
/// Final owner-only mode for the socket path.
pub const UNIX_ADMIN_OWNER_SOCKET_MODE: u32 = 0o600;
+/// Final Linux mode for a runtime directory shared with an admin group.
+pub const UNIX_ADMIN_GROUP_DIRECTORY_MODE: u32 = 0o750;
+/// Final Linux mode for a socket shared with an admin group.
+pub const UNIX_ADMIN_GROUP_SOCKET_MODE: u32 = 0o660;
/// Maximum time spent proving that an existing Unix socket has a live listener.
pub const UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -32,6 +40,7 @@ pub enum UnixAdminSocketError {
RuntimeDirectoryWrongOwner,
RuntimeDirectoryChanged,
RuntimeDirectoryPermissions { kind: io::ErrorKind },
+ RuntimeDirectoryGroup { kind: io::ErrorKind },
InvalidSocketPath,
WriterLockUnavailable { kind: io::ErrorKind },
WriterLockInvalidType,
@@ -45,6 +54,7 @@ pub enum UnixAdminSocketError {
StaleSocketCleanup { kind: io::ErrorKind },
SocketBind { kind: io::ErrorKind },
SocketPermissions { kind: io::ErrorKind },
+ SocketGroup { kind: io::ErrorKind },
ListenerConfiguration { kind: io::ErrorKind },
}
@@ -59,6 +69,9 @@ impl fmt::Display for UnixAdminSocketError {
Self::RuntimeDirectoryPermissions { .. } => {
"admin runtime directory permissions could not be secured"
}
+ Self::RuntimeDirectoryGroup { .. } => {
+ "admin runtime directory group could not be secured"
+ }
Self::InvalidSocketPath => "admin socket path is outside its runtime directory",
Self::WriterLockUnavailable { .. } => "admin writer lock is unavailable",
Self::WriterLockInvalidType => "admin writer lock path has an unsafe type",
@@ -72,6 +85,7 @@ impl fmt::Display for UnixAdminSocketError {
Self::StaleSocketCleanup { .. } => "stale admin socket could not be removed",
Self::SocketBind { .. } => "admin socket could not be bound",
Self::SocketPermissions { .. } => "admin socket permissions could not be secured",
+ Self::SocketGroup { .. } => "admin socket group could not be secured",
Self::ListenerConfiguration { .. } => "admin listener could not be configured",
})
}
@@ -103,6 +117,9 @@ pub struct UnixAdminSocketWriterAuthority {
_directory: File,
directory_identity: FileIdentity,
expected_uid: u32,
+ expected_gid: Option<u32>,
+ directory_mode: u32,
+ peer_authorization: AdminPeerAuthorizationPolicy,
_writer_lock: File,
}
@@ -119,20 +136,23 @@ impl fmt::Debug for UnixAdminSocketWriterAuthority {
impl UnixAdminSocketWriterAuthority {
/// Acquires owner-only writer authority for one existing runtime directory.
pub fn acquire(runtime_directory: impl AsRef<Path>) -> Result<Self, UnixAdminSocketError> {
+ Self::acquire_with_peer_authorization(
+ runtime_directory,
+ AdminPeerAuthorizationPolicy::owner_only(),
+ )
+ }
+
+ /// Acquires writer authority using one policy for permissions and peer admission.
+ pub fn acquire_with_peer_authorization(
+ runtime_directory: impl AsRef<Path>,
+ peer_authorization: AdminPeerAuthorizationPolicy,
+ ) -> Result<Self, UnixAdminSocketError> {
let runtime_directory = runtime_directory.as_ref().to_path_buf();
if !runtime_directory.is_absolute() {
return Err(UnixAdminSocketError::RuntimeDirectoryNotAbsolute);
}
let expected_uid = geteuid().as_raw();
let directory = open_secure_directory(&runtime_directory, expected_uid)?;
- fchmod(&directory, Mode::RWXU).map_err(|error| {
- UnixAdminSocketError::RuntimeDirectoryPermissions {
- kind: errno_kind(error),
- }
- })?;
- let directory_metadata = directory.metadata().map_err(|error| {
- UnixAdminSocketError::RuntimeDirectoryUnavailable { kind: error.kind() }
- })?;
let writer_lock = open_writer_lock(&directory, expected_uid)?;
FileExt::try_lock_exclusive(&writer_lock).map_err(|error| {
if error.kind() == io::ErrorKind::WouldBlock {
@@ -141,12 +161,37 @@ impl UnixAdminSocketWriterAuthority {
UnixAdminSocketError::WriterLockUnavailable { kind: error.kind() }
}
})?;
+ let expected_gid = peer_authorization.admin_gid();
+ if let Some(admin_gid) = expected_gid {
+ fchown(&directory, None, Some(Gid::from_raw(admin_gid))).map_err(|error| {
+ UnixAdminSocketError::RuntimeDirectoryGroup {
+ kind: errno_kind(error),
+ }
+ })?;
+ }
+ let directory_permissions = if expected_gid.is_some() {
+ Mode::RWXU | Mode::RGRP | Mode::XGRP
+ } else {
+ Mode::RWXU
+ };
+ let directory_mode: u32 = directory_permissions.bits().into();
+ fchmod(&directory, directory_permissions).map_err(|error| {
+ UnixAdminSocketError::RuntimeDirectoryPermissions {
+ kind: errno_kind(error),
+ }
+ })?;
+ let directory_metadata = directory.metadata().map_err(|error| {
+ UnixAdminSocketError::RuntimeDirectoryUnavailable { kind: error.kind() }
+ })?;
let authority = Self {
runtime_directory,
_directory: directory,
directory_identity: FileIdentity::from_metadata(&directory_metadata),
expected_uid,
+ expected_gid,
+ directory_mode,
+ peer_authorization,
_writer_lock: writer_lock,
};
authority.ensure_directory_identity()?;
@@ -173,6 +218,8 @@ impl UnixAdminSocketWriterAuthority {
return Err(UnixAdminSocketError::RuntimeDirectoryChanged);
}
if metadata.uid() != self.expected_uid
+ || self.expected_gid.is_some_and(|gid| metadata.gid() != gid)
+ || metadata.permissions().mode() & 0o777 != self.directory_mode
|| FileIdentity::from_metadata(&metadata) != self.directory_identity
{
return Err(UnixAdminSocketError::RuntimeDirectoryChanged);
@@ -181,7 +228,7 @@ impl UnixAdminSocketWriterAuthority {
}
}
-/// A bound owner-only Unix admin listener with identity-safe cleanup.
+/// A bound Unix admin listener with policy-aligned modes and identity-safe cleanup.
pub struct UnixAdminSocketBinding {
listener: UnixListener,
socket_path: PathBuf,
@@ -225,13 +272,45 @@ impl UnixAdminSocketBinding {
return Err(error);
}
};
- if let Err(error) = fs::set_permissions(
+ if let Some(admin_gid) = authority.expected_gid
+ && let Err(error) = chownat(
+ &authority._directory,
+ socket_path
+ .file_name()
+ .expect("resolved socket path always has a file name"),
+ None,
+ Some(Gid::from_raw(admin_gid)),
+ AtFlags::SYMLINK_NOFOLLOW,
+ )
+ {
+ drop(listener);
+ remove_matching_socket(&authority, &socket_path, socket_identity);
+ return Err(UnixAdminSocketError::SocketGroup {
+ kind: errno_kind(error),
+ });
+ }
+ let socket_mode = if authority.expected_gid.is_some() {
+ UNIX_ADMIN_GROUP_SOCKET_MODE
+ } else {
+ UNIX_ADMIN_OWNER_SOCKET_MODE
+ };
+ if let Err(error) =
+ fs::set_permissions(&socket_path, fs::Permissions::from_mode(socket_mode))
+ {
+ drop(listener);
+ remove_matching_socket(&authority, &socket_path, socket_identity);
+ return Err(UnixAdminSocketError::SocketPermissions { kind: error.kind() });
+ }
+ if let Err(error) = verify_bound_socket(
&socket_path,
- fs::Permissions::from_mode(UNIX_ADMIN_OWNER_SOCKET_MODE),
+ socket_identity,
+ authority.expected_uid,
+ authority.expected_gid,
+ socket_mode,
) {
drop(listener);
remove_matching_socket(&authority, &socket_path, socket_identity);
- return Err(UnixAdminSocketError::SocketPermissions { kind: error.kind() });
+ return Err(error);
}
if let Err(error) = listener.set_nonblocking(true) {
drop(listener);
@@ -247,11 +326,22 @@ impl UnixAdminSocketBinding {
})
}
- /// Borrows the nonblocking listener without transferring cleanup authority.
- #[must_use]
- pub fn listener(&self) -> &UnixListener {
+ pub(crate) fn listener(&self) -> &UnixListener {
&self.listener
}
+
+ /// Returns the policy shared by socket permissions and peer admission.
+ #[must_use]
+ pub const fn peer_authorization(&self) -> AdminPeerAuthorizationPolicy {
+ self.authority.peer_authorization
+ }
+
+ pub(crate) const fn peer_authorizer(&self) -> PeerAuthorizer {
+ PeerAuthorizer::new(
+ self.authority.peer_authorization,
+ self.authority.expected_uid,
+ )
+ }
}
impl Drop for UnixAdminSocketBinding {
@@ -374,6 +464,36 @@ fn inspect_socket(
}
}
+fn verify_bound_socket(
+ socket_path: &Path,
+ expected_identity: FileIdentity,
+ expected_uid: u32,
+ expected_gid: Option<u32>,
+ expected_mode: u32,
+) -> Result<(), UnixAdminSocketError> {
+ let metadata = fs::symlink_metadata(socket_path)
+ .map_err(|error| UnixAdminSocketError::SocketPathUnavailable { kind: error.kind() })?;
+ if !metadata.file_type().is_socket()
+ || FileIdentity::from_metadata(&metadata) != expected_identity
+ {
+ return Err(UnixAdminSocketError::SocketPathWrongType);
+ }
+ if metadata.uid() != expected_uid {
+ return Err(UnixAdminSocketError::SocketPathWrongOwner);
+ }
+ if expected_gid.is_some_and(|gid| metadata.gid() != gid) {
+ return Err(UnixAdminSocketError::SocketGroup {
+ kind: io::ErrorKind::PermissionDenied,
+ });
+ }
+ if metadata.permissions().mode() & 0o777 != expected_mode {
+ return Err(UnixAdminSocketError::SocketPermissions {
+ kind: io::ErrorKind::PermissionDenied,
+ });
+ }
+ Ok(())
+}
+
fn remove_matching_socket(
authority: &UnixAdminSocketWriterAuthority,
socket_path: &Path,
@@ -442,9 +562,51 @@ mod tests {
fn owner_only_mode_inventory_is_literal_and_stable() {
assert_eq!(UNIX_ADMIN_OWNER_DIRECTORY_MODE, 0o700);
assert_eq!(UNIX_ADMIN_OWNER_SOCKET_MODE, 0o600);
+ assert_eq!(UNIX_ADMIN_GROUP_DIRECTORY_MODE, 0o750);
+ assert_eq!(UNIX_ADMIN_GROUP_SOCKET_MODE, 0o660);
assert_eq!(u32::from(Mode::RWXU.bits()), 0o700);
}
+ #[cfg(target_os = "linux")]
+ #[tokio::test]
+ async fn configured_admin_group_sets_group_access_modes_and_identity() {
+ let directory = tempfile::tempdir().expect("temporary directory");
+ let socket = directory.path().join("admin.sock");
+ let admin_gid = rustix::process::getegid().as_raw();
+ let policy = AdminPeerAuthorizationPolicy::with_admin_gid(admin_gid)
+ .expect("current Linux group is valid");
+ let authority = UnixAdminSocketWriterAuthority::acquire_with_peer_authorization(
+ directory.path(),
+ policy,
+ )
+ .expect("group writer authority");
+ let binding = UnixAdminSocketBinding::bind(authority, &socket)
+ .await
+ .expect("group admin binding");
+
+ assert_eq!(mode(directory.path()), UNIX_ADMIN_GROUP_DIRECTORY_MODE);
+ assert_eq!(mode(&socket), UNIX_ADMIN_GROUP_SOCKET_MODE);
+ assert_eq!(
+ fs::symlink_metadata(directory.path())
+ .expect("directory metadata")
+ .gid(),
+ admin_gid
+ );
+ assert_eq!(
+ fs::symlink_metadata(&socket)
+ .expect("socket metadata")
+ .gid(),
+ admin_gid
+ );
+ assert_eq!(binding.peer_authorization(), policy);
+
+ let error = UnixAdminSocketWriterAuthority::acquire(directory.path())
+ .expect_err("active group-authorized writer excludes owner-only reconfiguration");
+ assert_eq!(error, UnixAdminSocketError::WriterAlreadyActive);
+ assert_eq!(mode(directory.path()), UNIX_ADMIN_GROUP_DIRECTORY_MODE);
+ assert_eq!(mode(&socket), UNIX_ADMIN_GROUP_SOCKET_MODE);
+ }
+
#[tokio::test]
async fn refuses_a_live_socket_owned_outside_the_writer_guard() {
let directory = tempfile::tempdir().expect("temporary directory");
diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs
@@ -16,6 +16,7 @@ pub use admin::{
AdminContractVersionError, AdminCorrelationId, AdminError, AdminErrorCode, AdminErrorCodeError,
AdminErrorMessage, AdminErrorMessageError, AdminFailureResponse, AdminIdentifierError,
AdminIdentifierField, AdminMutationRequest, AdminOperationId, AdminPayloadError,
+ AdminPeerAuthorizationPolicy, AdminPeerAuthorizationPolicyError, AdminPeerAuthorizationSupport,
AdminSuccessResponse, AdminTransportLimitField, AdminTransportLimitValues,
AdminTransportLimits, AdminTransportLimitsError,
};
@@ -28,8 +29,9 @@ pub use admin::{
AdminRouteFailureStatus, AdminRouteOutcome, AdminRouteOutcomeError, AdminRoutePath,
AdminRoutePathError, AdminRouteRegistrationError, AdminRouter, AdminServer,
AdminServerConfigError, AdminServerError, UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT,
- UNIX_ADMIN_OWNER_DIRECTORY_MODE, UNIX_ADMIN_OWNER_SOCKET_MODE, UnixAdminSocketBinding,
- UnixAdminSocketError, UnixAdminSocketWriterAuthority,
+ UNIX_ADMIN_GROUP_DIRECTORY_MODE, UNIX_ADMIN_GROUP_SOCKET_MODE, UNIX_ADMIN_OWNER_DIRECTORY_MODE,
+ UNIX_ADMIN_OWNER_SOCKET_MODE, UnixAdminSocketBinding, UnixAdminSocketError,
+ UnixAdminSocketWriterAuthority,
};
pub use build_info::{
BuildInfo, BuildInfoEnvironment, BuildInfoError, BuildInfoField, BuildMode, ContractVersions,
diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs
@@ -7,6 +7,7 @@ const ADMIN_SOURCE: &str = concat!(
include_str!("../src/admin/client.rs"),
include_str!("../src/admin/limits.rs"),
include_str!("../src/admin/model.rs"),
+ include_str!("../src/admin/peer.rs"),
include_str!("../src/admin/server.rs"),
include_str!("../src/admin/unix.rs"),
);