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
|
|
@ -888,18 +888,23 @@ async fn register_ci_runner(token: &str) -> Result<(String, String)> {
|
|||
}
|
||||
|
||||
/// `ControlInfraContainer` — start/stop/restart a hive infrastructure
|
||||
/// container via `systemctl <verb> container@<container>.service`. The
|
||||
/// [`InfraContainer`] enum is the allowlist: serde already rejected any
|
||||
/// unknown / unsafe name (hive-c0re has no variant, so a stop can't sever
|
||||
/// the daemon socket) at deserialisation, so no root-side `.contains()`
|
||||
/// check is needed here. Serves both the hive-wide `hivectl stop`/`start`
|
||||
/// flow and an `infra_admin` agent's `restart` (action = Restart).
|
||||
/// service via `systemctl <verb> <unit>`. The [`InfraContainer`] enum is
|
||||
/// the allowlist: serde already rejected any unknown / unsafe name
|
||||
/// (hive-c0re has no variant, so a stop can't sever the daemon socket) at
|
||||
/// deserialisation, so no root-side `.contains()` check is needed here.
|
||||
/// Serves both the hive-wide `hivectl stop`/`start` flow and an
|
||||
/// `infra_admin` agent's `restart` (action = Restart).
|
||||
///
|
||||
/// ⚠️ The unit is derived from the variant, never sent by the caller —
|
||||
/// which is what keeps this from being a general `systemctl` pass-through.
|
||||
/// It is not always `container@<name>.service`: the gateway resolves to the
|
||||
/// host's `nginx.service`.
|
||||
async fn control_infra_container(
|
||||
container: InfraContainer,
|
||||
action: InfraAction,
|
||||
) -> Result<(String, String)> {
|
||||
let verb = action.systemctl_verb();
|
||||
let unit = format!("container@{}.service", container.unit_name());
|
||||
let unit = container.service_unit();
|
||||
let out = Command::new("systemctl")
|
||||
.args([verb, &unit])
|
||||
.output()
|
||||
|
|
@ -2173,34 +2178,33 @@ async fn read_container_journal(container: &str, query: &JournalQuery) -> Result
|
|||
Ok((stdout, stderr))
|
||||
}
|
||||
|
||||
/// Synchronise the nginx unit inside the `hive-gateway` container.
|
||||
/// Synchronise the host's nginx unit after an `agents.conf` write.
|
||||
///
|
||||
/// Queries `ActiveState` via `systemctl --machine=hive-gateway` (requires
|
||||
/// root — machine-bus transport enters the container namespace), then
|
||||
/// dispatches:
|
||||
/// Queries `ActiveState` and dispatches:
|
||||
/// - `active` → `systemctl reload nginx` (SIGHUP, zero-downtime)
|
||||
/// - `failed` → `systemctl reset-failed nginx` + `systemctl start nginx`
|
||||
/// - otherwise → `systemctl start nginx`
|
||||
///
|
||||
/// ⚠️ `nginx` is hard-coded on purpose — see `PrivRequest::ReloadGatewayNginx`.
|
||||
/// The unit name is the scope of this verb, and it used to be enforced by
|
||||
/// `--machine=hive-gateway` (which could only reach into that container).
|
||||
/// With nginx on the host there is no namespace to bound it, so the
|
||||
/// literal is the only thing standing between "reload the gateway" and
|
||||
/// "reload anything".
|
||||
///
|
||||
/// Returns `(String::new(), String::new())` on success so it fits the
|
||||
/// `exec` return type directly.
|
||||
async fn sync_gateway_nginx() -> Result<(String, String)> {
|
||||
let state_out = Command::new("systemctl")
|
||||
.args([
|
||||
"--machine=hive-gateway",
|
||||
"show",
|
||||
"--property=ActiveState",
|
||||
"--value",
|
||||
"nginx",
|
||||
])
|
||||
.args(["show", "--property=ActiveState", "--value", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
.context("query nginx ActiveState in hive-gateway")?;
|
||||
.context("query gateway nginx ActiveState")?;
|
||||
if !state_out.status.success() {
|
||||
tracing::warn!(
|
||||
exit_code = ?state_out.status.code(),
|
||||
stderr = %String::from_utf8_lossy(&state_out.stderr).trim(),
|
||||
"systemctl show ActiveState exited non-zero — gateway container may be down"
|
||||
"systemctl show ActiveState exited non-zero — gateway nginx may be down"
|
||||
);
|
||||
}
|
||||
let state = String::from_utf8_lossy(&state_out.stdout).trim().to_owned();
|
||||
|
|
@ -2209,10 +2213,10 @@ async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
match state.as_str() {
|
||||
"active" => {
|
||||
let out = Command::new("systemctl")
|
||||
.args(["--machine=hive-gateway", "reload", "nginx"])
|
||||
.args(["reload", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
.context("reload nginx in hive-gateway")?;
|
||||
.context("reload gateway nginx")?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"gateway nginx reload failed ({}): {}",
|
||||
|
|
@ -2224,14 +2228,14 @@ async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
"failed" => {
|
||||
// Clear start-limit so the next start can proceed.
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--machine=hive-gateway", "reset-failed", "nginx"])
|
||||
.args(["reset-failed", "nginx"])
|
||||
.status()
|
||||
.await;
|
||||
let out = Command::new("systemctl")
|
||||
.args(["--machine=hive-gateway", "start", "nginx"])
|
||||
.args(["start", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
.context("start nginx after reset-failed in hive-gateway")?;
|
||||
.context("start gateway nginx after reset-failed")?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"gateway nginx start (after reset-failed) failed ({}): {}",
|
||||
|
|
@ -2243,10 +2247,10 @@ async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
_ => {
|
||||
// inactive, activating, deactivating, unknown — just start.
|
||||
let out = Command::new("systemctl")
|
||||
.args(["--machine=hive-gateway", "start", "nginx"])
|
||||
.args(["start", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
.context("start nginx in hive-gateway")?;
|
||||
.context("start gateway nginx")?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"gateway nginx start failed (state={state:?}) ({}): {}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue