AI Agent Security: One Container Per Agent, and the Credential Theft It Closed
Three AI agents, one container, one Unix user - so any of them could read the founder agent's 47 secrets. The build-log account of finding it and fixing it.

AI agent security fails at the runtime boundary, not at the chat window. On 30 July 2026 we found that all three of our internal AI agents ran inside one container as a single Unix user, which meant any of them could read the founder agent's environment file and the 47 secrets in it. The fix was one container and one host user ID per agent.
This is the build-log account: what we believed, what was actually true, how it was diagnosed, and what broke on the way to fixing it. Roughly 235 skill files stopped working the moment we split the containers, and the same week exposed a second structural hole in our approval gate. Both are worth publishing because neither was a bug in anyone's code. They were both a boundary we had described in a document instead of enforcing in a mount namespace.
What is the AI agent security risk in a shared runtime?
The risk is that agent identity and process identity come apart. If several agents run as the same operating-system user, every access-control decision downstream of that user is decided by the user, not by the agent. One agent can then read another's credentials and act with them, and every log downstream records the wrong actor.
Our mesh at the time was three agents on separate profiles: the founder agent, an HR agent and an operations agent, each with its own instruction set, its own tools and its own credentials. On paper that is isolation. In practice all three ran in one container, all three were user ID 10000, and all three had a terminal, a file tool and code execution enabled. Directory permissions of 0700 on each profile isolated nothing, because one user owned all of them.
OWASP names the general shape of this in its LLM06: Excessive Agency entry: an agent with more permission, autonomy or functionality than its task requires. What our case adds is the specific mechanism. The excess permission was not granted to the agent at all. It was inherited from a runtime the agents happened to share.
How did we find that one agent could read another agent's credentials?
An adversarial review pass raised it as a hypothesis and we verified it from a subordinate agent's own shell. The test was not "can this agent read a file it should not". It was "what does the data layer think this agent is", which is the question that actually matters.
Our data store sits on the host behind a forced-command wrapper, and every agent identifies itself to that wrapper with a key held in its own profile. Running the identity check from the operations agent's shell, holding the founder agent's key, returned the founder agent's profile and read access to everything. The host-side access control list was correct and did its job. It simply was not us asking.
That distinction is the whole finding. The operations agent's own key still returned a refusal on compensation data, exactly as designed. The founder agent's key returned everything, exactly as designed. Nothing was misconfigured. The credential had just walked across a boundary that did not exist.
Why did file permissions not stop it?
Because file permissions describe what a user may read, and all three agents were one user. Permissions are the wrong layer to express "these two processes should not trust each other" when the two processes share an identity. The only thing 0700 protected against was a fourth account that did not exist.
It is worth quoting the correction we had to make to our own build log. An earlier entry recorded that team members would get messaging channels only, because per-profile credential isolation was "fail-closed". That clause was wrong, and it is why the hole sat unnoticed for two days. The original finding had been scoped to the dashboard and concluded that a chat-only surface was safe. It is not: the agent holds a terminal regardless of which interface you talk to it through. Restricting the interface cannot contain a shared trust domain.
Saltzer and Schroeder wrote the applicable principle in 1975, in The Protection of Information in Computer Systems, and it is not least privilege. It is least common mechanism: "Every shared mechanism (especially one involving shared variables) represents a potential information path between users and must be designed with great care to be sure it does not unintentionally compromise security." A shared container is a shared mechanism. We had reasoned about privileges and never about what the three agents held in common.
How do you isolate AI agents so one cannot read another's secrets?
Give each agent its own container and its own host user ID, and bind-mount only that agent's own home into it. That produces two independent failures for an attacker rather than one: the sibling's path does not exist inside the mount namespace, and the host user could not read it even if it did.
We verified the second layer separately rather than assuming it, by attempting to read both siblings' environment files as each new user ID. Permission denied in every direction. The first layer is the one people build; the second is the one that still holds after somebody adds a convenience mount six months later.
Four things bit us during the split, and all four are the kind that cost an afternoon:
- The image refuses an arbitrary user override at the container level - it aborts, because remapping the user out from under the supervision tree breaks process init. The supported route is the image's own user and group ID variables, which remap the account at boot and take ownership of the volume.
- An earlier test with a direct user flag appeared to work, and it was a false pass: it only ran because the entrypoint had been overridden to a bare shell, which skipped the init system entirely. A test that bypasses the mechanism under test proves nothing.
- A profile directory on its own is not a complete agent home. Each new home also needed the shared authentication file and the root configuration keys merged underneath the profile's own, or the gateway would not start.
- The agent process runs as the remapped unprivileged user. Only supervision runs as root, which is the same shape as before the split, so nothing about the privilege model changed - only the number of trust domains.
The cost was smaller than expected. All three containers together used 1.4GB of 7.9GB of RAM. The old shared container was stopped with its restart policy disabled and kept as a rollback point rather than deleted. Collaboration between agents was unaffected, because the shared data store lives on the host and is reached through the same forced commands as before. The difference is that the access control list is now real instead of bypassable.
If you are standing up a multi-agent system and want this designed in rather than retrofitted, that is what our AI agent development service exists for. The full mesh build is written up in the Neogen AI OS case study.
What broke when we split the containers?
Nearly every skill in the library, silently. Each agent's home moved to a new path when it became the container root, and every skill file still referenced the old shared layout. Roughly 235 files were telling their agent to run tools at a path that no longer existed.
The part worth publishing is why our verification missed it. A sweep run immediately after the split reported that all references resolved. That sweep only checked absolute paths. One asset was referenced relatively, and chasing an unrelated question about report templates was what exposed it: re-running the check for relative patterns surfaced the real problem behind a confident all-clear. We repointed 117 files for the founder agent, 113 for operations and 5 for HR. Three residual references were skills whose tools that profile deliberately lacks, so they were removed rather than left to fail quietly.
The credential bug we introduced while fixing a credential bug
Merging the root configuration into each new home copied the dashboard username, the password hash and the session-signing secret into all three containers. A shared signing secret means a cookie minted by one agent's dashboard would very likely validate on another. Harmless for as long as two of the three published no ports, and fatal the moment we exposed them over HTTPS for desktop access. Each container now carries its own username, its own hash and its own secret.
The same merge enabled the API server in all three homes while its key existed only in the root environment file, which was not copied. All three gateways refused to start until each received a unique key. The root environment file and the root configuration were a matched pair, and splitting one without the other breaks the gateway. That failure was loud, which is the only reason it cost minutes instead of days.
Does an approval gate make an AI agent safe?
No. An approval gate proves who asked. It does not prove what runs. Ours required a typed one-time code from the founder for every registered action, and the code discipline held perfectly. What the approval could not do was tell him what he was approving.
Two shapes defeated it, neither of which is an incident. There was no evidence of compromise and the code delivered was the code intended. Both are structural, which is worse, because structure repeats.
Approve by reference, not by value
Most submissions looked like a download followed by an execution: fetch a script to a temporary path, then run it. The text a human reviews says "download and run this file". The code that actually runs is whatever that URL serves at execution time, which is after approval. One submission passed its payload as a base64 blob, which is literally unreadable in an approval prompt. Reviewing the last fifteen host-command prompts, four were raw base64.
One approved fetch crossed the boundary the isolation existed to create
A single approved action pulled a code archive over plain HTTP, with no signature and no checksum, from our older shared development box, and unpacked it as root on the leadership box. Engineering has root on that older machine. For the length of that window, anyone with root there could have served different bytes and taken root on the isolated box. The isolation we had built two days earlier was routed around by a convenience download. Later fetches moved inside the boundary, which closes the trust question while leaving the time-of-check gap intact.
The approval prompt did not show what would run
The gate sent its prompts as Markdown with unescaped content. Underscores are italic markers, so a script filename rendered to the approver with its underscores eaten, and it rendered differently in the approval message than in the failure notice, depending on how many underscores happened to pair up across the whole text. An approval prompt whose text differs from what executes authorises something nobody read. Never apply markup to text a human is being asked to approve. Plain text, always.
Both approve-by-reference shapes are now refused at submission: long encoded blobs, and fetch-then-execute chains. Payloads that cannot be shown to the approver do not reach the approver.
What happens when the only write lane is root?
Every routine task becomes a root request. Of 138 gated approvals in the fortnight to 8 August 2026, 119 were the arbitrary host-command verb and only 19 were real registered business actions. The founder approved 106 and rejected 4. A 96% approval rate is not a filter, it is latency.
Then 42 of those 106 approved actions failed anyway, which tells you the human review was not catching errors either. The cause was not a permissive gate. An agent that wanted to render a client document had no way to run its own script except to ask for root on the host, so ordinary work was being escalated to the most dangerous verb in the system. This is privilege escalation by absence of a lower lane, and tightening the gate makes it worse rather than better.
The fix was to build the lane that should have existed. A script-run action executes inside the submitting agent's own container as its own user, which is strictly less privilege than the host command it replaces, because the agent already owns that filesystem. The target container is derived from the SSH key identity and can never be passed as a parameter.
Two follow-on findings came from the same audit and both generalise:
- The hand-written documentation was teaching the wrong default. The skill describing the gate said the host-command verb could do anything on the host, and never mentioned that most of an agent's work was already self-service. The agents were behaving exactly as instructed. That skill is now generated from the gate's own registry tables, because a document derived from the code cannot drift from it.
- Namespaces need a preflight, not just syntax checking. An agent submitted a container path to a host-root executor, twice, for the same document. The first attempt included a directory creation, which built a phantom tree on the host mirroring a container path, and a client document was later written into it: outside every container, outside the backup scope, unreachable by any agent. Submissions referencing a container path without an explicit container execution are now refused with an explanation.
The rule we now apply to every agent we deploy
Never mount one agent's directory into another agent's container. That is the entire control, and everything else in this post is a consequence of having learned it late.
The second primitive is the one that took longer to accept: a gate proves who asked, never what runs. Approval is an authentication mechanism wearing the costume of an authorisation mechanism. It belongs on top of a real boundary, not instead of one. If you want to know whether your own agents share a trust domain, run your data layer's identity check from a subordinate agent's shell while holding a sibling's key. If it answers with the sibling's identity, your interface restrictions are decoration.
We build agent meshes for other companies on the same architecture, and this class of finding is what the first week of that work looks for. If you are running more than one agent against real credentials, talk to us about an isolation review.
Frequently asked questions
Is one container per agent overkill for a mesh of two or three agents?
No, because the failure mode does not scale down. Two agents sharing a runtime is exactly the case in this post, and the theft was possible with three. The measured cost for us was 1.4GB of RAM across three containers and one afternoon of path repointing. The cost of the shared-runtime version is that every credential in the system is held by every agent in it.
Does per-agent isolation stop agents from working together?
It does not, provided collaboration runs through a shared service rather than a shared filesystem. Our agents read and write the same data store, reached over an authenticated wrapper that checks the caller's own key, and they pass work to each other over a message bus with per-agent keys. Both survived the split untouched. The architecture is written up in our post on the agent mesh.
Can you get the same isolation with virtual machines or Kubernetes namespaces instead?
Yes. The control is one identity per agent plus no shared mount, and several mechanisms deliver it. Containers with distinct host user IDs were the cheapest option for us on a single box. Whichever you pick, verify the second layer independently: confirm the sibling path is absent from the namespace and confirm the identity cannot read it even when the path is present.
Should an AI agent have a terminal tool at all?
Yes, if it is scoped to the agent's own container. Removing the terminal is the wrong lever, because the agent reaches the same files through its file and code-execution tools, and a chat-only surface changes nothing about what the process can touch. The lever is the boundary the tools run inside, not the list of tools.
What is the difference between an approval gate and a permission boundary?
A gate is per-action and needs a human present; it can tell you a request came from an authorised agent. A boundary is structural and holds when nobody is looking; it decides what that agent can reach at all. A gate on top of a shared trust domain authorises an intention. A gate on top of a real boundary authorises an action.
How do you audit an existing agent deployment for this?
Three checks, in order. Confirm each agent runs as a distinct operating-system user. Attempt to read a sibling's credential file from inside each agent, as that agent, and treat any success as a finding rather than a warning. Then read your own approval log and count what fraction of approvals are the arbitrary-execution verb - if it is most of them, the gate is measuring latency, not risk. We wrote up the wider operating experience in running a Hermes agent in production.

Founder and Director at Neogen Media. Writing field notes on AI automation, growth systems, and the integrated playbook we ship for Indian SMBs. Based in Kochi.
Follow on LinkedIn