Skip to content

Commit 62ea380

Browse files
Mossakadsymepelikhan
authored
docs: add network egress permissions for containerized MCP servers (#192)
* docs: add network egress permissions for containerized MCP servers Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * docs: Enhance MCP documentation and validation for network permissions Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> --------- Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 551a025 commit 62ea380

4 files changed

Lines changed: 159 additions & 4 deletions

File tree

docs/mcps.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,41 @@ tools:
194194
X-Custom-Key: "${secrets.CUSTOM_KEY}"
195195
```
196196

197+
## Network Egress Permissions
198+
199+
Restrict outbound network access for containerized MCP servers using a per‑tool domain allowlist. Define allowed domains under `mcp.permissions.network.allowed`.
200+
201+
```yaml
202+
tools:
203+
fetch:
204+
mcp:
205+
container: mcp/fetch
206+
permissions:
207+
network:
208+
allowed:
209+
- "example.com"
210+
allowed: ["fetch"]
211+
```
212+
213+
Enforcement in compiled workflows:
214+
215+
- A [Squid proxy](https://www.squid-cache.org/) is generated and pinned to a dedicated Docker network for each proxy‑enabled MCP server.
216+
- The MCP container is configured with `HTTP_PROXY`/`HTTPS_PROXY` to point at Squid; iptables rules only allow egress to the proxy.
217+
- The proxy is seeded with an `allowed_domains.txt` built from your `allowed` list; requests to other domains are blocked.
218+
219+
Notes:
220+
221+
- **Only applies to stdio MCP servers with `container`** - Non‑container stdio and `type: http` servers will cause compilation errors
222+
- Use bare domains without scheme; list each domain you intend to permit.
223+
224+
### Validation Rules
225+
226+
The compiler enforces these network permission rules:
227+
228+
- ❌ **HTTP servers**: `network egress permissions do not apply to remote 'type: http' servers`
229+
- ❌ **Non-container stdio**: `network egress permissions only apply to stdio MCP servers that specify a 'container'`
230+
- ✅ **Container stdio**: Network permissions work correctly
231+
197232
## Debugging and Troubleshooting
198233

199234
### MCP Server Inspection
@@ -255,4 +290,4 @@ Error: Tool 'my_tool' not found
255290
## External Resources
256291

257292
- [Model Context Protocol Specification](https://github.com/modelcontextprotocol/specification)
258-
- [GitHub MCP Server](https://github.com/github/github-mcp-server)
293+
- [GitHub MCP Server](https://github.com/github/github-mcp-server)

docs/security-notes.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,33 @@ tools:
174174

175175
#### Egress Filtering
176176

177-
A critical guardrail is strict control over outbound network connections. Consider using network proxies to enforce allowlists for outbound hosts.
177+
A critical guardrail is strict control over outbound network connections. Agentic Workflows now supports declarative network allowlists for containerized MCP servers.
178+
179+
Example (domain allowlist):
180+
181+
```yaml
182+
tools:
183+
fetch:
184+
mcp:
185+
type: stdio
186+
container: mcp/fetch
187+
permissions:
188+
network:
189+
allowed:
190+
- "example.com"
191+
allowed: ["fetch"]
192+
```
193+
194+
Enforcement details:
195+
196+
- Compiler generates a per‑tool Squid proxy and Docker network; MCP egress is forced through the proxy via iptables.
197+
- Only listed domains are reachable; all others are denied at the network layer.
198+
- Applies to `mcp.container` stdio servers. Non‑container stdio and `type: http` servers are not supported and will cause compilation errors.
199+
200+
Operational guidance:
201+
202+
- Use bare domains (no scheme). Explicitly list each domain you intend to permit.
203+
- Prefer minimal allowlists; review the compiled `.lock.yml` to verify proxy setup and rules.
178204

179205
### Agent Security and Prompt Injection Defense
180206

pkg/workflow/mcp-config.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ func ValidateMCPConfigs(tools map[string]any) error {
371371
}
372372

373373
// Validate MCP configuration requirements (before transformation)
374-
if err := validateMCPRequirements(toolName, mcpConfig); err != nil {
374+
if err := validateMCPRequirements(toolName, mcpConfig, config); err != nil {
375375
return err
376376
}
377377
}
@@ -471,7 +471,7 @@ func hasNetworkPermissions(toolConfig map[string]any) (bool, []string) {
471471
}
472472

473473
// validateMCPRequirements validates the specific requirements for MCP configuration
474-
func validateMCPRequirements(toolName string, mcpConfig map[string]any) error {
474+
func validateMCPRequirements(toolName string, mcpConfig map[string]any, toolConfig map[string]any) error {
475475
// Validate 'type' property
476476
mcpType, hasType := mcpConfig["type"]
477477
if err := validateStringProperty(toolName, "type", mcpType, hasType); err != nil {
@@ -489,6 +489,25 @@ func validateMCPRequirements(toolName string, mcpConfig map[string]any) error {
489489
return fmt.Errorf("tool '%s' mcp configuration 'type' value must be one of: stdio, http", toolName)
490490
}
491491

492+
// Validate network permissions usage first
493+
hasNetPerms, _ := hasNetworkPermissions(toolConfig)
494+
if !hasNetPerms {
495+
// Also check if permissions are nested in the mcp config itself
496+
hasNetPerms, _ = hasNetworkPermissions(map[string]any{"mcp": mcpConfig})
497+
}
498+
if hasNetPerms {
499+
switch typeStr {
500+
case "http":
501+
return fmt.Errorf("tool '%s' has network permissions configured, but network egress permissions do not apply to remote 'type: http' servers", toolName)
502+
case "stdio":
503+
// Network permissions only apply to stdio servers with container
504+
_, hasContainer := mcpConfig["container"]
505+
if !hasContainer {
506+
return fmt.Errorf("tool '%s' has network permissions configured, but network egress permissions only apply to stdio MCP servers that specify a 'container'", toolName)
507+
}
508+
}
509+
}
510+
492511
// Validate type-specific requirements
493512
switch typeStr {
494513
case "http":

pkg/workflow/mcp_json_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,81 @@ func TestValidateMCPConfigs(t *testing.T) {
320320
wantErr: true,
321321
errMsg: "missing property 'url'",
322322
},
323+
{
324+
name: "network permissions with HTTP type should fail",
325+
tools: map[string]any{
326+
"httpWithNetPerms": map[string]any{
327+
"mcp": map[string]any{
328+
"type": "http",
329+
"url": "https://example.com",
330+
},
331+
"permissions": map[string]any{
332+
"network": map[string]any{
333+
"allowed": []any{"example.com"},
334+
},
335+
},
336+
"allowed": []any{"tool1"},
337+
},
338+
},
339+
wantErr: true,
340+
errMsg: "network egress permissions do not apply to remote 'type: http' servers",
341+
},
342+
{
343+
name: "network permissions with stdio non-container should fail",
344+
tools: map[string]any{
345+
"stdioNonContainerWithNetPerms": map[string]any{
346+
"mcp": map[string]any{
347+
"type": "stdio",
348+
"command": "python",
349+
},
350+
"permissions": map[string]any{
351+
"network": map[string]any{
352+
"allowed": []any{"example.com"},
353+
},
354+
},
355+
"allowed": []any{"tool1"},
356+
},
357+
},
358+
wantErr: true,
359+
errMsg: "network egress permissions only apply to stdio MCP servers that specify a 'container'",
360+
},
361+
{
362+
name: "network permissions with stdio container should pass",
363+
tools: map[string]any{
364+
"stdioContainerWithNetPerms": map[string]any{
365+
"mcp": map[string]any{
366+
"type": "stdio",
367+
"container": "mcp/fetch",
368+
},
369+
"permissions": map[string]any{
370+
"network": map[string]any{
371+
"allowed": []any{"example.com"},
372+
},
373+
},
374+
"allowed": []any{"tool1"},
375+
},
376+
},
377+
wantErr: false,
378+
},
379+
{
380+
name: "network permissions in mcp section with HTTP type should fail",
381+
tools: map[string]any{
382+
"httpWithMcpNetPerms": map[string]any{
383+
"mcp": map[string]any{
384+
"type": "http",
385+
"url": "https://example.com",
386+
"permissions": map[string]any{
387+
"network": map[string]any{
388+
"allowed": []any{"example.com"},
389+
},
390+
},
391+
},
392+
"allowed": []any{"tool1"},
393+
},
394+
},
395+
wantErr: true,
396+
errMsg: "network egress permissions do not apply to remote 'type: http' servers",
397+
},
323398
}
324399

325400
for _, tt := range tests {

0 commit comments

Comments
 (0)