dev

Security conventions

Defense-in-depth with 4 independent mechanisms, built on the 3-layer access model. Threat model, secrets lifecycle, and validation.

Work in progress

This security model is experimental. Running an LLM-powered agent on a development machine introduces risks that are not yet fully addressed: prompt injection, data exfiltration via API calls, supply chain attacks through generated code. The layers described here reduce the attack surface but do not eliminate it. Treat this as a foundation to harden, not a finished security posture.

Defense in depth

The agent container is treated as semi-trusted: benign but potentially buggy. Security does not rely on any single mechanism. Four independent layers ensure that a breach at one level is contained by the next.

Layer 1File system boundaries

Secrets in env/ (not mounted). Read-only mounts for engine. Isolated workspace directory.

Layer 2Container isolation

Non-root user with UID/GID mapping. Network isolation (separate bridge). No Docker socket access.

Layer 3Linux kernel security

All capabilities dropped. AppArmor mandatory access control (docker-default). no-new-privileges prevents gaining privileges through execution. Bind mounts do not enforce nosuid, nodev, or noexec here.

Layer 4Resource limits

Process count limits (pids_limit: 256). File descriptor limits. Memory and CPU constraints.

3-layer access model

Every file, every volume mount, every access decision follows a 3-layer model. Where a file lives determines its access level.

L1 Workspace Read-Write

Agent's mutable workspace: source code, documentation, delivery tickets, agent config. Everything the agent creates or modifies lives here.

L2 Reference Read-Only

Engine, Taskfiles, package sources are mounted read-only. The agent can read conventions but cannot modify the infrastructure it runs on. To propose L2 changes, the agent writes to an exchange zone (exchange.dev.tmp/proposed/ for the dev agent, exchange.lab.tmp/ for metalab). The operator reviews the diff and applies manually via cp.

L3 Inaccessible Not mounted

Secrets (env/), Docker socket, host filesystem. Not restricted, simply absent from the container.

Resilience invariant

The dev agent and the metalab agent run on separate networks with separate runtime state. At least one of them must stay functional at all times: if one breaks, the operator uses the other to diagnose and recover. Both share the same image, so an image change is tested on one agent before the other is restarted (staged rebuild).

Container hardening

Non-root execution

The agent runs as a non-root user with UID/GID matching the host operator. This prevents privilege escalation, ensures file ownership consistency, and limits damage from any container compromise.

Capability dropping

compose.yaml
cap_drop:
  - ALL    # Remove all ~15 default capabilities

security_opt:
  - no-new-privileges:true  # Prevent privilege escalation
  - apparmor=docker-default  # Kernel-level MAC

Docker grants ~15 capabilities by default. The agent container drops all of them: no NET_RAW (packet sniffing), no SYS_ADMIN (mount/kernel), no MKNOD (device nodes). The agent functions correctly with zero capabilities.

Resource limits

compose.yaml
pids_limit: 256      # Max processes (fork bomb protection)
mem_limit: 2g        # Max 2 GB RAM
cpus: 2              # Max 2 CPU cores

Network isolation

Each agent runs on its own internal network: no direct route to the internet, to the other agent, or to the app containers. The app containers use the internal gateway network of the proxy.

Only the proxy (pkg.proxy) reaches the internet, through a separate egress network. augr-host-only publishes the Phoenix and app ports to the host, without internet access. augr-host-internet is a temporary direct route that no container uses by default.

The OAuth login (task augr:pkg:agent:connect) is the exception: a short-lived container on the host network, which still reaches the provider through the proxy, on 127.0.0.1:13128. See Providers and models.

Egress runs through the proxy

Agents reach their LLM provider only through the proxy: every request passes through one logged point, and agents have no internet when the proxy is down. The proxy does not filter destinations yet: through it, an agent can reach any host, including the app containers. Filtering outbound calls per agent is planned, so that each agent only reaches what its work requires.

This design also works behind a corporate proxy, with TLS inspection, and with a private npm registry: see Corporate proxy, Custom CA certificates, and Private npm registry.

Secrets management

Real secrets never live in the repository, and env/ is not mounted in the agents.

1
Template

workspace/env.template/ : placeholders only, committed to Git. No real values.

2
Instantiation

task admin:setup copies templates to env/. Operator fills in real values.

3
Aggregation

task augr:build aggregates per-package env files into env/.env.project. Validates no empty required vars.

4
Injection

Docker Compose injects variables as container environment variables. No .env file is visible to the agent.

Known limitations
  • A process can read the credentials it receives: the agent sees its own environment variables.
  • OAuth credentials persist in the OpenCode data volume of each agent.
  • The seed app containers can read the private npm registry token, mounted for pnpm at runtime. This exception is temporary; see Private npm registry.

Where a provider credential lives depends on the connection method; see Providers and models.

Threat model

The agent is treated as semi-trusted: it acts in good faith but may have bugs, hallucinations, or be running compromised dependencies.

Secret exposure via file accesscritical

env/ not mounted, only ENV injection

Container escape via Docker socketcritical

Socket not mounted

Privilege escalation via setuidhigh

Non-root execution and no-new-privileges (not bind-mount nosuid)

Lateral movement to app containershigh

Separate internal networks; published ports and proxy policy remain boundaries

Resource exhaustion (fork bomb)medium

pids_limit, mem_limit, cpus

Malicious code execution on hosthigh

Non-root user, no capabilities, AppArmor

Secret commit to Gitcritical

env/ in .gitignore, not in /workspace

Kernel module loadinghigh

AppArmor, no SYS_ADMIN capability

Operator / agent boundaries

Agent CAN

  • Read/write files in /workspace
  • Read engine and Taskfiles (L2)
  • Propose L2 changes via exchange zones
  • Analyze code, propose changes
  • Access internet for LLM APIs
  • Create documentation

Agent CANNOT

  • Execute task commands
  • Run Docker/Podman commands
  • Control other containers through the Docker socket
  • Modify L2 files (read-only)
  • Access env/, Docker socket

Validation

The seed includes a security audit Taskfile that validates all constraints automatically:

terminal
task security-audit:validate

This checks: no secrets in workspace, agent container running as non-root, no Docker socket mounted, network isolation active, AppArmor loaded, capabilities dropped, PID/memory limits set, mount options correct.