All articles

Beyond containers: hardening my local development environment

Reducing the blast radius with containers, an egress proxy, ephemeral secrets, and scoped AI tool permissions.

Beyond containers: hardening my local development environment

Image generated with ChatGPT.

Containers have been my default for more than a decade. What I hadn't done was apply the same discipline to everything else on my host. The recent wave of supply-chain attacks made that gap impossible to ignore. No more "just this once" npm install on the laptop.

Recently, I've been gradually tightening my local development environment around a simple idea: Assume something will eventually behave in a way I didn't expect. A dependency. A package. A tool. An AI agent. My goal isn't perfect security. It's reducing the blast radius when that happens.

Four changes did most of the heavy lifting.

1. Every project runs inside its own container — including the frontend

Most teams already containerize databases, backend services, and maybe a reverse proxy. I went further: frontend included, global dependencies gone. My host machine runs Docker, Git, an editor, and a shell. That's about it.

Each project gets its own Dockerfile and Compose configuration. If a project goes sideways, I throw the container away. If a dependency turns out to be malicious, it's confined to a disposable environment instead of running directly on my host.

As a side effect, onboarding and machine replacement became dramatically easier.

2. A forward proxy in front of every project

This is the change I'd keep if I could only keep one.

Most developers ask what their code can access. I started asking where it can talk.

I run Squid as a sidecar on a project-local Docker network and route outbound traffic through it. It's a forward proxy for egress — not a reverse proxy fronting services. Each project gets a strict allow-list of destinations: package registries, required APIs, and little else.

Why I like this: A compromised dependency has a much harder time quietly exfiltrating data. An AI coding agent running inside the container is naturally constrained to destinations I've explicitly approved. I get visibility into what applications and agents are actually reaching for. It's essentially application-level egress control for local development.

One important caveat: Squid isn't magic. It only governs traffic intentionally routed through it. It won't stop malicious code from deleting files, modifying source code, or abusing permissions it already has. It's primarily about reducing unnecessary network access and making exfiltration harder.

3. No more permanent .env files

I used to have .env files scattered across project folders. Plaintext secrets sitting on disk, surviving reboots, occasionally copied into the wrong terminal window. The .env.tpl file contains op://vault/item/field references for secrets, while non-secret settings remain as plain configuration.

I launch projects with:

op run --env-file=.env.tpl -- docker compose up --build -d

1Password resolves the references and injects the values at runtime. No resolved secrets sitting on disk. No generated credentials files hanging around after a project stops.

Far less "did I accidentally commit that?" anxiety.

4. Constraining what my AI tools can actually do

I use OpenCode and other agentic coding tools heavily. My default used to be simple: give the model everything. Full filesystem, full network, every available tool. It took me longer than I'd like to admit to question that assumption.

Now I start from the opposite direction: limit the available tools, accessible resources, and which models can perform which actions. Combine that with the proxy allow-list, and the agent's reachable surface becomes intentionally narrow.

No magic. Just fewer doors.

The mental model

  • Containerization gives me isolation.
  • The egress proxy gives me network control.
  • 1Password CLI gives me ephemeral secrets.
  • AI tool permissions give me capability scoping.

None of these ideas is new individually. Stacked together, they flip the default from allow-by-default to deny-by-default. I have to consciously open holes when I need them, rather than discovering later that I left them open.

The interesting thing about most supply-chain attacks is that they don't start with sophisticated exploits. They start with developers doing normal developer things. Installing a package. Running a command. Giving a tool permission.

That's why I've become less interested in predicting every possible threat and more interested in reducing the impact when something eventually goes wrong. I'm still migrating projects one at a time — this is where my local development environment stands today.

Want a starting point? The opencode.jsonc and squid.conf I'm running are in the first comment.

#DevEx #Security #Containers #AISecurity #DeveloperProductivity