feat(3088): move the gateway's nginx + dnsmasq onto the host
The gateway's nginx + dnsmasq no longer run in their own nspawn container. `nix/host-modules/hive-gateway/default.nix` loses the `containers.hive-gateway` wrapper and everything that existed only to punch holes in it: `privateNetwork = false`, `CAP_NET_ADMIN`, five bind mounts, its own `stateVersion`, `networking.firewall.enable = false`, `networking.resolvconf.enable = false`, and the `hive-gateway-resolv` path+service pair. 465 -> 303 lines. The container never bought isolation here. It shared the host netns by necessity — nginx binds the host's :80/:443, dnsmasq answers on the bridge — so each of those settings was undoing a boundary the gateway could not afford in the first place. Four things made it more than a deletion, none of them visible in the nix diff: - The self-signed cert service also imports the hive CA leaf, so removing it with the container would have left nginx naming a missing cert file, which it refuses to load at all. - The nginx reload is a hive-priv verb. It still needs root, but no longer for the reason its doc gave, and `--machine=` was both transport and scope — so the unit name is now hard-coded in the helper as the containment. - The lifecycle verb named a container that stops existing. - `journalctl -M hive-gateway` had no machine to enter. Per the operator's ruling, the operator verb keeps working and agents lose it. `InfraContainer` answered three questions that used to share an answer; it now splits into `name()` (identity), `target()` (Container vs HostUnit), `service_unit()` (the systemd unit), and `agent_restartable()`, which the MCP restart path checks before the capability so the refusal cannot read as "ask for infra_admin". `SIBLING_CONTAINERS` drops the gateway — it gates the requests that name a container as a string — while `FromStr` still accepts it, because that answers what a name is, not who may act on it. The dashboard's gateway journal reads host journald filtered to `nginx.service`. Prose was corrected where it only named a location, and re-argued where the container was doing security work: a `0666` per-agent socket was safe because only the gateway container had the directory bind-mounted. There is no mount now, so the directory permissions are the whole of the access control — the constraint holds, its mechanism doesn't. Gate: nix fmt / clippy --all-targets -D warnings / cargo test all clean (710 tests); hivectl-cli.md regenerated from the clap tree. The nix eval was run in both TLS shapes at this commit: every delta in the rendered virtualHosts is one of the three intended path moves, dnsmasq settings are byte-identical, and the absence probe flips true -> false with bindMounts emptied.
This commit is contained in:
parent
cae2cf8df6
commit
07852cabc1
34 changed files with 704 additions and 618 deletions
|
|
@ -35,17 +35,18 @@ The socket is `0666`. It has to be: nginx runs as a different user and
|
|||
sockets, and rests on the same argument — *"the bind source dir is per-agent on
|
||||
host so blast radius is unchanged."*
|
||||
|
||||
What keeps that safe is that the directory holds **one** socket and is
|
||||
bind-mounted into **one** container. So:
|
||||
What keeps that safe is that the directory holds **one** socket. So:
|
||||
|
||||
> **Never point `socketPath` at a directory that carries anything else.**
|
||||
> `/run/hyperhive` above all — it holds `host.sock`, the host **admin** socket.
|
||||
> Mounting that directory to reach this socket would hand the gateway container
|
||||
> the admin socket along with it.
|
||||
> Pointing nginx at that directory to reach this socket would put the admin
|
||||
> socket within its reach too.
|
||||
|
||||
Changing `socketPath` therefore means re-checking the gateway bind-mount, not
|
||||
just the daemon. A unit test pins the default path so a tidying edit fails
|
||||
instead of reviewing cleanly.
|
||||
This got *less* forgiving when nginx moved onto the host: the gateway used to
|
||||
reach a unix upstream through a bind-mount, so the mount list was a second
|
||||
bound on what it could touch. There is no mount now — the directory is the
|
||||
whole of the access control. A unit test pins the default path so a tidying
|
||||
edit fails instead of reviewing cleanly.
|
||||
|
||||
`RuntimeDirectoryPreserve=yes` and the daemon's stale-socket unlink on start are
|
||||
a **pair**: preserving the directory without the unlink means `bind` fails with
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ use axum::{Router, routing::get};
|
|||
/// `ExecStart`, so the default names a directory the unit just produced.
|
||||
///
|
||||
/// The directory is its own — deliberately not shared with hive-c0re's
|
||||
/// `/run/hyperhive`. nginx reaches a unix upstream by having the socket's
|
||||
/// *directory* bind-mounted into the gateway container, so co-locating
|
||||
/// this socket with c0re's admin socket would hand the gateway the admin
|
||||
/// socket along with it.
|
||||
/// `/run/hyperhive`. The socket is `0666`, so its directory is the only
|
||||
/// access control it has; co-locating it with c0re's admin socket would
|
||||
/// put both within reach of whatever can reach either. That used to be
|
||||
/// enforced by which *directory* was bind-mounted into the gateway
|
||||
/// container; with nginx on the host the mount is gone and the directory
|
||||
/// is all that is left, so the rule matters more, not less.
|
||||
const DEFAULT_SOCKET: &str = "/run/swarm-controller/controller.sock";
|
||||
|
||||
fn socket_path() -> PathBuf {
|
||||
|
|
@ -96,8 +98,9 @@ mod tests {
|
|||
/// The socket must not share a directory with anything else, because
|
||||
/// the socket is `0666` and the directory is therefore the only access
|
||||
/// control it has. `/run/hyperhive` in particular holds hive-c0re's
|
||||
/// **admin** socket, and nginx reaches a unix upstream by mounting the
|
||||
/// socket's whole directory into the gateway container.
|
||||
/// **admin** socket. nginx used to reach a unix upstream by mounting
|
||||
/// the socket's whole directory into the gateway container; it runs on
|
||||
/// the host now, so nothing narrows its reach but the directory itself.
|
||||
///
|
||||
/// A test rather than a comment: the failure this guards against is a
|
||||
/// one-word edit that looks tidier and reads fine in review.
|
||||
|
|
@ -111,7 +114,7 @@ mod tests {
|
|||
Path::new("/run/swarm-controller"),
|
||||
"the socket's directory is its access control — moving it under a shared \
|
||||
directory (notably /run/hyperhive, which holds the host admin socket) \
|
||||
exposes everything else in that directory to the gateway container"
|
||||
exposes everything else in that directory to the gateway's nginx"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue