Crates.io | priv_sep |
lib.rs | priv_sep |
version | 3.0.0-alpha.1.3 |
created_at | 2023-07-25 06:19:55.703818+00 |
updated_at | 2025-08-26 14:15:54.694125+00 |
description | FFI for setresuid(2), setresgid(2), chroot(2), pledge(2), and unveil(2). |
homepage | |
repository | https://git.philomathiclife.com/repos/priv_sep/ |
max_upload_size | |
id | 925219 |
size | 134,454 |
priv_sep
priv_sep
is a library that uses the system's libc to perform privilege separation and privilege reduction.
priv_sep
in action for OpenBSDuse core::convert::Infallible;
use priv_sep::{Permissions, PrivDropErr, Promise, Promises};
use std::{
fs,
io::Error,
net::{Ipv6Addr, SocketAddrV6},
};
use tokio::net::TcpListener;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<Infallible, PrivDropErr<Error>> {
/// Config file.
const CONFIG: &str = "config";
// Get the user ID and group ID for nobody from `passwd(5)`.
// `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`.
// `pledge(2)` `id`, `inet`, `rpath`, `stdio`, and `unveil`.
// Bind to TCP `[::1]:443` as root.
// `setresgid(2)` to the group ID associated with nobody.
// `setresuid(2)` to the user ID associated with nobody.
// Remove `id` from our `pledge(2)`d promises.
let (listener, mut promises) = Promises::new_chroot_then_priv_drop_async(
"nobody",
"/path/chroot/",
[Promise::Inet, Promise::Rpath, Promise::Unveil],
false,
async || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await,
).await?;
// At this point, the process is running under nobody.
// Only allow file system access to `config` and only allow read access to it.
Permissions::READ.unveil(CONFIG)?;
// Read `config`.
// This will of course fail if the file does not exist or nobody does not
// have read permissions.
let config = fs::read(CONFIG)?;
// Remove file system access.
Permissions::NONE.unveil(CONFIG)?;
// Remove `rpath` and `unveil` from our `pledge(2)`d promises
// (i.e., only have `inet` and `stdio` abilities when we begin accepting TCP connections).
promises.remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])?;
loop {
// Handle TCP connections.
if let Ok((_, ip)) = listener.accept().await {
assert!(ip.is_ipv6());
}
}
}
priv_sep
in action for Unix-like OSesuse core::convert::Infallible;
use priv_sep::{UserInfo, PrivDropErr};
use std::{
io::Error,
net::{Ipv6Addr, SocketAddrV6},
};
use tokio::net::TcpListener;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<Infallible, PrivDropErr<Error>> {
// Get the user ID and group ID for nobody from `passwd(5)`.
// `chroot(2)` to `/path/chroot/` and `chdir(2)` to `/`.
// Bind to TCP `[::1]:443` as root.
// `setresgid(2)` to the group ID associated with nobody.
// `setresuid(2)` to the user ID associated with nobody.
let listener = UserInfo::chroot_then_priv_drop_async("nobody", "/path/chroot/", false, async || {
TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0)).await
}).await?;
// At this point, the process is running under nobody.
loop {
// Handle TCP connections.
if let Ok((_, ip)) = listener.accept().await {
assert!(ip.is_ipv6());
}
}
}
This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV will be updated.
MSRV changes will correspond to a SemVer minor version bump.
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Before any PR is sent, cargo clippy
and cargo t
should be run. Additionally
RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features
should be run to ensure documentation can be built.
The crate is only tested on the x86_64-unknown-linux-gnu
, x86_64-unknown-openbsd
, and aarch64-apple-darwin
targets; but it should work on most platforms.