The security layer is the OS boundary, not agent guardrails

Photo by Gabriel Heinzer on Unsplash

An autonomous agent runs on the same server as production WordPress sites and the secrets behind them, so what it can reach is a security question, not a convenience one. Mine, Izuna, would otherwise be able to read the site files under /var/www and the database credentials in wp-config.php. The reflex is to harden it from the inside: a prompt-injection scanner, a dangerous-command gate, a tool-error sanitizer. Every one of those checks lives inside what it is meant to police, so each shares the failure it is built to catch. The thing you are trying to constrain is the thing enforcing the constraint. I put the defense outside the agent instead, in the operating system (OS) and filesystem boundary. That decision belongs to Reliable agent systems.

TL;DR

The primary security layer is the OS user and filesystem boundary, not the agent’s own guardrails. A no-login, no-sudo account called izuna-aiagent holds Izuna behind a traversal block and a file-read block, so it cannot enter /var/www/ at all or read wp-config.php; the internal guardrails stay, as the secondary layer, never as the defense.

The constraint the obvious answer runs into

Hardening those guardrails is the obvious answer, and one platform fact rules it out. As of Hermes Agent v0.14, the agent inherits only the permissions of the Unix user it runs as, so the confinement you get is the confinement of the account you hand it. A boundary that lives inside the thing it constrains is not a boundary. That is why the internal checks stay, as the secondary layer, and why relying on them as the primary defense was rejected outright.

Make the operating-system boundary the primary layer

The decision was to make the OS boundary the primary security layer. izuna-aiagent is a system user with no login shell and no sudo by default, and zero read/write to /var/www/ is enforced by two independent layers beneath it. The confinement is a property of the account and the filesystem, not of the agent’s behavior.

Three alternatives were live, and each was rejected on a stated reason.

  • Relying on the agent’s internal guardrails as the primary defense. Rejected: they are explicitly the secondary layer.
  • Adding izuna-aiagent to the www-data group. Rejected: that join would expose all four WordPress sites, not only the digest docroot.
  • Relying on policy alone, rather than a structural write boundary. Rejected: the primary defense is structural.

The same write boundary, read as a division of roles rather than as a security edge, is its own subject: Separating spec, build and QA.

The two layers that keep the agent out

The block runs on two independent layers, and each covers a distinct thing.

  • Layer 1 (traversal block): /var/www/ and every WordPress root sit at drwxr-x--- www-data:www-data, and izuna-aiagent is not in the www-data group, so it cannot enter /var/www/ at all.
  • Layer 2 (file read block): wp-config.php sits at 0640 www-data:www-data, so it is not world-readable even if traversal were possible.

The one write path, opened narrowly

Izuna still has to write. It produces HTML digests into /var/www/<vhost>/<dir>/, which needs an opening in the block. Rather than add izuna-aiagent to the www-data group, a single access control list (ACL) grants traversal only: setfacl -m u:izuna-aiagent:x /var/www, an enter permission and not a read one. The write capability itself comes from the agent owning the digest-output directories (izuna-aiagent:www-data 750), scoped by IZUNA_DIGESTS_DIR in its environment file, and no ACL sits inside the dgfoozilla subtree.

The privileged path follows the same shape. /etc/sudoers.d/50-izuna-aiagent enumerates explicit NOPASSWD script paths only, with no shell, no ALL, and no wildcards, always edited through visudo so that a syntax error cannot lock out sudo entirely. Each whitelisted script is a root-owned 755 stub wrapping one command (a configuration test, a firewall status check, a backup snapshot listing), and the file now enumerates about 16 of them. IZUNA_HOME is confined to /home/izuna-aiagent/.

How a chosen automation runs day to day, rather than what it may reach, is a different discipline .

What each risk is covered by

Every risk in the record maps to a structural layer, and the pattern is the same each time: the control sits in the account or the filesystem, never in the agent’s judgment.

The database credentials live in wp-config.php, and the backup credentials live in a dedicated secrets file for restic. Neither is reachable from the account Izuna runs as.

RiskThe layer that covers it
The agent edits WordPress filesOS permissions; izuna-aiagent is not in www-data
The agent runs an unapproved scriptthe per-script sudoers whitelist
The agent gains a shell through sudothe whitelist carries no ALL rule
A prompt injection arrives through cronthe scanner for cron traffic added at v0.13
The agent reads the database credentials or the backup secretsno wp-config.php access, and an IZUNA_HOME confined to /home/izuna-aiagent/

Nothing broke

Every record behind this piece reports Broke: none. There is no incident to narrate, and I will not invent one: the mechanism went in as prevention, not as the repair for a break. That puts the weight on what each layer was built to cover, which is the register above, and on the upkeep the narrow write path requires, which the next section describes.

What this does not solve

Three boundaries, drawn plainly, because a security layer that is oversold is one that will disappoint.

First, the block confines what the agent can reach, not what it does once inside the paths it may enter. The internal guardrails still run, as the secondary layer, and they remain the thing that judges behavior within those limits.

Second, the confinement is not total across every profile. The structural write boundary covers the profiles whose write root is set in their own environment file, but one profile’s restraint is policy rather than structure, so its git commits are the thing nothing else audits. That profile belongs to the role-separation register: Separate spec, build and QA in an agent team.

Third, this is a confinement, not autonomy. It exists so the agent can run without a live operator while a human stays in the chain. It does not validate what the agent produces, which is a separate guard: Stop hallucinations with deterministic guards. And the narrow write path carries its own upkeep: the ACL entries live in filesystem extended attributes, so they survive reboots and package upgrades, and they must be re-verified after any virtual private server (VPS) snapshot restore.

The task-shape routing that decides which executor takes which work is a separate axis , and the contract that shapes what the agent renders is another .

The architecture beneath this boundary has its own choices as well: which model runs and where it runs .

The pattern I would start from is the one above: put the boundary in the account and the filesystem, then let the agent’s guardrails sit on top of it. The trade-offs I accepted to run this stack are on the about page. If this is the shape of your problem, let’s talk.