c6f0d7af9b
Three v2-hardening features in aura-cli, one pass:
- nat::NatGuard: RAII auto-config of IP forwarding + MASQUERADE on server
startup. Linux (sysctl ip_forward + iptables -t nat MASQUERADE) and
macOS (sysctl ip.forwarding + pfctl with /tmp/aura-nat.conf). dry_run
works on every platform (logs "would run: ..."). Reverts everything in
Drop. New [server.nat] {auto, egress_iface, dry_run}; absent section =
back-compat no-op. Removes v1's "manual NAT/forwarding" step.
- privdrop::drop_to_user: drop euid/gid after binding TUN + privileged
ports. Linux setresuid/setresgid, macOS setgid+setuid (permanent drop),
Windows no-op with warning. New [server] / [client] run_as = "..."
(optional). Skipped with info-log if already non-root.
- admin: split transport into cfg(unix) Unix-socket and cfg(windows) Tokio
named-pipe modules sharing one JSON-line serve/request flow over
AsyncRead/AsyncWrite. DEFAULT_SOCKET = "/tmp/aura-admin.sock" on Unix,
r"\\.\pipe\aura-admin" on Windows. Removes v1's "admin Unix-only".
Deps: nix 0.29 user feature under [target.'cfg(unix)'.dependencies] (cli-
local, not workspace). Workspace: 155 tests passed (+13), clippy -D warnings
clean, fmt clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
5.3 KiB
Rust
136 lines
5.3 KiB
Rust
//! 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());
|
|
}
|
|
}
|