Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
48b1f0c
Add proxy configuration support for MCP tools with network restrictions
Mossaka Aug 19, 2025
efdcf8f
Enhance MCP configuration schema with additional properties for conta…
Mossaka Aug 19, 2025
a4ab627
Refactor proxy configuration handling: consolidate proxy file generat…
Mossaka Aug 19, 2025
58ce788
Update workflow triggers for test-proxy
Mossaka Aug 19, 2025
6e2c060
Removed the step to start proxy services
Mossaka Aug 19, 2025
19440bc
Update permissions in test-proxy workflow to allow issue reporting
Mossaka Aug 19, 2025
c1b7c39
Fix proxy service naming in Docker Compose configuration
Mossaka Aug 19, 2025
7809943
Enhance MCP configuration handling: pass tool name to getMCPConfig an…
Mossaka Aug 19, 2025
4658a28
Update Docker command in transformContainerToDockerCommand for proxy …
Mossaka Aug 19, 2025
622f52b
Remove unnecessary includae statements from test-proxy workflow docum…
Mossaka Aug 19, 2025
36f9d11
Enhance proxy configuration: update Docker Compose command for proxy-…
Mossaka Aug 19, 2025
19f51d8
Add DEBUG environment variable to MCP configuration for enhanced logging
Mossaka Aug 19, 2025
9475738
Debugging: Update Claude Code Action to use forked version
Mossaka Aug 20, 2025
1bdca9a
Merge branch 'main' into mossaka/proxy-2
Mossaka Aug 20, 2025
ec392dd
Enhance proxy setup: remove unused proxy domain, pre-pull Docker imag…
Mossaka Aug 20, 2025
699d675
regenerate the yaml
Mossaka Aug 20, 2025
d627d3b
Update Claude Code Action to use forked version
Mossaka Aug 20, 2025
ba9ff3a
Enhance proxy configuration: enforce egress control for tools, update…
Mossaka Aug 20, 2025
d86925d
regenerate the yaml
Mossaka Aug 20, 2025
7db185b
Enhance proxy configuration: add iptables rules to accept established…
Mossaka Aug 20, 2025
1e0731b
Enhance proxy configuration: update iptables rules for established co…
Mossaka Aug 20, 2025
0d5e231
fmt
Mossaka Aug 20, 2025
5e0d70f
fixed some linting issues
Mossaka Aug 20, 2025
be63988
Merge branch 'main' into mossaka/proxy-2
pelikhan Aug 22, 2025
c5bd900
Refactor GitHub Actions workflow to enhance output sanitization and s…
pelikhan Aug 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
616 changes: 616 additions & 0 deletions .github/workflows/test-proxy.lock.yml

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions .github/workflows/test-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
on:
pull_request:
branches: [ "main" ]
workflow_dispatch:

permissions:
issues: write # needed to write the output report to an issue

tools:
fetch:
mcp:
type: stdio
container: mcp/fetch
permissions:
network:
allowed:
- "example.com"
allowed:
- "fetch"

github:
allowed:
- "create_issue"
- "create_comment"
- "get_issue"

engine: claude
runs-on: ubuntu-latest
---

# Test Network Permissions

## Task Description

Test the MCP network permissions feature to validate that domain restrictions are properly enforced.

- Use the fetch tool to successfully retrieve content from `https://example.com/` (the only allowed domain)
- Attempt to access blocked domains and verify they fail with network errors:
- `https://httpbin.org/json`
- `https://api.github.com/user`
- `https://www.google.com/`
- `http://malicious-example.com/`
- Verify that all blocked requests fail at the network level (proxy enforcement)
- Confirm that only example.com is accessible through the Squid proxy

Create a GitHub issue with the test results, documenting:
- Which domains were successfully accessed vs blocked
- Error messages received for blocked domains
- Confirmation that network isolation is working correctly
- Any security observations or recommendations

