Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
449 changes: 449 additions & 0 deletions .github/workflows/example-blocked-domains.lock.yml

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions .github/workflows/example-blocked-domains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
engine: copilot
on:
workflow_dispatch:

network:
firewall: true
allowed:
- defaults
- github
- node
blocked:
- tracker.example.com
- analytics.example.com
---

# Example: Blocked Domains

This workflow demonstrates using the `blocked` field in network configuration to block specific domains while allowing others.

The workflow allows access to:
- Basic infrastructure (`defaults`)
- GitHub domains (`github`)
- Node.js/NPM ecosystem (`node`)

But explicitly blocks:
- `tracker.example.com` (tracking domain)
- `analytics.example.com` (analytics domain)

Blocked domains take precedence over allowed domains, providing fine-grained control over network access.
7 changes: 7 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,13 @@ network:
# Array of Domain name or ecosystem identifier (supports wildcards like
# '*.example.com' and ecosystem names like 'python', 'node')

# List of blocked domains or ecosystem identifiers (e.g., 'python', 'node',
# 'tracker.example.com'). Blocked domains take precedence over allowed domains.
# (optional)
blocked: []
# Array of Domain name or ecosystem identifier to block (supports wildcards like
# '*.example.com' and ecosystem names like 'python', 'node')

# Sandbox configuration for AI engines. Controls agent sandbox (AWF or Sandbox
# Runtime) and MCP gateway.
# (optional)
Expand Down
65 changes: 64 additions & 1 deletion docs/src/content/docs/reference/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,72 @@ network:

# No network access
network: {}

# Block specific domains
network:
allowed:
- defaults # Basic infrastructure
- python # Python/PyPI ecosystem
blocked:
- "tracker.example.com" # Block specific tracking domain
- "analytics.example.com" # Block analytics

# Block entire ecosystems
network:
allowed:
- defaults
- github
- node
blocked:
- python # Block Python/PyPI even if in defaults
```

## Blocking Domains

Use the `blocked` field to block specific domains or ecosystems while allowing others. Blocked domains take precedence over allowed domains, enabling fine-grained control:

```yaml wrap
# Block specific tracking/analytics domains
network:
allowed:
- defaults
- github
blocked:
- "tracker.example.com"
- "analytics.example.com"

# Block entire ecosystem within broader allowed set
network:
allowed:
- defaults # Includes many ecosystems
blocked:
- python # Block Python/PyPI specifically

