Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
48 changes: 48 additions & 0 deletions containers/agent/setup-iptables.sh
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,54 @@ if [ -n "$AWF_ENABLE_HOST_ACCESS" ]; then
fi
fi

# Allow host service ports (--allow-host-service-ports) ONLY to host gateway
# These ports bypass DANGEROUS_PORTS restrictions because they are restricted
# to the host gateway IP only (for GitHub Actions services containers).
# Must be applied BEFORE dangerous port RETURN rules so traffic to host gateway
# on these ports is accepted, not dropped.
if [ -n "$AWF_HOST_SERVICE_PORTS" ] && [ -n "$AWF_ENABLE_HOST_ACCESS" ]; then
# Parse port list once, before resolving gateway IPs, so both blocks can use it
IFS=',' read -ra HSP_PORTS <<< "$AWF_HOST_SERVICE_PORTS"

# Resolve host gateway IP
HSP_HOST_GW_IP=$(getent hosts host.docker.internal 2>/dev/null | awk 'NR==1 { print $1 }')
HSP_NET_GW_IP=$(route -n 2>/dev/null | awk '/^0\.0\.0\.0/ { print $2; exit }')

if [ -n "$HSP_HOST_GW_IP" ] && is_valid_ipv4 "$HSP_HOST_GW_IP"; then
echo "[iptables] Allowing host service ports to host gateway ($HSP_HOST_GW_IP): $AWF_HOST_SERVICE_PORTS"
for port in "${HSP_PORTS[@]}"; do
port=$(echo "$port" | xargs)
if ! [[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "[iptables] WARNING: Skipping invalid service port: $port"
continue
fi
if [ -n "$port" ]; then
Comment on lines +251 to +260

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ports from $AWF_HOST_SERVICE_PORTS are passed directly into iptables --dport. Since this env var can come from the host environment (or from the CLI with currently-loose parsing), a malformed value can cause iptables failures and abort the script (set -e). Consider validating each port token in setup-iptables.sh (numeric only, 1-65535) and emitting a clear warning/error for invalid entries before running iptables.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Service port validation in setup-iptables.sh now uses a strict single-port regex (^[1-9][0-9]{0,4}$) with range check 1-65535. Invalid entries are warned and skipped. This is more restrictive than is_valid_port_spec (which accepts ranges) — aligned with the TypeScript validator that only accepts single numeric ports.

echo "[iptables] Allow host service port $port to $HSP_HOST_GW_IP"
# FILTER: allow traffic to host gateway on this port
# (NAT bypass is already handled by the blanket RETURN rule in the host access block above)
iptables -A OUTPUT -p tcp -d "$HSP_HOST_GW_IP" --dport "$port" -j ACCEPT
fi
done
fi

# Also allow to network gateway (same as the host access block does)
if [ -n "$HSP_NET_GW_IP" ] && is_valid_ipv4 "$HSP_NET_GW_IP" && [ "$HSP_NET_GW_IP" != "$HSP_HOST_GW_IP" ]; then
echo "[iptables] Allowing host service ports to network gateway ($HSP_NET_GW_IP): $AWF_HOST_SERVICE_PORTS"
for port in "${HSP_PORTS[@]}"; do
port=$(echo "$port" | xargs)
Comment on lines +246 to +273

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HSP_PORTS is only initialized inside the "host gateway" branch, but the "network gateway" branch iterates over "${HSP_PORTS[@]}" unconditionally. If host.docker.internal doesn't resolve (but a network gateway exists), no rules will be added for the network gateway. Parse AWF_HOST_SERVICE_PORTS into HSP_PORTS before both branches (or guard the second loop) so the fallback path works reliably.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HSP_PORTS is parsed at line 246 (IFS=',' read -ra HSP_PORTS <<< "$AWF_HOST_SERVICE_PORTS"), before both the host gateway branch (line 252) and network gateway branch (line 270). The comment at line 245 explicitly notes this: "Parse port list once, before resolving gateway IPs, so both blocks can use it."

if ! [[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "[iptables] WARNING: Skipping invalid service port: $port"
continue
fi
if [ -n "$port" ]; then
# FILTER: allow traffic to network gateway on this port
# (NAT bypass is already handled by the blanket RETURN rule in the host access block above)
iptables -A OUTPUT -p tcp -d "$HSP_NET_GW_IP" --dport "$port" -j ACCEPT
fi
done
fi
fi

# Block dangerous ports at NAT level (defense-in-depth with Squid ACL filtering)
# These ports are explicitly blocked to prevent access to sensitive services
# even if Squid ACL filtering fails. The ports RETURN from NAT (not redirected)
Expand Down
115 changes: 54 additions & 61 deletions docs-site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions src/cli-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ describe('runMainWorkflow', () => {
);
});

it('passes allowHostServicePorts in hostAccess config when set', async () => {
const configWithServicePorts: WrapperConfig = {
...baseConfig,
enableHostAccess: true,
allowHostPorts: '3000',
allowHostServicePorts: '5432,6379',
};
const dependencies: WorkflowDependencies = {
ensureFirewallNetwork: jest.fn().mockResolvedValue({ squidIp: '172.30.0.10', proxyIp: '172.30.0.30' }),
setupHostIptables: jest.fn().mockResolvedValue(undefined),
writeConfigs: jest.fn().mockResolvedValue(undefined),
startContainers: jest.fn().mockResolvedValue(undefined),
runAgentCommand: jest.fn().mockResolvedValue({ exitCode: 0 }),
};
const performCleanup = jest.fn().mockResolvedValue(undefined);
const logger = createLogger();

await runMainWorkflow(configWithServicePorts, dependencies, { logger, performCleanup });

const expectedHostAccess: HostAccessConfig = {
enabled: true,
allowHostPorts: '3000',
allowHostServicePorts: '5432,6379',
};
expect(dependencies.setupHostIptables).toHaveBeenCalledWith(
'172.30.0.10', 3128, ['8.8.8.8', '8.8.4.4'], undefined, undefined, expectedHostAccess
);
});

it('passes undefined hostAccess when enableHostAccess is not set', async () => {
const dependencies: WorkflowDependencies = {
ensureFirewallNetwork: jest.fn().mockResolvedValue({ squidIp: '172.30.0.10', proxyIp: '172.30.0.30' }),
Expand Down
2 changes: 1 addition & 1 deletion src/cli-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function runMainWorkflow(
// When DoH is enabled, the DoH proxy needs direct HTTPS access to the resolver
const dohProxyIp = config.dnsOverHttps ? '172.30.0.40' : undefined;
const hostAccess: HostAccessConfig | undefined = config.enableHostAccess
? { enabled: true, allowHostPorts: config.allowHostPorts }
? { enabled: true, allowHostPorts: config.allowHostPorts, allowHostServicePorts: config.allowHostServicePorts }
: undefined;
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, dnsServers, apiProxyIp, dohProxyIp, hostAccess);
onHostIptablesSetup?.();
Expand Down
Loading
Loading