The test should demonstrate that MCP containers are properly isolated and can only access explicitly allowed domains through the network proxy.
139 changes: 121 additions & 18 deletions pkg/parser/schemas/mcp_config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,133 @@
"command": {
"type": "string",
"description": "Command for stdio MCP connections"
},
"container": {
"type": "string",
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$",
"description": "Container image for stdio MCP connections (alternative to command)"
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Arguments for command or container execution"
},
"env": {
"type": "object",
"patternProperties": {
"^[A-Z_][A-Z0-9_]*$": {
"type": "string"
}
},
"additionalProperties": false,
"description": "Environment variables for MCP server"
},
"headers": {
"type": "object",
"patternProperties": {
"^[A-Za-z0-9-]+$": {
"type": "string"
}
},
"additionalProperties": false,
"description": "HTTP headers for HTTP MCP connections"
},
"permissions": {
"type": "object",
"properties": {
"network": {
"type": "object",
"properties": {
"allowed": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*$",
"description": "Allowed domain name"
},
"minItems": 1,
"uniqueItems": true,
"description": "List of allowed domain names for network access"
}
},
"required": ["allowed"],
"additionalProperties": false,
"description": "Network access permissions"
}
},
"additionalProperties": false,
"description": "Permissions configuration for container-based MCP servers"
}
},
"required": ["type"],
"additionalProperties": true,
"if": {
"properties": {
"type": {
"const": "http"
"additionalProperties": false,
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "http"
}
}
},
"then": {
"required": ["url"],
"not": {
"anyOf": [
{"required": ["command"]},
{"required": ["container"]},
{"required": ["permissions"]}
]
}
}
}
},
"then": {
"required": ["url"]
},
"else": {
"if": {
"properties": {
"type": {
"const": "stdio"
},
{
"if": {
"properties": {
"type": {
"const": "stdio"
}
}
},
"then": {
"anyOf": [
{"required": ["command"]},
{"required": ["container"]}
],
"not": {
"allOf": [
{"required": ["command"]},
{"required": ["container"]}
]
}
}
},
"then": {
"required": ["command"]
{
"if": {
"required": ["container"]
},
"then": {
"properties": {
"type": {
"const": "stdio"
}
}
}
},
{
"if": {
"required": ["permissions"]
},
"then": {
"required": ["container"],
"properties": {
"type": {
"const": "stdio"
}
}
}
}
}
]
}
64 changes: 63 additions & 1 deletion pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,8 @@ func (c *Compiler) generateSafetyChecks(yaml *strings.Builder, data *WorkflowDat
func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, engine AgenticEngine) {
// Collect tools that need MCP server configuration
var mcpTools []string
var proxyTools []string

for toolName, toolValue := range tools {
// Standard MCP tools
if toolName == "github" {
Expand All @@ -1920,12 +1922,72 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any,
// Check if it's explicitly marked as MCP type in the new format
if hasMcp, _ := hasMCPConfig(mcpConfig); hasMcp {
mcpTools = append(mcpTools, toolName)

// Check if this tool needs proxy
if needsProxySetup, _ := needsProxy(mcpConfig); needsProxySetup {
proxyTools = append(proxyTools, toolName)
}
}
}
}

// Sort MCP tools to ensure stable code generation
// Sort tools to ensure stable code generation
sort.Strings(mcpTools)
sort.Strings(proxyTools)

// Generate proxy configuration files inline for proxy-enabled tools
// These files will be used automatically by docker compose when MCP tools run
if len(proxyTools) > 0 {
yaml.WriteString(" - name: Setup Proxy Configuration for MCP Network Restrictions\n")
yaml.WriteString(" run: |\n")
yaml.WriteString(" echo \"Generating proxy configuration files for MCP tools with network restrictions...\"\n")
yaml.WriteString(" \n")

// Generate proxy configurations inline for each proxy-enabled tool
for _, toolName := range proxyTools {
if toolConfig, ok := tools[toolName].(map[string]any); ok {
c.generateInlineProxyConfig(yaml, toolName, toolConfig)
}
}

yaml.WriteString(" echo \"Proxy configuration files generated.\"\n")

// Pre-pull images and start squid proxy ahead of time to avoid timeouts
yaml.WriteString(" - name: Pre-pull images and start Squid proxy\n")
yaml.WriteString(" run: |\n")
yaml.WriteString(" set -e\n")
yaml.WriteString(" echo 'Pre-pulling Docker images for proxy-enabled MCP tools...'\n")
yaml.WriteString(" docker pull ubuntu/squid:latest\n")

// Pull each tool's container image if specified, and bring up squid service
for _, toolName := range proxyTools {
if toolConfig, ok := tools[toolName].(map[string]any); ok {
if mcpConf, err := getMCPConfig(toolConfig, toolName); err == nil {
if containerVal, hasContainer := mcpConf["container"]; hasContainer {
if containerStr, ok := containerVal.(string); ok && containerStr != "" {
yaml.WriteString(fmt.Sprintf(" echo 'Pulling %s for tool %s'\n", containerStr, toolName))
yaml.WriteString(fmt.Sprintf(" docker pull %s\n", containerStr))
}
}
}
yaml.WriteString(fmt.Sprintf(" echo 'Starting squid-proxy service for %s'\n", toolName))
yaml.WriteString(fmt.Sprintf(" docker compose -f docker-compose-%s.yml up -d squid-proxy\n", toolName))

// Enforce that egress from this tool's network can only reach the Squid proxy
subnetCIDR, squidIP, _ := computeProxyNetworkParams(toolName)
yaml.WriteString(fmt.Sprintf(" echo 'Enforcing egress to proxy for %s (subnet %s, squid %s)'\n", toolName, subnetCIDR, squidIP))
yaml.WriteString(" if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi\n")
// Accept established/related connections first (position 1)
yaml.WriteString(" $SUDO iptables -C DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || $SUDO iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n")
// Accept all egress from Squid IP (position 2)
yaml.WriteString(fmt.Sprintf(" $SUDO iptables -C DOCKER-USER -s %s -j ACCEPT 2>/dev/null || $SUDO iptables -I DOCKER-USER 2 -s %s -j ACCEPT\n", squidIP, squidIP))
// Allow traffic to squid:3128 from the subnet (position 3)
yaml.WriteString(fmt.Sprintf(" $SUDO iptables -C DOCKER-USER -s %s -d %s -p tcp --dport 3128 -j ACCEPT 2>/dev/null || $SUDO iptables -I DOCKER-USER 3 -s %s -d %s -p tcp --dport 3128 -j ACCEPT\n", subnetCIDR, squidIP, subnetCIDR, squidIP))
// Then reject all other egress from that subnet (append to end)
yaml.WriteString(fmt.Sprintf(" $SUDO iptables -C DOCKER-USER -s %s -j REJECT 2>/dev/null || $SUDO iptables -A DOCKER-USER -s %s -j REJECT\n", subnetCIDR, subnetCIDR))
}
}
}

// If no MCP tools, no configuration needed
if len(mcpTools) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3210,7 +3210,7 @@ func TestTransformImageToDockerCommand(t *testing.T) {
mcpConfig[k] = v
}

err := transformContainerToDockerCommand(mcpConfig)
err := transformContainerToDockerCommand(mcpConfig, "test")

if tt.wantErr {
if err == nil {
Expand Down
Loading