//! Privilege drop: switch the process's effective + real + saved UID/GID to a non-root user after //! all privileged startup work is done (binding the TUN, binding low ports, configuring NAT). //! //! v1 spec from project notes: //! //! * Linux uses `setresgid(g,g,g)` + `setresuid(u,u,u)` (the full BSD-incompatible API; this also //! wipes the saved set-uid so the process can never `setuid(0)` back). //! * macOS does not expose `setresuid` in its BSD ABI — `nix` 0.29 provides `setgid` / `setuid` //! instead, which on macOS perform a permanent drop when the calling process is root. //! * Windows is a no-op (named pipes + service accounts cover the analogous use case there). //! //! The drop is **best-effort**: //! //! * If the current euid is not 0 (e.g. dev running `cargo test` as themselves), [`drop_to_user`] //! logs an info line and returns `Ok` without changing anything. //! * If the named user does not exist or the syscalls fail, the error bubbles up so `aura server` //! exits rather than silently continuing as root. //! //! Callers **must** invoke [`drop_to_user`] after every privileged operation completes; doing it //! earlier means the TUN open or NAT command may fail with EPERM. use anyhow::Result; #[cfg(unix)] use anyhow::{anyhow, Context}; /// Drop privileges to `username`. See module docs for platform behaviour. /// /// Returns `Ok(())` on: /// * a successful drop (Linux/macOS, called as root), /// * a no-op on Windows, /// * a no-op when already running as a non-root user (Linux/macOS). /// /// Returns `Err` only on Linux/macOS, when the user lookup or the syscalls themselves fail. #[cfg(unix)] pub fn drop_to_user(username: &str) -> Result<()> { use nix::unistd::{getuid, User}; if !getuid().is_root() { tracing::info!( target: "aura::privdrop", user = username, "privilege drop skipped: already running as a non-root user" ); return Ok(()); } let user = User::from_name(username) .with_context(|| format!("looking up user '{username}'"))? .ok_or_else(|| anyhow!("user '{username}' not found in passwd database"))?; let uid = user.uid; let gid = user.gid; // Order matters: drop GID first while we still have root, then UID. Doing UID first would // leave us as a non-root user that cannot setgid anymore. drop_uid_gid(uid, gid)?; tracing::info!( target: "aura::privdrop", user = username, uid = uid.as_raw(), gid = gid.as_raw(), "dropped privileges" ); Ok(()) } /// Windows stub: there is no analogous "drop to user" syscall sequence on Windows in v1. The /// server is expected to be run as a configured service account. #[cfg(windows)] pub fn drop_to_user(username: &str) -> Result<()> { tracing::warn!( target: "aura::privdrop", user = username, "privilege drop not implemented on Windows; run aura server as a low-privilege service account instead" ); Ok(()) } #[cfg(target_os = "linux")] fn drop_uid_gid(uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result<()> { use nix::unistd::{setresgid, setresuid}; // Full triple-drop on Linux: real + effective + saved. This guarantees the process cannot // regain root via setuid(0). setresgid(gid, gid, gid).with_context(|| format!("setresgid({})", gid.as_raw()))?; setresuid(uid, uid, uid).with_context(|| format!("setresuid({})", uid.as_raw()))?; Ok(()) } #[cfg(target_os = "macos")] fn drop_uid_gid(uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result<()> { use nix::unistd::{setgid, setuid}; // macOS does not expose setresuid in its BSD ABI. setgid/setuid perform a permanent drop // when invoked as root: the kernel zeroes the saved set-uid alongside the real and effective // ids, so this is just as strong as setresuid here. setgid(gid).with_context(|| format!("setgid({})", gid.as_raw()))?; setuid(uid).with_context(|| format!("setuid({})", uid.as_raw()))?; Ok(()) } // Other unix targets (BSDs, etc.) — fall back to setgid/setuid which exist everywhere POSIX. #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))] fn drop_uid_gid(uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result<()> { use nix::unistd::{setgid, setuid}; setgid(gid).with_context(|| format!("setgid({})", gid.as_raw()))?; setuid(uid).with_context(|| format!("setuid({})", uid.as_raw()))?; Ok(()) } #[cfg(test)] mod tests { use super::*; /// On a developer/CI host this test runs as a non-root user, so drop_to_user must be a /// no-op (no panic, no error, no actual privilege change). Verifies the early-return path. #[test] #[cfg(unix)] fn no_op_when_already_non_root() { // Use "nobody" as the requested user — it exists on every Unix CI image but we should // never actually call setuid on it because we are not root. let res = drop_to_user("nobody"); // If running as root (unlikely in CI), the test still completes successfully — the call // would actually drop us. Either way, the function returns Ok. assert!( res.is_ok(), "drop_to_user should return Ok on a non-root host, got: {res:?}" ); } #[test] #[cfg(windows)] fn windows_is_a_noop() { // Windows: always Ok, regardless of username. assert!(drop_to_user("any").is_ok()); } }