VayuPress Docs

ADR-0123: In-Panel Tier 2/3 Activation via an Unprivileged Intent + Root Reconcile Agent

Status: Accepted
Date: 2026-07-08
Author: @johalputt
Relates to: ADR-0122 (Tier 2/3 UI + privilege boundary), ADR-0056 (process isolation), ADR-0112 (VayuShield resilience tiers)

Context

Operators want to turn VayuShield's Tier 2 (kernel nftables) and Tier 3 (nginx edge) hardening on and off from VayuOS — no terminal. But applying those needs root (nftables, systemctl reload nginx), and VayuPress deliberately runs as an unprivileged service (ProtectSystem=strict, no CAP_NET_ADMIN, SystemCallFilter blocks @privileged). Giving the web app the ability to run root-level firewall/nginx commands would make a web-app compromise a direct path to root — defeating the very isolation the design depends on (ADR-0056, ADR-0122). So a naïve "run the script from the handler" is not acceptable.

Decision

Separate intent (unprivileged) from action (privileged) with a one-way, input-free channel.

The web app expresses intent only

The VayuOS toggle writes or removes an empty flag file in a control directory the service user owns (<state>/vayushield-control/tier{2,3}.want). That is the only thing the panel does — no privileged call, no parameters, just create/delete an empty file it already has permission to write. (handleOSShieldTier, shieldSetTierWant.)

A separate root agent performs the action

deploy/vayushield-agent.sh runs as its own root systemd service (vayushield-agent.service), completely separate from the vayupress service. It polls the control dir and, on a change, runs only the fixed, vetted, root-owned scripts installed at /usr/local/lib/vayushield (vayushield-firewall.sh apply|remove; copy/validate/reload the nginx conf). It never reads any argument or content from anything the web app writes — the mere presence of a flag file is the entire signal — so there is no command-injection surface. It writes back a status word (tierN.state) and a heartbeat (agent.alive) that the panel only displays / uses to detect that the helper is installed.

Ownership / trust

The control dir is owned by the service user (created by the installer / lazily by the app), so the unprivileged app writes flags there and the root agent only reads them (root can also write status into an app-owned dir). The agent never trusts flag contents; a bad nginx conf is validated with nginx -t and rolled back before reload, so a toggle can never leave the site down.

Zero-extra-terminal install

The agent is installed/refreshed automatically by scripts/update-vayupress.sh (which already runs with the required privileges during a normal update) — copying the scripts to /usr/local/lib/vayushield, installing the unit, and enabling it. So the operator gets in-panel toggles after their next normal update with no extra terminal step; the one-time privileged bootstrap rides the update they already run. (VAYUSHIELD_AGENT=0 opts out.) The in-app updater cannot do this (it is unprivileged), which is correct — only a genuinely privileged path may install a root service.

Consequences

  • Tier 2/3 become real on/off switches in VayuOS, with live status and full reversibility, and no terminal for day-to-day use.
  • The unprivileged-service security boundary is fully preserved: the web app gains nothing; all privileged work is isolated in a root agent that trusts none of the web app's input.
  • Until the agent is installed (i.e. before the next update), the panel says so and falls back to the documented copy-paste commands (ADR-0122).
  • Because per-IP kernel limits act on whatever IP reaches the origin, Tier 2 is fully effective for a direct-to-origin deployment; behind a CDN it mainly guards direct-to-origin attacks (the panel notes this). Tier-1 in-binary defenses and Tier-3 edge shaping are unaffected by that distinction.