Adventures in Declarative Agent Security, Part 1

02 Aug 2026 • 6 min read

YAML shapes everything else; why not agent security?

When you inspect an application running in Kubernetes, you typically start by looking at its YAML manifests. Continue tracing the deployment backwards and you’ll likely find the GitHub or GitLab CI pipeline that ran it, itself defined in YAML. The pipeline might have even wrapped it up in a YAML Helm chart, which itself may have been wrapped in a Helmfile YAML configuration. If you make it all the way down the Russian nesting doll scheme into the application, you’ll likely find it has still more YAML files configuring dependencies. Not to mention it likely communicates over HTTP using JSON: a subset of YAML!

Buzz Lightyear gesturing at the words “YAML, YAML everywhere”

All of this is to say that YAML has become a common way to express complex systems as flexible, declarative configuration. Agent security, specifically the management of permissions for filesystem and network access, presents a similar problem. My ideal solution would:

  • Prevent nightmare scenarios:
  • Preserve a productive developer experience by letting me define simple, fine-grained YAML policies for filesystem and network access.

Initially I looked for an out-of-the-box solution to this problem. The only one I found that satisfied my other criteria (maintained by a reputable org, works across Linux and macOS (Apple Silicon), quick to set up and get running) was NVIDIA OpenShell. However, I was disappointed to discover that OpenShell’s official Linux installer requires root privileges, which was a dealbreaker for me.

Unable to find an existing solution, I decided to roll my own! The simplest solution I came up with was to use Docker Compose. The agent would run in one container, while all of its outbound network traffic would pass through a separate proxy container that controls which requests are allowed.

The first step was setting up a minimal Dockerfile. I started with the latest Ubuntu LTS and tacked on Vim so I could easily poke around inside the container.

DOCKERFILE
FROM ubuntu:26.04

RUN apt-get update && apt-get install -y vim

CMD ["bash"]

The next step was adding a minimal compose.yaml file:

YAML
services:
  agent:
    build: .
    stdin_open: true
    tty: true

With that in place:

  1. Build and start the container: docker compose up --detach --build

  2. Open a shell inside it: docker compose exec agent bash

  3. Verify Vim is installed: vim --version | head -n 1

    This should output something like VIM - Vi IMproved 9.x.

  4. Confirm DNS resolution works before we disable networking: getent hosts google.com

    This should return one or more addresses.

  5. Run exit to leave the container, then shut it down: docker compose down

Next, I added network_mode: none to the agent service in compose.yaml to disable networking:

YAML
services:
  agent:
    build: .
    stdin_open: true
    tty: true
    network_mode: none

I spun the container back up, reran getent hosts google.com, and verified that it returned no results!

Then add codex installation to Dockerfile:

DOCKERFILE
RUN curl -fsSL https://chatgpt.com/codex/install.sh \
    | env \
        CODEX_INSTALL_DIR=/usr/local/bin \
        CODEX_HOME=/opt/codex \
        CODEX_NON_INTERACTIVE=1 \
        sh

Next, I set up an internal network that the agent would eventually use to reach the proxy. I replaced network_mode: none in the agent service with:

YAML
networks:
  - agent-network

Then I added the network to the top-level networks section in compose.yaml:

YAML
networks:
  agent-network:
    internal: true

Setting internal: true allows containers on agent-network to communicate with each other but prevents them from reaching the internet. I spun the agent container back up and verified that it still couldn’t access the internet.

Next, I added a Squid proxy to the top-level services section. The proxy connects to both the internal agent network and an internet-facing network:

YAML
services:
  proxy:
    image: ubuntu/squid:7.2-26.04_edge
    networks:
      - agent-network
      - internet

Then I added the internet-facing network to the top-level networks section:

YAML
networks:
  internet:

At this point, the proxy could reach the internet, but the agent still couldn’t because it was connected only to agent-network. I spun the containers back up and verified that the agent still couldn’t access the internet.

Before routing the agent’s traffic through the proxy, I created a squid.conf file that denied every request by default:

TEXT
http_port 3128
http_access deny all

Then I mounted the configuration read-only into the proxy service:

YAML
services:
  proxy:
    volumes:
      - ./squid.conf:/etc/squid/squid.conf:ro

This gave the proxy a deny-all policy before the agent began using it.

Next, I configured the agent service to send HTTP and HTTPS requests through the proxy:

YAML
services:
  agent:
    environment:
      HTTP_PROXY: http://proxy:3128
      HTTPS_PROXY: http://proxy:3128

I recreated the containers and verified that an internet request from the agent was rejected.

Next, I updated squid.conf to allow HTTPS connections only to OpenAI and ChatGPT domains:

TEXT
http_port 3128

acl CONNECT method CONNECT
acl codex_domains dstdomain auth.openai.com .chatgpt.com

http_access allow CONNECT codex_domains
http_access deny all

Then I restarted the proxy and verified that Google was blocked while ChatGPT was reachable:

BASH
docker compose restart proxy
docker compose exec agent curl -I https://google.com
docker compose exec agent curl -I https://chatgpt.com | head -n 1

The second command should return 403 Forbidden from Squid, while the final command should output HTTP/1.1 200 Connection established.

Finally, with device code login enabled in my ChatGPT security settings, I logged in to Codex:

BASH
docker compose exec agent codex login --device-auth

After following the terminal instructions, I saw Successfully logged in. The login persisted for the life of the container.

To avoid logging in every time I recreated the agent container, I added a named volume to compose.yaml:

YAML
services:
  agent:
    volumes:
      - codex-data:/root/.codex

volumes:
  codex-data:

After recreating the containers, I logged in again using the same command as before. This time, the login persisted after bringing the containers down and back up. I verified it with:

BASH
docker compose exec agent codex login status

Finally, I tested Codex inside the container:

BASH
docker compose exec agent \
  codex exec --skip-git-repo-check \
  "Reply with exactly: Codex network works"

The final response should be Codex network works.

I’ve now mostly accomplished my initial goal: I have an agent running in a sandbox, but its security is not governed entirely by YAML because the network policy still lives in squid.conf. Eventually, I’ll add a conversion layer that translates simpler YAML policies into the resulting proxy configuration.

Security caveat

I am not a cybersecurity expert, and this project should not be treated as a hardened security boundary.

Its primary goal is to reduce the risk of accidental data exposure or unintended agent behavior. For example, it can prevent an agent working on a private project from searching the public internet with sensitive project details. It is not designed to contain a malicious agent or defend against sophisticated attacks.

The final setup is available on GitHub . The version used in this tutorial is tagged 1.0.0 .

In Part 2, coming soon, I’ll add Claude Code with models from Amazon Bedrock to this setup!

Start searching

Enter keywords to search articles.