Gateway and agent run in separate containers connected by a Docker internal network. The agent container has no internet access — all traffic is forced through the gateway via default route (ip route replace default via $GATEWAY_IP).
┌─ gateway container ─────────────────┐
│ Networks: [internal, default] │
│ Holds: real credentials, CA key │──── internet
│ IP forwarding + iptables :443→:8443│
│ Runs: gateway binary (:8443, :53) │
└──────────────┬──────────────────────┘
│ Docker internal network
┌──────────────┴──────────────────────┐
│ agent container │
│ Networks: [internal] ONLY │
│ No secrets, no internet access │
│ default route → gateway │
│ Runs: channel manager + agent │
└─────────────────────────────────────┘
Allow all by default. MITM only for hosts where credential injection is needed. Everything else passes through with end-to-end TLS preserved.
Rationale: Dev agents need npm install, pip install, curl arbitrary URLs. Default deny creates too much friction.
Agent container uses a default route via gateway. All outbound traffic flows naturally to the gateway:
# Resolve gateway container IP (Docker DNS, before switching resolv.conf)
GATEWAY_IP=$(getent hosts $GATEWAY_HOST | awk '{print $1}')
# Switch DNS to gateway resolver
echo "nameserver $GATEWAY_IP" > /etc/resolv.conf
# Set default route via gateway (all outbound traffic flows to gateway)
ip route replace default via $GATEWAY_IPGateway enables IP forwarding and redirects port 443 to its proxy:
# Gateway entrypoint
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443Agent has iptables only for port forwards (exposed service ports → localhost). Even with root, route changes are useless since the internal network only reaches the gateway.
Agent → api.github.com:443
→ default route sends to gateway
→ Gateway PREROUTING redirects :443 to proxy :8443
→ Gateway reads SNI: "api.github.com"
→ Matches github plugin rule → MITM mode
→ Terminates TLS (sandbox CA), reads HTTP request
→ Strips dummy token, injects real PAT
→ Opens TLS to real api.github.com, forwards
→ Agent receives response (thinks it talked directly)
Agent never sees real credentials. The channel manager gets dummy tokens. Real creds exist only in the gateway container.
Gateway logs are protected against credential leaks with two layers:
- Structural — request paths are logged before rewriters inject secrets, so tokens never appear in debug output even at the most verbose log level.
- Value-based — a
redact.Handlerwraps the logger and scans all messages and attributes for known secret values (collected from rewriter env vars at startup). Any match is replaced with[REDACTED].
The existing key-based ReplaceAttr filter also catches attributes explicitly named token, authorization, or api_key.
This is global — every log line the gateway emits passes through the redaction handler, regardless of which subsystem produced it.
When docker: true, the docker plugin contributes a DinD sidecar. The gateway itself handles Docker API validation — no separate proxy container needed.
- Agent runs
docker run ...→ connects todind:2375 - Gateway intercepts (default route, like all TCP)
- Gateway's
DockerHandlervalidates the request (block privileged, host binds) - Injects gateway redirect into spawned container config
- Forwards to real DinD
Docker API is HTTP (not HTTPS), so no MITM/TLS needed — plain HTTP inspection.
Spawned containers:
- Forced onto internal network
- Default route points to gateway
- Cannot spawn further containers (no DOCKER_HOST env)
| Vector | Mitigation |
|---|---|
| Agent reads secrets via env/filesystem | ✓ Secrets only in gateway container. Agent container has no access. |
| Agent reads /proc to find credentials | ✓ Gateway is a different container (different PID namespace). |
| Agent gets root, reads secrets | ✓ Different container — root in agent cannot access gateway filesystem. |
| Agent gets root, bypasses proxy | ✓ Docker internal network has no internet route. Changing routes doesn't help. |
| Agent kills gateway | ✓ Different container. Agent cannot signal gateway process. |
| Agent modifies routes | Possible with root, but useless — no internet route exists. |
| DNS tunneling | DNS goes to gateway's resolver. No raw UDP to internet. |
| DinD direct access | DinD uses TLS client cert auth. Cert in gateway container only. |
| Resource exhaustion | mem_limit, cpus, pids_limit per container. |
services:
gateway:
networks: [internal, default]
cap_add: [NET_ADMIN] # for IP forwarding + iptables redirect (:443→:8443)
read_only: true
agent:
networks: [internal] # NO internet
cap_add: [NET_ADMIN] # for ip route setup at boot
security_opt: [no-new-privileges:true]
read_only: true
tmpfs: [/tmp, /run]
mem_limit: 4g
cpus: 2
pids_limit: 256- Gateway holds all real credentials (API tokens, OAuth secrets)
- Agent container has only dummy/empty values
- OAuth tokens stored in shared volume (
oauth-tokens) accessible only to gateway and channel-manager - Token files use 0600 permissions and atomic write-rename pattern
- OAuth refresh requests enforce HTTPS and block private IPs (SSRF protection)
Gateway container:
/etc/gateway/config.yaml root:root 0444 (has credential mappings)
/etc/gateway/ca.key root:root 0400 (MITM signing key)
Environment: TELEGRAM_BOT_TOKEN, GITHUB_PAT, etc.
Agent container:
/home/agent/ agent:agent 0750 (writable, volume)
Environment: GATEWAY_HOST (non-secret, for DNS resolution)
NO credentials, NO CA key, NO gateway config
| Failure | Behavior |
|---|---|
| Gateway crashes | All TCP from agent fails (safe default). Docker restart policy recovers. |
| Agent can't resolve gateway | Entrypoint fails fast with clear error. Container won't start. |
| Channel manager crashes | Agent dies (child). Docker restart policy recovers. |
| DinD crashes | Docker commands fail. Agent retries. |
| Threat | Single Container (old) | Separate Containers (current) |
|---|---|---|
| Agent user reads secrets | ✗ Possible via /proc | ✓ Blocked (different container) |
| Agent root reads secrets | ✗ Possible | ✓ Blocked (different container) |
| Agent root bypasses proxy | ✗ Can modify iptables + reach internet | ✓ No internet route exists |
| Agent root kills gateway | ✗ Can signal gateway process | ✓ Different PID namespace |