docs: move privsep socket-activation + child-state rw rationale out of code comments
This commit is contained in:
parent
58b5434466
commit
fb1f7efbe4
4 changed files with 40 additions and 23 deletions
|
|
@ -59,3 +59,19 @@ The `area:ops` issues followed this sequencing:
|
||||||
runs as the unprivileged `hive-core` user and delegates root
|
runs as the unprivileged `hive-core` user and delegates root
|
||||||
operations to `hive-priv`, a narrow socket-activated helper. See
|
operations to `hive-priv`, a narrow socket-activated helper. See
|
||||||
[`docs/security.md`](security.md) for the privilege boundary table.
|
[`docs/security.md`](security.md) for the privilege boundary table.
|
||||||
|
|
||||||
|
### hive-priv socket activation
|
||||||
|
|
||||||
|
`hive-priv` is **always** socket-activated by the `hive-priv.socket`
|
||||||
|
systemd unit. The unit binds `/run/hive/priv.sock` with
|
||||||
|
`SocketGroup=hive-core` and mode `0660` and passes the ready listener
|
||||||
|
to the helper as fd 3 (`LISTEN_FDS`). The helper requires this and
|
||||||
|
bails if it isn't socket-activated — there is intentionally no
|
||||||
|
self-bind fallback.
|
||||||
|
|
||||||
|
Dropping the old fallback removed a dev/prod divergence: when
|
||||||
|
`hive-priv` bound the socket itself it created the file owned by
|
||||||
|
root's primary group rather than `hive-core`, so a `hive-core` client
|
||||||
|
couldn't connect the way the socket unit's `SocketGroup` grant
|
||||||
|
intends. Requiring socket activation everywhere means dev and prod
|
||||||
|
take the exact same path and the group grant always holds.
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,20 @@ Under `/var/lib/hyperhive/agents/<name>/`:
|
||||||
- `hyperhive-turn-stats.sqlite` — per-turn timing stats.
|
- `hyperhive-turn-stats.sqlite` — per-turn timing stats.
|
||||||
- `hyperhive-model` — single-line model name override file.
|
- `hyperhive-model` — single-line model name override file.
|
||||||
|
|
||||||
|
### Parent access to child state
|
||||||
|
|
||||||
|
A parent agent gets each direct child's `state`, `harness`, and
|
||||||
|
`config` dirs bind-mounted **read-write** (`bind_child_agent_dirs` in
|
||||||
|
`lifecycle.rs`). The RW on `state` is deliberate, not an oversight: a
|
||||||
|
parent manages its children, which includes writing into a child's
|
||||||
|
state for recovery (e.g. seeding notes, clearing a stuck sentinel) as
|
||||||
|
well as reading it. `config` is RW because the parent authors proposed
|
||||||
|
config changes for the child (the approval flow commits into the
|
||||||
|
child's config repo), and `harness` is RW for the same management
|
||||||
|
reasons. Per-child isolation still holds: a container only ever has
|
||||||
|
its *own* dirs plus its direct children's bind-mounted, never a
|
||||||
|
sibling's.
|
||||||
|
|
||||||
Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only
|
Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only
|
||||||
applied repo. Tracks `flake.nix` (module-only boilerplate; never
|
applied repo. Tracks `flake.nix` (module-only boilerplate; never
|
||||||
edited after first spawn) + `agent.nix` (the actual config; the
|
edited after first spawn) + `agent.nix` (the actual config; the
|
||||||
|
|
|
||||||
|
|
@ -1100,18 +1100,10 @@ const HOST_META_ROOT: &str = "/var/lib/hyperhive/meta";
|
||||||
const HOST_SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
|
const HOST_SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
|
||||||
|
|
||||||
/// Append bind flags for `child`'s state, harness, and config dirs into
|
/// Append bind flags for `child`'s state, harness, and config dirs into
|
||||||
/// `binds`. All three are RW so the parent can read/write state and
|
/// `binds`, all read-write. The RW on `state` is deliberate (recovery),
|
||||||
/// submit config-change requests. Creates missing host-side directories
|
/// not an oversight; see docs/persistence.md ("Parent access to child
|
||||||
/// so nspawn doesn't refuse to start; missing dirs are non-fatal.
|
/// state") for the rationale. Creates missing host-side directories so
|
||||||
/// Mount a child agent's `state`, `harness`, and `config` dirs into the
|
/// nspawn doesn't refuse to start; missing dirs are non-fatal.
|
||||||
/// parent, all read-write. The RW on `state` is deliberate (not just a
|
|
||||||
/// read mount): a parent manages its children, which includes writing
|
|
||||||
/// into a child's state for recovery (e.g. seeding notes / clearing a
|
|
||||||
/// stuck sentinel) as well as reading it. `config` is RW because the
|
|
||||||
/// parent authors proposed config changes for the child (the approval
|
|
||||||
/// flow commits into the child's config repo). `harness` is RW for the
|
|
||||||
/// same management reasons. Per-child isolation still holds: a child
|
|
||||||
/// only ever has its *own* dirs bind-mounted, never a sibling's.
|
|
||||||
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
||||||
let state_dir = format!("{HOST_AGENTS_ROOT}/{child}/state");
|
let state_dir = format!("{HOST_AGENTS_ROOT}/{child}/state");
|
||||||
let harness_dir = format!("{HOST_AGENTS_ROOT}/{child}/harness");
|
let harness_dir = format!("{HOST_AGENTS_ROOT}/{child}/harness");
|
||||||
|
|
@ -1229,9 +1221,9 @@ async fn set_nspawn_flags(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Topology-driven child mounts: every direct child of this agent gets
|
// Topology-driven child mounts: every direct child of this agent gets
|
||||||
// its state, harness, and config dirs bind-mounted RW so the parent
|
// its state, harness, and config dirs bind-mounted RW (parent reads +
|
||||||
// can read AND write child state (recovery) and manage its config.
|
// writes child state for recovery, and manages config). See
|
||||||
// See `bind_child_agent_dirs` for why state is RW, not read-only.
|
// `bind_child_agent_dirs`.
|
||||||
let direct_children = crate::topology::children_of(agent_name);
|
let direct_children = crate::topology::children_of(agent_name);
|
||||||
for child in &direct_children {
|
for child in &direct_children {
|
||||||
bind_child_agent_dirs(child, &mut binds);
|
bind_child_agent_dirs(child, &mut binds);
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,9 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn socket_listener() -> Result<UnixListener> {
|
fn socket_listener() -> Result<UnixListener> {
|
||||||
// hive-priv is ALWAYS socket-activated: systemd's `hive-priv.socket`
|
// hive-priv is ALWAYS socket-activated by the `hive-priv.socket` unit
|
||||||
// unit binds `/run/hive/priv.sock` (SocketGroup=hive-core, mode 0660)
|
// (fd 3 via LISTEN_FDS). There is intentionally no self-bind fallback,
|
||||||
// and passes it as fd 3 via LISTEN_FDS. We require that — there is
|
// so dev and prod take the same path; see docs/boundary.md.
|
||||||
// intentionally no self-bind fallback, so dev and prod take the exact
|
|
||||||
// same path. (The old fallback re-bound the socket itself as root's
|
|
||||||
// primary group, never `hive-core`, so a hive-core client couldn't
|
|
||||||
// connect the way the socket unit's grant intends; dropping it removes
|
|
||||||
// that dev/prod divergence.)
|
|
||||||
let listen_fds: Option<i32> = std::env::var("LISTEN_FDS")
|
let listen_fds: Option<i32> = std::env::var("LISTEN_FDS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse().ok());
|
.and_then(|s| s.parse().ok());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue