# PRD 0001: Per-agent egress proxy via pipelock - **Status:** Active - **Author:** didericis - **Created:** 2026-05-08 ## Summary Run pipelock as a sidecar container on each bot-bottle agent's only egress route, scanning all outbound HTTP for hostname allowlist violations and DLP matches. ## Problem Today the agent container has unrestricted internet egress, and even on allowed channels there is no content-level inspection. Specifically: - Containers have unrestricted internet egress; a misbehaving agent can POST to any host. - Allowed channels (`api.anthropic.com`, git remotes) can still carry content-level exfil with no detection. - DNS exfil via subdomain encoding is not detected anywhere in the stack. - MCP tool calls and responses pass through unscanned. These gaps are documented in `docs/research/network-egress-guard.md`, `docs/research/secret-exfil-tripwire-encodings.md`, and `docs/research/pipelock-assessment.md`. The pipelock assessment recommends adopting pipelock as the v2 sidecar (in place of smokescreen) layered on top of a v1 iptables+dnsmasq floor. ## Goals / Success Criteria The feature works when all of the following are observable: - The agent container has no default route; `curl https://example.com` from inside fails when `example.com` is not on the allowlist. - The agent container can reach `api.anthropic.com` and `claude` runs end-to-end through the proxy. - Pipelock blocks a known credential pattern in a request body and surfaces a structured log line for the block. - The subdomain-entropy check fires on a `.evil.com` request. The feature is **done** when all of the following ship: - `cli.py start` brings up a per-agent pipelock sidecar on a `--internal` Docker network and points the agent's `HTTPS_PROXY` at it. - A per-agent pipelock YAML config is generated from a bottle-level `egress.allowlist` field, plus baked-in defaults for Claude Code's required hosts so basic bottles work out of the box. - The existing `cli.py` y/N preflight shows the resolved allowlist before launch. - When the agent container exits, the pipelock sidecar and the internal network are torn down cleanly (no orphaned containers or networks). ## Non-goals - Closing every exfil vector. SSH session content, raw TCP, UDP, ICMP, and TLS-SNI domain fronting all remain known gaps after this PRD ships and are explicitly out of scope. - Audit logging or persistent log storage of pipelock decisions. v1 logs to stdout only; durable logging is a follow-up PRD. - Replacing the v1 iptables layer. Pipelock sits above iptables, not in place of it (see `pipelock-assessment.md` §Recommendation). - Multi-tenant or remote-pipelock deployments. v1 is one pipelock container per agent container, on the same Docker host. ## Scope ### In scope - New manifest schema: an `egress` object on each bottle, with `allowlist: [hostname]` for v1. - Generation of a per-bottle pipelock YAML config at launch. - Per-agent Docker `--internal` network creation and teardown. - Pipelock sidecar container lifecycle (start, attach to network, receive config, stop on agent exit). - `HTTPS_PROXY` / `HTTP_PROXY` injection into the agent container. - Preflight integration: the existing y/N plan in `cli.py` lists the resolved allowlist. ### Out of scope - The v1 iptables + ipset + dnsmasq layer (separate PRD; see `network-egress-guard.md`). - TLS interception / domain-fronting mitigation. Pipelock does not terminate TLS and this PRD does not introduce CA-trust injection. - Per-bottle DLP rule customization beyond pipelock's 48 built-in patterns. Custom signed rule bundles are deferred. - Mediator-signed action receipts and any other pipelock features potentially gated under the ELv2 enterprise subtree (see open question on licensing in `pipelock-assessment.md`). ## Proposed Design ### New services / components Two new modules under `bot_bottle/`: - **`bot_bottle/pipelock.py`** — pipelock-specific logic. Generates the per-bottle YAML config from the manifest's `egress` block plus baked-in defaults; copies the YAML into the sidecar via `docker cp`; starts and stops the sidecar container; resolves the allowlist for display in the preflight. - **`bot_bottle/network.py`** — Docker network plumbing. Creates the per-agent `--internal` network (named `bot-bottle-net-` with the same slug-and-suffix scheme used for container names), attaches the agent and sidecar to it, removes it on teardown. Kept separate from `bot_bottle/docker.py` so a future PRD can add non-pipelock network controls without entangling them with pipelock specifics. This split mirrors the existing per-concern module pattern (`manifest.py`, `env_resolve.py`, `skills.py`, `ssh.py`). ### Existing code touched - **`bot_bottle/cli/start.py`** — wire the new lifecycle into the `start` subcommand: create the internal network, launch the pipelock sidecar, then launch the agent container with `HTTPS_PROXY` / `HTTP_PROXY` set to the sidecar's service name. Add the resolved allowlist to the preflight y/N output. Tear down sidecar + network in the existing exit handler. - **`README.md`** — public-facing description should mention that agent containers route HTTP egress through pipelock by default, and document the new `egress.allowlist` bottle field. `Dockerfile` is intentionally not touched for v1 — `HTTPS_PROXY` / `HTTP_PROXY` are injected per-launch via `docker run -e`, not baked into the image. This keeps the image agnostic to whether a sidecar is in use (useful if a future bottle definition opts out of the proxy for testing). `bot_bottle/docker.py` may grow one or two helpers if there is a clean place for shared primitives, but the network-specific helpers live in `bot_bottle/network.py`. Decide during implementation; not a contract. ### Data model changes The bottle schema gains an `egress` object. The structure is designed to allow incremental additions without a breaking rename: ```jsonc { "bottles": { "default": { "env": { "...": "..." }, "ssh": [], "egress": { "allowlist": [ "api.anthropic.com", "github.com" ] } } } } ``` Resolution rules: - The effective allowlist is ``. - Baked-in defaults cover hosts Claude Code itself needs: `api.anthropic.com`, `statsig.anthropic.com`, `sentry.io`, `claude.ai`, `platform.claude.com`, `downloads.claude.ai`, `raw.githubusercontent.com` (per `pipelock-assessment.md` and Claude Code's network-config docs). - Bottles with no `egress` block use defaults only. - Future keys (`dlp`, `mode`, `data_budget`, etc.) are reserved under the same `egress` object; v1 ignores unknown keys. The `agent` schema is unchanged. Egress is a property of the container/sandbox, not the task — multiple agents pointing at the same bottle share the same allowlist. ### External dependencies - **Pipelock binary** is pulled from `ghcr.io/luckypipewrench/pipelock@sha256:`. The digest is pinned in `bot_bottle/pipelock.py` (or a sibling constants module) and bumped deliberately, mirroring the claude-code version pinning pattern in `Dockerfile`. - No new host-side runtimes. The pipelock image is the only new external artifact. ## Open questions - **ELv2 licensing.** Several capabilities discussed in `pipelock-assessment.md` (mediator-signed action receipts, signed rule bundles) may live under the `enterprise/` subtree and require accepting Elastic License v2 terms. Before implementation, audit which features used by this PRD are Apache-2.0-core. v1's plan (proxy + 48 default DLP patterns + subdomain entropy + sidecar topology) is expected to be core-only, but this should be confirmed. - **Where to put the digest pin.** A constant in `bot_bottle/pipelock.py` is the lowest-friction option; a separate `bot_bottle/versions.py` (or similar) may be cleaner once there are multiple pinned dependencies. Decide during implementation. - **Per-agent overrides.** The PRD scopes egress to the bottle. If a later use case calls for tightening (not loosening) the allowlist for one agent within a bottle, revisit. Out of scope for v1. - **Default-allowlist drift.** Claude Code's required hostnames may change with new versions. v1 hardcodes the current set; a follow-up could derive them from the pinned claude-code version or a published manifest from Anthropic. - **Sidecar log surface.** Pipelock decisions go to the sidecar's stdout. v1 leaves these visible only via `docker logs ` — fine for inspection but not aggregated. Persistent / structured logging is a non-goal here, called out for the follow-up. - **DNS resolver routing.** Pipelock's subdomain-entropy check fires on URLs it sees, not on raw UDP/53. Without the v1 dnsmasq layer the agent could still query a non-allowlisted resolver directly. Document the dependency on the v1 PRD (or note explicitly that v1 of this PRD ships with that gap if the iptables PRD lands later). ## References - `docs/research/pipelock-assessment.md` — recommendation and rationale. - `docs/research/network-egress-guard.md` — v1 iptables+dnsmasq baseline. - `docs/research/secret-exfil-tripwire-encodings.md` — content-tripwire framing this PRD partially addresses via pipelock's DLP layer. - Pipelock README: - Claude Code network configuration: