Assisted-by: Codex
9.4 KiB
PRD 0001: Per-agent egress proxy via pipelock
- Status: Draft
- 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.comfrom inside fails whenexample.comis not on the allowlist. - The agent container can reach
api.anthropic.comandclauderuns 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
<base64-of-secret>.evil.comrequest.
The feature is done when all of the following ship:
cli.py startbrings up a per-agent pipelock sidecar on a--internalDocker network and points the agent'sHTTPS_PROXYat it.- A per-agent pipelock YAML config is generated from a bottle-level
egress.allowlistfield, plus baked-in defaults for Claude Code's required hosts so basic bottles work out of the box. - The existing
cli.pyy/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
egressobject on each bottle, withallowlist: [hostname]for v1. - Generation of a per-bottle pipelock YAML config at launch.
- Per-agent Docker
--internalnetwork creation and teardown. - Pipelock sidecar container lifecycle (start, attach to network, receive config, stop on agent exit).
HTTPS_PROXY/HTTP_PROXYinjection into the agent container.- Preflight integration: the existing y/N plan in
cli.pylists 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'segressblock plus baked-in defaults; copies the YAML into the sidecar viadocker 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--internalnetwork (namedbot-bottle-net-<slug>with the same slug-and-suffix scheme used for container names), attaches the agent and sidecar to it, removes it on teardown. Kept separate frombot_bottle/docker.pyso 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 thestartsubcommand: create the internal network, launch the pipelock sidecar, then launch the agent container withHTTPS_PROXY/HTTP_PROXYset 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 newegress.allowlistbottle 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:
{
"bottles": {
"default": {
"env": { "...": "..." },
"ssh": [],
"egress": {
"allowlist": [
"api.anthropic.com",
"github.com"
]
}
}
}
}
Resolution rules:
- The effective allowlist is
<baked-in-defaults> ∪ <bottle.egress.allowlist>. - 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(perpipelock-assessment.mdand Claude Code's network-config docs). - Bottles with no
egressblock use defaults only. - Future keys (
dlp,mode,data_budget, etc.) are reserved under the sameegressobject; 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:<digest>. The digest is pinned inbot_bottle/pipelock.py(or a sibling constants module) and bumped deliberately, mirroring the claude-code version pinning pattern inDockerfile. - 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 theenterprise/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.pyis the lowest-friction option; a separatebot_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 <sidecar>— 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: https://github.com/luckyPipewrench/pipelock/blob/main/README.md
- Claude Code network configuration: https://code.claude.com/docs/en/network-config