You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/architecture.md
+13-1Lines changed: 13 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -344,7 +344,19 @@ AgentSpec JSON is validated against `schemas/agentspec.v1.0.schema.json` (JSON S
344
344
345
345
## Egress Security
346
346
347
-
Egress controls operate at both build time and runtime. Build-time controls generate allowlist artifacts and Kubernetes NetworkPolicy manifests. Runtime controls include an in-process `EgressEnforcer` (Go `http.RoundTripper`) and a local `EgressProxy` for subprocess HTTP traffic. See [Egress Security](security/egress.md) for details.
347
+
Egress controls operate at both build time and runtime. Build-time controls generate allowlist artifacts and Kubernetes NetworkPolicy manifests. Runtime controls include:
348
+
349
+
-**IP Validation** — Rejects non-standard IP formats (octal, hex, packed decimal) and IPv6 transition addresses embedding private IPs
350
+
-**SafeDialer** — Validates resolved IPs post-DNS against blocked CIDR ranges before connecting (prevents DNS rebinding)
351
+
-**EgressEnforcer** — In-process `http.RoundTripper` backed by `SafeTransport` for domain allowlist enforcement
352
+
-**EgressProxy** — Local HTTP/HTTPS forward proxy for subprocess traffic, also backed by `SafeDialer`
353
+
-**Redirect credential stripping** — `http_request` and `webhook_call` strip `Authorization`/`Cookie` headers on cross-origin redirects
354
+
355
+
The A2A server adds:
356
+
-**CORS restriction** — Origin allowlist (localhost by default), configurable via flag/env/YAML
|`--cors-origins`| localhost | Comma-separated CORS allowed origins (e.g., `https://app.example.com,https://admin.example.com`). Use `*` to allow all origins |
150
151
151
152
### Examples
152
153
@@ -165,6 +166,9 @@ forge run --host 0.0.0.0 --shutdown-timeout 30s
165
166
166
167
# Run with guardrails enforced
167
168
forge run --enforce-guardrails --env .env.production
169
+
170
+
# Run with custom CORS origins (for K8s ingress)
171
+
forge run --cors-origins 'https://app.example.com,https://admin.example.com'
-**Localhost bypass**: `127.0.0.1`, `::1`, and `localhost` are always allowed in all modes
47
47
48
+
## IP Validation
49
+
50
+
All egress paths validate hostnames against non-standard IP formats before domain matching. The IP validator (`forge-core/security/ip_validator.go`) rejects SSRF bypass vectors:
51
+
52
+
| Blocked Format | Example | Reason |
53
+
|---------------|---------|--------|
54
+
| Octal |`0177.0.0.1`| Resolves to `127.0.0.1` in some parsers |
55
+
| Hexadecimal |`0x7f000001`| Resolves to `127.0.0.1` in some parsers |
56
+
| Packed decimal |`2130706433`| Resolves to `127.0.0.1` in some parsers |
57
+
| Leading zeros |`127.0.0.01`| Ambiguous parsing across languages |
Both the EgressEnforcer and EgressProxy use `SafeTransport` (an `http.Transport` wired to the SafeDialer) for non-localhost connections.
89
+
90
+
## Container-Aware Private IP Handling
91
+
92
+
In container and Kubernetes environments, pods communicate via service DNS names that resolve to RFC 1918 addresses (e.g., `10.96.x.x`). Blocking these would break inter-service communication.
93
+
94
+
The `allowPrivateIPs` setting is resolved with this precedence:
95
+
96
+
1.**Explicit config** — `egress.allow_private_ips` in `forge.yaml`
97
+
2.**Auto-detect** — `true` if `InContainer()` detects Docker/Kubernetes
Cloud metadata (`169.254.169.254`) is **always** blocked regardless of the `allowPrivateIPs` setting.
107
+
48
108
## Runtime Egress Enforcer
49
109
50
-
The `EgressEnforcer` (`forge-core/security/egress_enforcer.go`) is an `http.RoundTripper` that wraps the default HTTP transport. Every outbound HTTP request from in-process Go code (builtins like `http_request`, `web_search`, LLM API calls) passes through it.
110
+
The `EgressEnforcer` (`forge-core/security/egress_enforcer.go`) is an `http.RoundTripper` that wraps a `SafeTransport`. Every outbound HTTP request from in-process Go code (builtins like `http_request`, `web_search`, LLM API calls) passes through it.
1. Reject non-standard IP formats (`ValidateHostIP`)
119
+
2. Allow localhost (bypass SafeTransport, use `http.DefaultTransport`)
120
+
3. Check domain against allowlist (`DomainMatcher.IsAllowed`)
121
+
4. Forward via `SafeTransport` (post-DNS IP validation)
122
+
57
123
Blocked requests return: `egress blocked: domain "X" not in allowlist (mode=allowlist)`
58
124
59
125
The enforcer fires an `OnAttempt` callback for every request, enabling audit logging with domain, mode, and allow/deny decision.
@@ -187,8 +253,11 @@ egress:
187
253
capabilities:
188
254
- slack
189
255
- telegram
256
+
allow_private_ips: false # default: auto-detect from container env
190
257
```
191
258
259
+
The `allow_private_ips` field controls whether RFC 1918 addresses are allowed through the SafeDialer. When omitted, it defaults to `true` inside containers (detected via `KUBERNETES_SERVICE_HOST` or `/.dockerenv`) and `false` otherwise. Cloud metadata (`169.254.169.254`) is always blocked.
260
+
192
261
## Production vs Development
193
262
194
263
| Setting | Production | Development |
@@ -217,9 +286,12 @@ Events without `"source"` come from the in-process enforcer; events with `"sourc
217
286
| File | Purpose |
218
287
|------|---------|
219
288
| `forge-core/security/types.go` | Profile and mode types, `EgressConfig` |
@@ -55,6 +55,8 @@ Forge agents are designed to never expose inbound listeners to the public intern
55
55
- Slack: Socket Mode (outbound WebSocket via `apps.connections.open`)
56
56
- Telegram: Long-polling via `getUpdates`
57
57
-**Local-only HTTP server** — The A2A dev server binds to `localhost` by default
58
+
-**CORS restriction** — The A2A server restricts `Access-Control-Allow-Origin` to localhost by default; configurable via `--cors-origins` flag, `FORGE_CORS_ORIGINS` env var, or `cors_origins` in `forge.yaml`
59
+
-**Security response headers** — All A2A responses include `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, and `Content-Security-Policy: default-src 'none'`
58
60
-**No hidden listeners** — Every network binding is explicit and logged
59
61
60
62
This means a running Forge agent has zero inbound attack surface by default.
@@ -63,17 +65,25 @@ This means a running Forge agent has zero inbound attack surface by default.
63
65
64
66
## Egress Enforcement
65
67
66
-
Forge restricts outbound network access at three levels:
68
+
Forge restricts outbound network access at multiple levels:
67
69
68
-
### 1. In-Process Enforcer
70
+
### 1. IP Validation
69
71
70
-
The `EgressEnforcer` is a Go `http.RoundTripper` that wraps every outbound HTTP request from in-process tools (`http_request`, `web_search`, LLM API calls). It validates the destination domain against a resolved allowlist before forwarding.
72
+
All egress paths reject non-standard IP formats (octal, hex, packed decimal, leading zeros) that could bypass allowlist checks. IPv6 transition addresses (NAT64, 6to4, Teredo) embedding private IPv4 addresses are also blocked.
71
73
72
-
### 2. Subprocess Proxy
74
+
### 2. In-Process Enforcer
73
75
74
-
Skill scripts and `cli_execute` subprocesses bypass Go-level enforcement. A local `EgressProxy` on `127.0.0.1:<random-port>`validates domains for subprocess HTTP traffic via `HTTP_PROXY`/`HTTPS_PROXY` env var injection.
76
+
The `EgressEnforcer` is a Go`http.RoundTripper` backed by a `SafeTransport` that validates resolved IPs post-DNS. Every outbound HTTP request from in-process tools (`http_request`, `web_search`, LLM API calls) is checked against IP validation, domain allowlist, and post-resolution CIDR blocking.
75
77
76
-
### 3. Kubernetes NetworkPolicy
78
+
### 3. Subprocess Proxy
79
+
80
+
Skill scripts and `cli_execute` subprocesses bypass Go-level enforcement. A local `EgressProxy` on `127.0.0.1:<random-port>` validates domains and resolved IPs for subprocess HTTP traffic via `HTTP_PROXY`/`HTTPS_PROXY` env var injection.
81
+
82
+
### 4. Redirect Credential Stripping
83
+
84
+
HTTP clients used by `http_request` and `webhook_call` tools strip `Authorization`, `Cookie`, and `Proxy-Authorization` headers when a redirect crosses origin boundaries (different scheme, host, or port).
85
+
86
+
### 5. Kubernetes NetworkPolicy
77
87
78
88
In containerized deployments, generated Kubernetes `NetworkPolicy` manifests enforce egress at the pod level, restricting traffic to allowed domains on ports 80/443.
79
89
@@ -244,7 +254,7 @@ Production builds enforce:
244
254
245
255
| Document | Description |
246
256
|----------|-------------|
247
-
| [Egress Security](egress.md) | Deep dive into egress enforcement: profiles, modes, domain matching, proxy architecture, NetworkPolicy |
257
+
| [Egress Security](egress.md) | Deep dive into egress enforcement: IP validation, SafeDialer, profiles, modes, domain matching, proxy architecture, NetworkPolicy |
0 commit comments