hive-c0re: grant hive-admin group a polkit rule for choom
This commit is contained in:
parent
f49eef299b
commit
66c3138dd1
3 changed files with 89 additions and 20 deletions
|
|
@ -214,11 +214,15 @@ new containers and restarts pick the values up immediately.
|
||||||
|
|
||||||
Drop into an interactive Claude session inside an agent container.
|
Drop into an interactive Claude session inside an agent container.
|
||||||
Replaces the current process with `machinectl shell <name>@h-<name>`
|
Replaces the current process with `machinectl shell <name>@h-<name>`
|
||||||
running claude from the agent's state dir. Requires root (same as all
|
running claude from the agent's state dir. Requires root, or membership in
|
||||||
`machinectl shell` operations) — hyperhive ships no polkit rule granting
|
`hive-admin` (the same sudoless-`hivectl` group as the [host admin
|
||||||
those actions to the operator group, so `choom` refuses up front with a
|
socket](../trust-boundary/boundary.md) — hive-c0re ships a polkit rule
|
||||||
message naming that requirement rather than letting systemd reject the
|
granting that group the one action `machinectl shell` needs); `choom`
|
||||||
exec later.
|
checks this itself and refuses up front with a message naming the
|
||||||
|
requirement rather than letting systemd reject the exec later with an
|
||||||
|
opaque polkit prompt. Same caveat as the socket grant: a shell opened
|
||||||
|
before you were added to `hive-admin` won't see it until you log back in
|
||||||
|
— secondary group membership applies at login.
|
||||||
|
|
||||||
It also needs the daemon socket, unlike the other exec-into-a-container
|
It also needs the daemon socket, unlike the other exec-into-a-container
|
||||||
paths: the "is this actually an agent?" pre-flight reads the agents root,
|
paths: the "is this actually an agent?" pre-flight reads the agents root,
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,65 @@ use anyhow::{Result, bail};
|
||||||
|
|
||||||
use crate::util::agent_exists;
|
use crate::util::agent_exists;
|
||||||
|
|
||||||
/// Refuse early when the caller isn't root.
|
/// Whether the caller's supplementary groups include `hive-admin`.
|
||||||
///
|
///
|
||||||
/// `machinectl shell` needs root or a polkit grant, and hyperhive ships
|
/// hive-c0re's polkit rule (`nix/host-modules/hive-c0re/default.nix`) grants
|
||||||
/// no polkit rules — so unprivileged callers hit an authentication
|
/// `org.freedesktop.machine1.shell` to this group, so a member reaches the
|
||||||
/// prompt/refusal from systemd that says nothing about which hivectl
|
/// same outcome as root without one. `getgrnam`/`getgroups` read the
|
||||||
/// verb wanted it. Naming the requirement here keeps the failure honest
|
/// *process's* actual credentials, the same ones the kernel would check —
|
||||||
/// and points at the one thing that would fix it.
|
/// not a fresh `/etc/group` lookup by name — so this can say no for a user
|
||||||
fn require_root() -> Result<()> {
|
/// who was just added to the group but hasn't logged back in yet, same
|
||||||
// SAFETY: `geteuid` takes no arguments, reads a process attribute
|
/// caveat as the host-socket grant (`hivectl/src/client.rs::connect_hint`).
|
||||||
// the kernel always has, and cannot fail.
|
fn in_hive_admin_group() -> bool {
|
||||||
|
// SAFETY: `getgrnam` takes a valid NUL-terminated string literal and
|
||||||
|
// returns either null (no such group) or a pointer to static storage
|
||||||
|
// owned by libc, which is read once, immediately, before any other
|
||||||
|
// libc call could invalidate it.
|
||||||
|
let gid = unsafe {
|
||||||
|
let grp = libc::getgrnam(c"hive-admin".as_ptr());
|
||||||
|
if grp.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(*grp).gr_gid
|
||||||
|
};
|
||||||
|
// SAFETY: a zero-length call with a null buffer is documented to return
|
||||||
|
// the caller's supplementary-group count without writing anything, so
|
||||||
|
// the real buffer below is sized to exactly what the second call needs.
|
||||||
|
let count = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
|
||||||
|
let Ok(count) = usize::try_from(count) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut groups = vec![0u32; count];
|
||||||
|
let Ok(capacity) = i32::try_from(groups.len()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// SAFETY: `capacity` is exactly `groups.len()`, so this write cannot
|
||||||
|
// overflow the buffer.
|
||||||
|
let n = unsafe { libc::getgroups(capacity, groups.as_mut_ptr()) };
|
||||||
|
let Ok(n) = usize::try_from(n) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
groups.truncate(n);
|
||||||
|
groups.contains(&gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refuse early when the caller can't actually run `machinectl shell`.
|
||||||
|
///
|
||||||
|
/// Root always can. So can a `hive-admin` member, via the polkit grant
|
||||||
|
/// above. Anyone else hits an opaque polkit authentication prompt/refusal
|
||||||
|
/// from systemd that names neither hivectl nor the fix — naming both here
|
||||||
|
/// instead is the entire point of this check.
|
||||||
|
fn require_privilege() -> Result<()> {
|
||||||
|
// SAFETY: `geteuid` takes no arguments, reads a process attribute the
|
||||||
|
// kernel always has, and cannot fail.
|
||||||
let euid = unsafe { libc::geteuid() };
|
let euid = unsafe { libc::geteuid() };
|
||||||
if euid != 0 {
|
if euid != 0 && !in_hive_admin_group() {
|
||||||
bail!(
|
bail!(
|
||||||
"choom needs root: it runs `machinectl shell`, which requires root \
|
"choom needs root or `hive-admin` group membership: it runs `machinectl shell`, \
|
||||||
(hyperhive ships no polkit rule granting it) - re-run with sudo"
|
gated by a polkit rule for that group. If you were just added to `hive-admin`, \
|
||||||
|
log out and back in first — secondary group membership applies at login, so a \
|
||||||
|
shell opened before the change still won't have it. `id -nG` shows what your \
|
||||||
|
running shell actually has"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -36,8 +80,8 @@ fn require_root() -> Result<()> {
|
||||||
/// user from its state dir, reproducing the harness's per-turn claude
|
/// user from its state dir, reproducing the harness's per-turn claude
|
||||||
/// invocation (flags, session selection, why it never collides with the
|
/// invocation (flags, session selection, why it never collides with the
|
||||||
/// live harness session). See `docs/tools/hivectl.md` (Choom) for the
|
/// live harness session). See `docs/tools/hivectl.md` (Choom) for the
|
||||||
/// full rationale. Inherits the caller's PTY; requires root + a running
|
/// full rationale. Inherits the caller's PTY; requires root or `hive-admin`
|
||||||
/// container.
|
/// group membership, plus a running container.
|
||||||
///
|
///
|
||||||
/// The existence check goes over the daemon socket even though the exec
|
/// The existence check goes over the daemon socket even though the exec
|
||||||
/// itself doesn't need it: the agents root isn't readable by the
|
/// itself doesn't need it: the agents root isn't readable by the
|
||||||
|
|
@ -50,7 +94,7 @@ pub(crate) async fn choom(socket: &Path, name: &str, resume_session: Option<&str
|
||||||
hive_host_sock::AGENTS_ROOT
|
hive_host_sock::AGENTS_ROOT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
require_root()?;
|
require_privilege()?;
|
||||||
let container = hive_host_sock::container_name(name);
|
let container = hive_host_sock::container_name(name);
|
||||||
// Enter as the agent's unix user (== agent name) so claude reads the
|
// Enter as the agent's unix user (== agent name) so claude reads the
|
||||||
// right `$HOME/.claude`.
|
// right `$HOME/.claude`.
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,27 @@ in
|
||||||
members = cfg.adminUsers;
|
members = cfg.adminUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Let `hive-admin` members run `hivectl agent <name> choom`
|
||||||
|
# (`machinectl shell`) without sudo, same opt-in-by-group shape as the
|
||||||
|
# socket grant above. `machinectl shell <name>@h-<name>` triggers exactly
|
||||||
|
# one polkit action, `org.freedesktop.machine1.shell` ("Acquire a shell
|
||||||
|
# in a local container") — not `.login` (a different verb, `machinectl
|
||||||
|
# login`, which `choom` never calls) and not `.host-shell` (the
|
||||||
|
# host-target variant, for a bare `machinectl shell` with no `@machine`
|
||||||
|
# suffix; `choom` always targets `<name>@h-<name>`). `enable = true` is
|
||||||
|
# required here: `security.polkit.extraConfig` is silently dropped
|
||||||
|
# unless the module itself is turned on, and nothing else in a headless
|
||||||
|
# hive pulls polkit in the way a desktop session would.
|
||||||
|
security.polkit.enable = true;
|
||||||
|
security.polkit.extraConfig = ''
|
||||||
|
polkit.addRule(function(action, subject) {
|
||||||
|
if (action.id == "org.freedesktop.machine1.shell" &&
|
||||||
|
subject.isInGroup("hive-admin")) {
|
||||||
|
return polkit.Result.YES;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
'';
|
||||||
|
|
||||||
# The gateway nginx is always the sole external entry point (it runs
|
# The gateway nginx is always the sole external entry point (it runs
|
||||||
# alongside hyperhive), so the per-agent web-port range stays closed on
|
# alongside hyperhive), so the per-agent web-port range stays closed on
|
||||||
# the host firewall. See `docs/networking/gateway.md::Firewall posture (host-level)`.
|
# the host firewall. See `docs/networking/gateway.md::Firewall posture (host-level)`.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue