47c3ba63f8
Flip Status: Draft -> Active for the 23 PRDs whose work has shipped to main (including 0027, now that PR #95 has merged). Leaves the terminal-status PRDs unchanged: 0007 and 0010 (Superseded) and 0014 (Retargeted) were replaced, not shipped as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
225 lines
9.4 KiB
Markdown
225 lines
9.4 KiB
Markdown
# 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 `<base64-of-secret>.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-<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
|
||
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> ∪ <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` (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:<digest>`. 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 <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>
|