# Combine domain and ecosystem blocking
network:
allowed:
- defaults
- github
- node
blocked:
- python # Block Python ecosystem
- "cdn.example.com" # Block specific CDN
```

## Security Model
:::tip[When to Use Blocked Domains]
- **Privacy**: Block tracking and analytics domains while allowing legitimate services
- **Security**: Block known malicious or compromised domains
- **Compliance**: Enforce organizational network policies
- **Fine-grained control**: Allow broad ecosystem access but block specific problematic domains
:::

**Key behaviors**:
- Blocked domains are subtracted from the allowed list
- Supports both individual domains and ecosystem identifiers
- Blocked domains include all subdomains (like allowed domains)
- Useful for blocking specific domains within broader ecosystem allowlists

## Configuration

Network permissions follow the principle of least privilege with four access levels:

Expand Down
9 changes: 9 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2038,6 +2038,15 @@
},
"$comment": "Empty array is valid and means deny all network access. Omit the field entirely or use network: defaults to use default network permissions."
},
"blocked": {
"type": "array",
"description": "List of blocked domains or ecosystem identifiers (e.g., 'python', 'node', 'tracker.example.com'). Blocked domains take precedence over allowed domains.",
"items": {
"type": "string",
"description": "Domain name or ecosystem identifier to block (supports wildcards like '*.example.com' and ecosystem names like 'python', 'node')"
},
"$comment": "Blocked domains are subtracted from the allowed list. Useful for blocking specific domains or ecosystems within broader allowed categories."
},
"firewall": {
"description": "AWF (Agent Workflow Firewall) configuration for network egress control. Only supported for Copilot engine.",
"deprecated": true,
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
}

awfArgs = append(awfArgs, "--allow-domains", allowedDomains)

// Add blocked domains if specified
blockedDomains := formatBlockedDomains(workflowData.NetworkPermissions)
if blockedDomains != "" {
awfArgs = append(awfArgs, "--block-domains", blockedDomains)
claudeLog.Printf("Added blocked domains: %s", blockedDomains)
}

awfArgs = append(awfArgs, "--log-level", awfLogLevel)
awfArgs = append(awfArgs, "--proxy-logs-dir", "/tmp/gh-aw/sandbox/firewall/logs")

Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri
}

awfArgs = append(awfArgs, "--allow-domains", allowedDomains)

// Add blocked domains if specified
blockedDomains := formatBlockedDomains(workflowData.NetworkPermissions)
if blockedDomains != "" {
awfArgs = append(awfArgs, "--block-domains", blockedDomains)
codexEngineLog.Printf("Added blocked domains: %s", blockedDomains)
}

awfArgs = append(awfArgs, "--log-level", awfLogLevel)
awfArgs = append(awfArgs, "--proxy-logs-dir", "/tmp/gh-aw/sandbox/firewall/logs")

Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/copilot_engine_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st
}

awfArgs = append(awfArgs, "--allow-domains", allowedDomains)

// Add blocked domains if specified
blockedDomains := formatBlockedDomains(workflowData.NetworkPermissions)
if blockedDomains != "" {
awfArgs = append(awfArgs, "--block-domains", blockedDomains)
copilotExecLog.Printf("Added blocked domains: %s", blockedDomains)
}

awfArgs = append(awfArgs, "--log-level", awfLogLevel)
awfArgs = append(awfArgs, "--proxy-logs-dir", "/tmp/gh-aw/sandbox/firewall/logs")

Expand Down
61 changes: 61 additions & 0 deletions pkg/workflow/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,67 @@ func GetClaudeAllowedDomainsWithSafeInputs(network *NetworkPermissions, hasSafeI
return mergeDomainsWithNetwork(ClaudeDefaultDomains, network)
}

// GetBlockedDomains returns the blocked domains from network permissions
// Returns empty slice if no network permissions configured or no domains blocked
// The returned list is sorted and deduplicated
// Supports ecosystem identifiers (same as allowed domains)
func GetBlockedDomains(network *NetworkPermissions) []string {
if network == nil {
domainsLog.Print("No network permissions specified, no blocked domains")
return []string{}
}

// Handle empty blocked list
if len(network.Blocked) == 0 {
domainsLog.Print("Empty blocked list, no domains blocked")
return []string{}
}

domainsLog.Printf("Processing %d blocked domains/ecosystems", len(network.Blocked))

// Process the blocked list, expanding ecosystem identifiers if present
// Use a map to deduplicate domains
domainMap := make(map[string]bool)
for _, domain := range network.Blocked {
// Try to get domains for this ecosystem category
ecosystemDomains := getEcosystemDomains(domain)
if len(ecosystemDomains) > 0 {
// This was an ecosystem identifier, expand it
domainsLog.Printf("Expanded ecosystem '%s' to %d domains", domain, len(ecosystemDomains))
for _, d := range ecosystemDomains {
domainMap[d] = true
}
} else {
// Add the domain as-is (regular domain name)
domainMap[domain] = true
}
}

// Convert map to sorted slice
expandedDomains := make([]string, 0, len(domainMap))
for domain := range domainMap {
expandedDomains = append(expandedDomains, domain)
}
SortStrings(expandedDomains)

return expandedDomains
}

// formatBlockedDomains formats blocked domains as a comma-separated string suitable for AWF's --block-domains flag
// Returns empty string if no blocked domains
func formatBlockedDomains(network *NetworkPermissions) string {
if network == nil {
return ""
}

blockedDomains := GetBlockedDomains(network)
if len(blockedDomains) == 0 {
return ""
}

return strings.Join(blockedDomains, ",")
}

// computeAllowedDomainsForSanitization computes the allowed domains for sanitization
// based on the engine and network configuration, matching what's provided to the firewall
func (c *Compiler) computeAllowedDomainsForSanitization(data *WorkflowData) string {
Expand Down
Loading
Loading