VayuVeil — what can observe this install, and what it has verified
VayuVeil lives at /os/vayuveil. It answers one question honestly:
What can observe this install, and which of the controls it claims have actually been read back from the kernel?
The design record is ADR-0150.
Read this first: it is a report, not a shield
The switch on the page governs reporting. Turning VayuVeil on makes the install inventory itself. Turning it off exposes nothing that was not already exposed. There is no button here that defends anything.
That is not an apology. ADR-0150 was written for a desktop operating system — the compositor, the per-window grants, the sandbox policy — and VayuPress is a server binary that runs headless on a VPS. §0 of the ADR records the correction: the endpoint track needs a desktop that does not exist, and the server track is what this binary can do. This page is the server track.
Green means one specific thing
A row is green only when both are true:
- Something is doing the work, and
- this process read back from the kernel that it is still doing it, at the moment the report ran.
Everything that clears that bar is process-scoped — it protects the VayuPress process and nothing else on the machine — and every green row says so in its own text. A reader who takes a process-scoped pass for a host-wide one has been misled by the report, whatever it said elsewhere.
The other verdicts are worth reading precisely:
| Verdict | Means |
|---|---|
| ok | Verified enforcing, right now, at the scope the row states |
| act | Something that should not be reachable is reachable, or has already happened |
| look | A real exposure you should understand, or a control held by the host rather than by this install |
| note | Context, or a limitation that is not a fault |
| unverified | Could not be established. Never rounded up to "fine" |
Unverified is not a soft pass. A platform that would not answer is reported as not having answered. That is the single rule this subsystem holds itself to, because a privacy report that claims a pass it has not verified is worse than no report — it is believed.
What the server track actually verifies
The process cannot be dumped
Two independent controls, on their own rows rather than averaged into one, so you can see which is holding:
PR_SET_DUMPABLE=0, read back withPR_GET_DUMPABLE. No core file,/proc/<pid>root-owned,PTRACE_ATTACHfrom a same-user process refused.RLIMIT_CORE=0, read back withgetrlimit. Even if something later made the process dumpable, the kernel still writes no core file.
A core file would contain session tokens, the keystore key, decrypted mail and any PGP material in memory at the time.
The service sandbox
Read from /proc/self/status and /proc/self/mountinfo at report time —
never from the unit file, because a unit on disk records what somebody intended
and this report is built on what the process got.
- Capture device nodes unreachable.
PrivateDevices=yesreplaces/devwith a minimal tmpfs, so there is no/dev/fb*, no/dev/input/event*, no/dev/dri/card*and no/dev/vcs*. That is denial of exactly the capture channels this ADR enumerates. - Privileges actually held. The effective capability set, as the kernel
reports it. The shipped unit leaves
CAP_NET_BIND_SERVICEand nothing else. - No privilege gain through
execve.PR_GET_NO_NEW_PRIVS. Inherited, so the in-app updater's re-exec and the Tor Space child keep it.
If a row here warns, it names the missing directive. A warning you cannot act on is a warning wasted — and the Unit hardening card below is how you act on it without opening a terminal.
Unit hardening — asking root, then checking it took
The service runs unprivileged and cannot edit its own systemd unit; that is one of the controls, not a limitation to work around. So the panel requests and reports what happened.
Press Request hardening and restart on the VayuVeil page. The panel writes an
empty flag file, a root-side .path unit runs a fixed root-owned worker, and the
worker writes a drop-in at
/etc/systemd/system/vayupress.service.d/20-vayuveil-hardening.conf. Nothing
you can do from the console influences which directives root writes — the list
lives in the worker.
Only five directives are ever written, and the rule that picks them is the rule the whole subsystem runs on: a directive earns its place only if this process can read its effect back afterwards.
| Directive | What it denies | Read back from |
|---|---|---|
NoNewPrivileges=yes |
privilege gain through a setuid binary or file capability | PR_GET_NO_NEW_PRIVS |
PrivateDevices=yes |
the framebuffer, input event devices and DRM card nodes | /proc/self/mountinfo |
PrivateTmp=yes |
/tmp/.X11-unix and anything else left in a shared /tmp |
/proc/self/mountinfo |
ProtectHome=yes |
home directories, including thumbnail caches | /proc/self/mountinfo |
MemorySwapMax=0 |
the kernel paging this service's memory to disk at all | the service's own cgroup |
Everything else systemd offers is refused, and the page prints each refusal with
its reason. ProtectSystem=strict is the one worth understanding: it is in the
shipped unit and correct there, because that unit also carries a
ReadWritePaths= list matched to the data directory. A drop-in written from a
button cannot know that list, and a wrong one leaves the service unable to write
its own database at the next restart.
Three things to know before pressing it
- The service restarts. Systemd applies unit directives at exec, so a drop-in written under a running process does nothing until it starts again. The page will disconnect for a moment; reload it.
- It reverts itself if the service does not come back. The worker samples the unit repeatedly rather than once — a crash loop with a five-second retry looks active if you check at the wrong moment — and on failure it removes the drop-in and restarts without it. Nothing hardened, nothing broken, and the card says so.
MemorySwapMax=0has a cost. It is what keeps decrypted mail and the keystore key off the disk, and it means that under real memory pressure the service is killed rather than swapped.
The verdict is the kernel, never the worker's report
This is the distinction the card exists to hold, and it decides what each chip means. The comparison that separates awaiting restart from did not take is against the drop-in file's own timestamp — the file systemd read at exec — and never against the worker's report, which is written after the restart it is reporting on.
| Chip | What it means |
|---|---|
| in force | every directive verified present, read back from the kernel |
| requested | a request is waiting for the worker; nothing has changed |
| awaiting restart | the drop-in was written after this process started, so this process does not have it — a configuration, not yet a control |
| did not take | the drop-in was already in place when this process started and the directive is still absent — written somewhere this service does not read from, overridden, or refused by the host |
| partly skipped | everything still missing is something the worker declined to write, with its reason shown; a restart will not change it |
| reverted | the service did not come back, so the drop-in was removed |
| failed | the worker reported a problem; its own sentence is on the card |
did not take is the only one of these that is a Fail in the posture report,
and it is a Fail because it is the state in which you have been told a control
exists that does not.
If the card shows a command instead of a button
The root-side worker is not on this host yet. If subdomain provisioning is set up here, the daily sweep installs it from the signed release bundle with no terminal use — the card will then show the button instead. The copyable command is there for a host without provisioning, or for an operator who would rather not wait.
Allow up to two daily sweeps. The sweep upgrades its own driver, and the upgraded driver only takes effect on the following run, so the first sweep delivers the worker and the second installs its watcher. That is worth stating rather than rounding down to "within a day": a page that promises one day and takes two teaches an operator that the page guesses.
That the sweep does this at all is a fix in its own right. The self-upgrade path delivers root-side scripts to every install and does not write systemd units, so the hardening worker arrived on machines with nothing watching for its request. The sweep now writes and enables the watcher too.
Swap — the one that catches people out
An encrypted data directory does not cover swap. Anonymous memory — decrypted mail, session tokens, the keystore key — is written to disk by the kernel under memory pressure, continuously, with nothing to notice.
Two separate facts, deliberately not combined:
VmSwapis an outcome: how many kB of this process are on disk already. Non-zero is reported as act, not as a warning, because it is not a risk — it has happened.memory.swap.maxis a control: whether this service's cgroup permits swapping at all.
Zero bytes swapped without that control is luck, and luck is not a pass. The
row goes green only when nothing has swapped and the cgroup forbids it. A unit
setting MemorySwapMax=0 is what makes that true.
Hibernation is reported separately, and having no hibernate target does not mean this channel is clean — that conflation is what the swap row exists to correct.
The capture suite
Eleven techniques are named. Four are attempted here; the other seven need a client library this binary does not link, and are reported NOT ATTEMPTED with the reason. Not-attempted is never counted as a defence — a suite that quietly skips what it cannot do reports a clean sweep it did not perform.
Attempted: the framebuffer, evdev, a DRM card node, console screen memory
(/dev/vcs*), and another process's memory via /proc/1/mem.
Console screen memory matters more on a server than on a desktop, and it was the last one added for that reason. A headless host has no framebuffer and no Wayland socket, so every other screen technique reports "nothing present" — but virtual consoles exist, and whatever a root login typed at one is sitting in those nodes as plain text.
The suite is judged on bytes, never on whether a call returned an error. A subsystem that returns the right status while producing the wrong bytes passes every transport check ever written.
When does it run? Its assertions run on every build, against an injected reader that returns bytes — which is what proves they bite. The live suite runs against the real host on every load of this page. It is not run as a CI gate: a CI runner has no framebuffer, no input devices and no card nodes, so a live run there would pass vacuously.
The permanent failures
Some rows never clear, whatever you configure. They are one per out-of-scope actor: a kernel-level attacker, DMA-capable hardware, firmware, and a camera pointed at the screen.
They are there on purpose. A report where everything eventually goes green teaches you to stop reading it. The rows that cannot be cleared are what make the rows that can be worth believing.
What is never claimed
Recorded in ADR-0150 §8 so no future copy quietly upgrades it. Not "screenshot-proof". Not protection against root, DMA, firmware or a camera. Not protection of anything other than the VayuPress process — every control here is process-scoped. And not a roadmap: the endpoint track describes software that does not exist, and nothing in the product may render it as forthcoming.
Reading it without opening the panel
Two read-only MCP tools expose the same report any connected client can call —
vayuveil_posture and vayuveil_unit_controls. They exist because this
subsystem's entire product is a report, and until they did that report lived in
exactly one place: a browser page. Both are scoped to settings/read, the same
scope VayuShield's read tools use.
vayuveil_posture returns every row with its verdict and reasoning, the summary
counts, whether reporting is switched on, and a scope sentence shipped
alongside the numbers rather than left in documentation — a caller reading
only the counts still cannot conclude that this install defends a screen. Each
row carries a permanent flag, so a limit that is there by construction is
distinguishable from a failure somebody can act on. Alert on the latter.
vayuveil_unit_controls returns the five baseline directives with in_force
and known as separate fields, the drop-in verdict, what the last run wrote
and skipped, and the list of directives deliberately refused with their reasons.
The capture suite is metered — at most one run every 15 seconds, for every
caller including the page. It opens device nodes, so running it once per
request would let a loop turn a read into device I/O. Both the page and the
payload state how old the result is; the payload field is capture_suite_age.
That caching is confined to the experiment. The control rows — process hardening, the service sandbox, host posture — are read from the kernel on every call and are never cached, because a remembered control is a configuration wearing evidence's clothes. Nothing goes green on the strength of the capture suite, which is why metering it is honest and metering a control would not be.
Neither tool can write, and nothing on this surface ever will. The write that would matter here makes root edit a systemd unit and restart a live service, and a model's context is full of text other people wrote — a blog comment, a fetched page, a mail body. Requesting hardening stays a human pressing a button in VayuOS, having read what it costs.
Reading it at boot
The posture is written to the log at startup too, because a subsystem visible only in a panel is invisible on the day the panel will not load. The line names the registered channel count, how many are enforced host-wide (zero, and it says so), the process controls, and the sandbox.
It logs at warning level for the two states worth acting on: a dumpable process, or a unit that never applied the sandbox. An unanswerable platform logs at info — warning on "unknown" would train you to ignore the level on exactly the machines where a real warning still means something.