Skip to content

fix: allow host service ports for GitHub Actions services containers#1436

Merged
Mossaka merged 1 commit intomainfrom
fix/allow-host-service-ports
Mar 26, 2026
Merged

fix: allow host service ports for GitHub Actions services containers#1436
Mossaka merged 1 commit intomainfrom
fix/allow-host-service-ports

Conversation

@Mossaka
Copy link
Copy Markdown
Collaborator

@Mossaka Mossaka commented Mar 25, 2026

Summary

  • Adds --allow-host-service-ports <ports> CLI flag that allows TCP connections to the host gateway IP on ports normally blocked as "dangerous" (e.g., PostgreSQL 5432, Redis 6379, MySQL 3306)
  • Designed for GitHub Actions services: containers that publish to the host via port mapping — the agent can reach these services on the host but still cannot reach databases on the internet
  • Automatically enables --enable-host-access when used; bypasses DANGEROUS_PORTS validation since traffic is restricted to host gateway only

Files changed

  • src/types.ts — Added allowHostServicePorts field to WrapperConfig
  • src/cli.ts — Added --allow-host-service-ports CLI option with validation and auto-enable logic
  • src/docker-manager.ts — Passes AWF_HOST_SERVICE_PORTS env var to agent and iptables-init containers
  • containers/agent/setup-iptables.sh — Adds iptables NAT RETURN + FILTER ACCEPT rules for service ports to host gateway, before dangerous port blocking
  • tests/fixtures/awf-runner.ts — Added allowHostServicePorts to test runner options
  • tests/integration/host-tcp-services.test.ts — Integration tests for the new flag

Closes gh-aw#22939

Test plan

  • npm run build passes
  • npm test passes (1167 unit tests)
  • npm run lint passes (0 errors)
  • Integration test: --allow-host-service-ports 5432 allows TCP to host gateway on port 5432
  • Integration test: dangerous ports are still blocked to internet destinations
  • Integration test: auto-enables host access
  • Integration test: multiple service ports work

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 25, 2026 21:16
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 82.69% 82.45% 📉 -0.24%
Statements 82.35% 82.10% 📉 -0.25%
Functions 81.11% 80.83% 📉 -0.28%
Branches 75.88% 75.53% 📉 -0.35%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 60.5% → 59.4% (-1.09%) 60.9% → 59.7% (-1.19%)
src/docker-manager.ts 86.3% → 86.6% (+0.33%) 85.7% → 86.0% (+0.32%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

Copy link
Copy Markdown
Collaborator Author

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

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

Security Review: --allow-host-service-ports

Reviewed the PR diff, containers/agent/setup-iptables.sh, src/squid-config.ts, src/cli.ts, src/docker-manager.ts, and src/types.ts.

Overall Assessment

The approach is fundamentally sound. The design correctly restricts dangerous-port access to the host gateway IP only, using destination-scoped iptables rules rather than blanket allowances. The use case (GitHub Actions services: containers) is legitimate and the implementation avoids weakening the external egress firewall. There are a few issues worth addressing, categorized below.


1. Bypass Risk — Can this reach non-host destinations? LOW RISK - SOUND

The iptables rules are properly destination-restricted:

iptables -A OUTPUT -p tcp -d "$HSP_HOST_GW_IP" --dport "$port" -j ACCEPT

The -d constraint ensures only the resolved host.docker.internal IP (and optionally the network gateway) can be reached on these ports. External IPs on dangerous ports still hit the final DROP rule. The integration test should block dangerous port to non-host destinations (internet) correctly validates this.

One observation: the NAT RETURN rules in the new block are redundant when AWF_ENABLE_HOST_ACCESS is set. The existing host access block (line 175) already does a blanket iptables -t nat -A OUTPUT -d "$HOST_GATEWAY_IP" -j RETURN for all ports to the host gateway. The new per-port NAT RETURN rules will never be evaluated because the blanket RETURN already matched. This isn't a security bug — the FILTER chain's per-port ACCEPT is what actually controls access — but the redundant NAT rules may confuse future maintainers into thinking they're load-bearing. Consider adding a comment clarifying that the effective security enforcement is in the FILTER chain, not NAT.

2. Port Validation / Injection — LOW RISK, ONE CONCERN

CLI-side validation (cli.ts): Good. Validates parseInt(port, 10) and checks 1 <= port <= 65535. Rejects NaN. This prevents command injection via the --allow-host-service-ports CLI flag.

Shell-side (setup-iptables.sh): The port values are passed via environment variable AWF_HOST_SERVICE_PORTS and used directly in --dport "$port". The xargs trim is the only sanitization. Unlike the existing --allow-host-ports code path in squid-config.ts (which does port.replace(/[^0-9-]/g, '')), the new code has no shell-side sanitization. Since iptables --dport expects a numeric value and rejects non-numeric input (causing the iptables command to fail with an error, not execute injected commands), and the CLI validates before passing to the container, this is low risk. However, for defense-in-depth consistency, consider adding numeric validation in the shell script as well:

if ! [[ "$port" =~ ^[0-9]+$ ]]; then
  echo "[iptables] WARNING: Skipping invalid port: $port"
  continue
fi

3. Privilege Escalation — NO ISSUE

The iptables rules are set by the awf-iptables-init init container, which runs in a separate container sharing the agent's network namespace. The agent container never gets NET_ADMIN capability. CAP_SYS_CHROOT and CAP_SYS_ADMIN are dropped via capsh before user code runs. The agent cannot modify iptables rules after startup. No escalation path introduced.

4. Data Exfiltration via Host Services — MEDIUM RISK - ACCEPTED BY DESIGN

This is the most significant trade-off. With --allow-host-service-ports 5432, a compromised agent could:

  • Write arbitrary data to a PostgreSQL/Redis/MySQL service running on the host
  • Use the database as a side channel to exfiltrate data if the database is externally accessible

This is inherent to the feature's design and documented in the PR. The mitigation is that this is opt-in and requires the workflow author to explicitly specify ports. The alternative (no database access) makes the services: use case impossible. Recommendation: Consider adding a CLI warning when this flag is used, similar to the existing --ssl-bump warning:

[WARN] --allow-host-service-ports bypasses dangerous port restrictions for host-local traffic.
[WARN] Ensure host services on these ports do not provide external network access.

5. Defense in Depth (Squid Layer) — MEDIUM - GAP EXISTS BUT ACCEPTABLE

Traffic to host service ports bypasses Squid entirely. The iptables NAT RETURN ensures packets go directly to the host gateway, not through the Squid proxy. This means:

  • No domain-based filtering (acceptable — it's an IP, not a domain)
  • No Squid access logging for these connections (audit gap)
  • No Safe_ports ACL check at the Squid layer

This is the correct design — Squid can't meaningfully filter direct TCP connections to host.docker.internal:5432 (it's not HTTP/HTTPS traffic). The iptables FILTER chain is the right enforcement layer here. However, the audit gap is worth noting: connections to host service ports won't appear in Squid's access.log or audit.jsonl. The iptables audit dump (iptables-audit.txt) will show the rules but not individual connections. Consider whether the iptables LOG rules should cover accepted host-service-port traffic for observability, not just blocked traffic.

6. Race Conditions — NO ISSUE

The iptables-init container runs as an init container (depends_on with condition: service_completed_successfully). The agent's entrypoint waits for /tmp/awf-init/ready before executing the user command. The capability drop happens in the agent's entrypoint after init completes. No race window exists.

7. Interaction with Other Flags — LOW RISK

  • --enable-dind: DinD uses the same network namespace. Host service port rules apply within the DinD context. No conflict.
  • --enable-api-proxy: API proxy sidecar is at 172.30.0.30, not the host gateway IP. No interaction.
  • --allow-host-ports: These two flags have different semantics (--allow-host-ports rejects dangerous ports, --allow-host-service-ports allows them). Both can be used simultaneously. No conflict — they append independent FILTER ACCEPT rules.
  • --ssl-bump: SSL bump only applies to Squid-proxied traffic. Host service port traffic bypasses Squid, so no interaction.

8. Additional Observations

DANGEROUS_PORTS list drift: The DANGEROUS_PORTS array in setup-iptables.sh has fewer entries than in squid-config.ts (missing 5984, 6984, 8086, 8088, 9200, 9300). This is a pre-existing issue, not introduced by this PR, but worth noting since --allow-host-service-ports interacts with the iptables-level list. If a user passes --allow-host-service-ports 9200 (Elasticsearch), the iptables dangerous-port RETURN + DROP won't block it to external IPs because port 9200 isn't in the shell-side list. The Squid Safe_ports ACL would still block it for proxied traffic, but direct connections (proxy-unaware) on that port would not be caught by the iptables dangerous-port defense.

No unit tests for Squid config interaction: The PR doesn't need to modify squid-config.ts (correct — this feature is iptables-only), but there are no unit tests verifying that --allow-host-service-ports values are NOT validated against DANGEROUS_PORTS at the Squid layer. This is fine since the code paths are separate, but a comment in cli.ts validation explaining why DANGEROUS_PORTS check is intentionally skipped would help.


Summary

Category Severity Status
Bypass to non-host destinations Low Sound — destination-scoped rules
Port injection Low CLI validates; recommend shell-side sanitization
Privilege escalation None No issue
Data exfiltration via host services Medium Accepted by design; recommend warning
Squid-level defense gap Medium Acceptable — correct layer; audit gap
Race conditions None No issue
Flag interactions Low No conflicts

Verdict: Approve with minor suggestions. The security model is correct. The feature intentionally and narrowly relaxes dangerous-port restrictions for a specific destination (host gateway only). The main recommendations are: (1) shell-side port validation for defense-in-depth, (2) CLI warning about the security implications, and (3) documenting the audit gap for host service port traffic.

Copy link
Copy Markdown
Collaborator Author

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

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

Code Review: --allow-host-service-ports

Overall the feature is well-motivated and the approach is sound. There are a few issues ranging from a bug to minor suggestions.


Bugs

1. HSP_PORTS may be unset when used for network gateway rules
containers/agent/setup-iptables.sh — The HSP_PORTS array is only set inside the if [ -n "$HSP_HOST_GW_IP" ] && is_valid_ipv4 "$HSP_HOST_GW_IP" block. If host.docker.internal fails to resolve but the network gateway IP is valid, the second for port in "${HSP_PORTS[@]}" loop iterates over an unset variable — silently doing nothing. This is a logic bug: service ports would not be allowed to the network gateway even though the condition was met.

Fix: Move the IFS=',' read -ra HSP_PORTS <<< "$AWF_HOST_SERVICE_PORTS" line above both if blocks (right after HSP_NET_GW_IP=...), matching the pattern used by the existing AWF_ALLOW_HOST_PORTS handling.


Correctness Concerns

2. NAT RETURN rules in the new block are redundant (no-ops)
The existing AWF_ENABLE_HOST_ACCESS block at line 175 already adds a blanket iptables -t nat -A OUTPUT -d "$HOST_GATEWAY_IP" -j RETURN (all ports, all protocols). Since --allow-host-service-ports auto-enables host access, this blanket NAT RETURN is always present. The port-specific NAT RETURN rules in the new block (iptables -t nat -A OUTPUT -p tcp -d "$HSP_HOST_GW_IP" --dport "$port" -j RETURN) will never match because the blanket rule already consumed the traffic.

This isn't a correctness bug (the FILTER ACCEPT rules that follow are needed and do the real work), but the NAT rules are dead code. Worth either:

  • (a) Removing the NAT RETURN rules from the new block and adding a comment explaining why they're not needed, or
  • (b) Adding a comment clarifying they're defense-in-depth in case the blanket RETURN is ever removed.

Same applies to the network gateway section.

3. Test should allow TCP connection to host service on a dangerous port uses port 15432
Port 15432 is NOT in the DANGEROUS_PORTS list (which includes 5432 but not 15432). This means the test would pass even without --allow-host-service-ports, since port 15432 is not blocked by the dangerous ports rules. The test name is misleading and doesn't validate the core feature.

The real validation comes from the port 5432 test, but that one may be skipped if the port is already in use. Consider either:

  • Using a port that IS in DANGEROUS_PORTS for the primary test (e.g., 6379 for Redis, which is less likely to be in use)
  • Or starting the echo server on 5432 as the primary test, with a clearer skip message

Suggestions

4. Port range support inconsistency
--allow-host-ports supports port ranges (e.g., 3000-3010), but --allow-host-service-ports validation in cli.ts uses parseInt() which rejects ranges. The iptables --dport flag supports ranges natively. If port ranges aren't intended for service ports, that's fine, but it should be documented. If they are intended, the validation needs to handle the start-end syntax.

5. CLI option placement
The new --allow-host-service-ports option is placed after --allow-host-ports but before --enable-dind. This is good grouping. Minor nit: considering the auto-enable behavior, the help text should mention that --enable-host-access is not required (currently it says "Auto-enables host access" which is clear enough).

6. Consider extracting shared port-parsing logic
The port parsing pattern (IFS=',' read -ra ... ; for port in ...; port=$(echo "$port" | xargs)) is now repeated three times in setup-iptables.sh (for AWF_ALLOW_HOST_PORTS, AWF_HOST_SERVICE_PORTS host gateway, and AWF_HOST_SERVICE_PORTS network gateway). A helper function would reduce duplication, though this is minor.

7. AWF_ALLOW_HOST_PORTS env var passthrough to iptables-init
The existing AWF_ALLOW_HOST_PORTS is passed to the iptables-init container at line 1213 in docker-manager.ts. The new AWF_HOST_SERVICE_PORTS is correctly added at line 1219+. Good.


Test Coverage Assessment

The integration tests cover:

  • Auto-enable host access ✓
  • TCP connection on a (non-dangerous) port to host ✓ (but see issue #3)
  • TCP connection on actual dangerous port (5432) ✓ (but may skip)
  • Blocking dangerous port to internet ✓
  • Multiple service ports ✓

Missing test cases:

  • What happens when --allow-host-service-ports is combined with --allow-host-ports on the same port? (should work, but good to verify no duplicate rule issues)
  • Invalid port validation (port 0, port 99999, non-numeric) — these are validated in cli.ts but no test exercises the error path
  • Empty string input (--allow-host-service-ports "")

Verdict

The feature design is correct and the iptables rules will work as intended (traffic flows via the FILTER ACCEPT rules; the NAT RETURN is redundant but harmless). The HSP_PORTS scoping bug (#1) should be fixed before merge. The test on port 15432 (#3) should be reworked to actually test a dangerous port. The other items are suggestions for improvement.

Status: Request changes (for bug #1; rest are suggestions)

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results — Copilot Engine

Test Result
GitHub MCP (last 2 merged PRs) #1420 fix: restore GITHUB_API_URL in agent container when api-proxy is enabled, #1419 fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled
Playwright (github.com title) ✅ "GitHub · Change is constant. GitHub keeps you ahead."
File write + read /tmp/gh-aw/agent/smoke-test-copilot-23564569294.txt
Bash tool cat confirmed file contents

Overall: PASS

cc @Mossaka

📰 BREAKING: Report filed by Smoke Copilot for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Smoke test results (run 23564569383)

Test Result
GitHub MCP – last 2 merged PRs: #1350 docs: Fix proxy env var docs..., #1414 fix: write apiKeyHelper...
Playwright – github.com title contains "GitHub"
File write – /tmp/gh-aw/agent/smoke-test-claude-23564569383.txt
Bash verify – file contents confirmed

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1436

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new CLI flag to allow TCP access to GitHub Actions services: containers via the host gateway on ports that are otherwise considered “dangerous”, while keeping dangerous-port egress blocked for non-host destinations.

Changes:

  • Added --allow-host-service-ports <ports> CLI option and WrapperConfig.allowHostServicePorts plumbing.
  • Passed AWF_HOST_SERVICE_PORTS into the agent/iptables init path and added iptables exceptions for host-gateway-only service ports.
  • Added integration tests and runner fixture support for the new flag.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/integration/host-tcp-services.test.ts New integration tests covering host-service-port allowlisting behavior.
tests/fixtures/awf-runner.ts Adds allowHostServicePorts option wiring for test runs.
src/types.ts Extends WrapperConfig with allowHostServicePorts and docs.
src/docker-manager.ts Exposes AWF_HOST_SERVICE_PORTS to containers (incl. iptables-init).
src/cli.ts Adds the new CLI option, validates it, and auto-enables host access.
containers/agent/setup-iptables.sh Adds NAT/FILTER rules to allow specified service ports to host gateway prior to dangerous-port enforcement.
Comments suppressed due to low confidence (2)

tests/integration/host-tcp-services.test.ts:146

  • The "block dangerous port to non-host destinations" assertion currently treats any connection failure as "BLOCKED" (curl failure -> echo BLOCKED). This can pass even if the firewall accidentally allows the traffic (e.g., if the remote port is simply closed/refuses). Consider asserting something that specifically indicates firewall enforcement (e.g., check iptables-audit.txt / init logs for the expected ACCEPT rule being limited to host gateway, or use a controlled non-host destination inside the test environment where the port is known-open).
  test('should block dangerous port to non-host destinations (internet)', async () => {
    // Even with --allow-host-service-ports 5432, traffic to external IPs
    // on port 5432 should still be blocked
    const result = await runner.runWithSudo(
      'bash -c \'curl -s --connect-timeout 5 http://example.com:5432/ 2>&1 || echo "BLOCKED"\'',
      {
        allowDomains: ['example.com'],
        allowHostServicePorts: '5432',
        logLevel: 'debug',
        timeout: 60000,
      }
    );

    expect(result).toSucceed();
    // The connection to example.com:5432 should fail because the port is only
    // allowed to the host gateway, not to internet destinations
    expect(result.stdout).toContain('BLOCKED');
  }, 120000);

tests/integration/host-tcp-services.test.ts:109

  • If binding to 5432 fails, the test logs and returns, which records as a pass rather than a skip. To avoid masking coverage gaps, consider using a runtime-conditional skip pattern (e.g., decide in beforeAll whether the port is available, then choose test vs test.skip) so CI output clearly shows the test was skipped.
    try {
      server = await startTcpEchoServer(TEST_PORT);
    } catch {
      // If we can't bind to 5432 (e.g., already in use or no privileges), skip
      console.log(`Skipping test: could not bind to port ${TEST_PORT}`);
      return;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +228 to +237

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" =~ ^[0-9]+$ ]]; then
echo "[iptables] WARNING: Skipping invalid port: $port"
continue
fi
if [ -n "$port" ]; then
Copy link

Copilot AI Mar 25, 2026

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
Collaborator 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.

// Pass host service ports to container for setup-iptables.sh (if specified)
// These ports bypass DANGEROUS_PORTS validation and are only allowed to host gateway
if (config.allowHostServicePorts) {
environment.AWF_HOST_SERVICE_PORTS = config.allowHostServicePorts;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

generateDockerCompose only applies host access wiring (extra_hosts + AWF_ENABLE_HOST_ACCESS) when config.enableHostAccess is set. If WrapperConfig.allowHostServicePorts is used programmatically without also setting enableHostAccess, AWF_HOST_SERVICE_PORTS will be passed but the iptables rule block won't run (it requires AWF_ENABLE_HOST_ACCESS) and host.docker.internal may not resolve. Consider treating allowHostServicePorts as implicitly enabling host access here to match the flag/docs behavior.

Suggested change
environment.AWF_HOST_SERVICE_PORTS = config.allowHostServicePorts;
environment.AWF_HOST_SERVICE_PORTS = config.allowHostServicePorts;
// Treat allowHostServicePorts as implicitly enabling host access so that
// setup-iptables.sh applies the required host access rules when this is set
if (!config.enableHostAccess) {
environment.AWF_ENABLE_HOST_ACCESS = 'true';
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. Added a safety net in docker-manager.ts: when allowHostServicePorts is set but AWF_ENABLE_HOST_ACCESS is not, it's now automatically set to 'true'. This ensures programmatic usage works even without the CLI's auto-enable logic.

Comment on lines +74 to +75
// Start a TCP echo server on a dangerous port (Redis 6379, in DANGEROUS_PORTS list)
const TEST_PORT = 6379;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This test is labeled as exercising a "dangerous" port, but TEST_PORT is set to 15432 (not in the dangerous port list). As written it only verifies basic host connectivity, not that dangerous-port blocking is bypassed specifically for host.docker.internal. Consider either using a port that is actually in DANGEROUS_PORTS (when available) or renaming/reframing the test so its assertions match what it covers.

This issue also appears in the following locations of the same file:

  • line 103
  • line 129
Suggested change
// Start a TCP echo server on a dangerous port (Redis 6379, in DANGEROUS_PORTS list)
const TEST_PORT = 6379;
// Start a TCP echo server on a "dangerous" port (PostgreSQL default 5432)
const TEST_PORT = 5432;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The tests already use actual dangerous ports: Redis 6379 (line 75) and PostgreSQL 5432 (line 107). The comment may have been based on a stale diff. The test at line 73-101 explicitly names Redis 6379 and verifies TCP connectivity through the firewall bypass.

src/cli.ts Outdated
Comment on lines +1682 to +1690
// `services:` containers that publish database ports to the host via port mapping.
if (config.allowHostServicePorts) {
// Validate port numbers (must be valid ports, but no DANGEROUS_PORTS check — see above)
const servicePorts = config.allowHostServicePorts.split(',').map(p => p.trim());
for (const port of servicePorts) {
const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
logger.error(`❌ Invalid port in --allow-host-service-ports: ${port}. Must be a number between 1 and 65535`);
process.exit(1);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The --allow-host-service-ports validation uses parseInt(), which will accept non-numeric inputs like "5432abc" (and even "5432-6000" parses as 5432). Those values then get passed through to iptables as --dport and can fail at runtime or behave unexpectedly. Consider validating each entry with a strict numeric regex (e.g. /^\d+$/), rejecting ranges/extra characters, and optionally normalizing back to a comma-separated canonical list.

Suggested change
// `services:` containers that publish database ports to the host via port mapping.
if (config.allowHostServicePorts) {
// Validate port numbers (must be valid ports, but no DANGEROUS_PORTS check — see above)
const servicePorts = config.allowHostServicePorts.split(',').map(p => p.trim());
for (const port of servicePorts) {
const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
logger.error(`❌ Invalid port in --allow-host-service-ports: ${port}. Must be a number between 1 and 65535`);
process.exit(1);
const rawServicePorts = config.allowHostServicePorts.split(',');
const validatedPorts: string[] = [];
for (const rawPort of rawServicePorts) {
const port = rawPort.trim();
// Skip empty entries (e.g., from trailing commas)
if (!port) {
continue;
}
// Require strictly numeric port entries (no ranges or extra characters)
if (!/^\d+$/.test(port)) {
logger.error(
`❌ Invalid port in --allow-host-service-ports: ${port}. Must be a numeric value between 1 and 65535`
);
process.exit(1);
}
const portNum = Number(port);
if (portNum < 1 || portNum > 65535) {
logger.error(
`❌ Invalid port in --allow-host-service-ports: ${port}. Must be a number between 1 and 65535`
);
process.exit(1);
}
// Normalize ports back to canonical numeric strings
validatedPorts.push(String(portNum));
}
// If, after filtering, no valid ports remain, treat as an error
if (validatedPorts.length === 0) {
logger.error('❌ --allow-host-service-ports must specify at least one valid port between 1 and 65535');
process.exit(1);
}
// Normalize config value to a canonical, comma-separated list
config.allowHostServicePorts = validatedPorts.join(',');

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already handled. validateAllowHostServicePorts() checks /^\d+$/.test(port) before parseInt, so inputs like 5432abc or 5432-6000 are rejected. The regex runs first and only strictly numeric strings reach the parseInt/range check.

Comment on lines +223 to +250
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" =~ ^[0-9]+$ ]]; then
echo "[iptables] WARNING: Skipping invalid port: $port"
continue
fi
if [ -n "$port" ]; then
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)
Copy link

Copilot AI Mar 25, 2026

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
Collaborator 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."

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 82.69% 82.40% 📉 -0.29%
Statements 82.35% 82.04% 📉 -0.31%
Functions 81.11% 80.83% 📉 -0.28%
Branches 75.88% 75.53% 📉 -0.35%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 60.5% → 59.2% (-1.28%) 60.9% → 59.5% (-1.38%)
src/docker-manager.ts 86.3% → 86.6% (+0.33%) 85.7% → 86.0% (+0.32%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

Copy link
Copy Markdown
Collaborator Author

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

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

Second-Round Review: --allow-host-service-ports

All six first-round issues have been addressed in commit a37fcb8d:

Issue Status
HSP_PORTS scoping bug (unset for network gateway) Fixed -- IFS=',' read -ra HSP_PORTS moved above both gateway blocks
Redundant NAT RETURN rules Fixed -- removed; comments explain blanket RETURN handles NAT bypass
Test port 15432 (not in DANGEROUS_PORTS) Fixed -- changed to 6379 (Redis), actually in the dangerous list; added try/catch skip
Shell-side port validation Fixed -- ^[0-9]+$ regex check added to both loops
CLI warning about security implications Fixed -- two logger.warn() lines added
Comment about intentional DANGEROUS_PORTS skip Fixed -- explanatory comment added in cli.ts

Minor remaining nit (non-blocking)

The CLI-side validation still uses parseInt(port, 10) which silently accepts inputs like "5432abc" (parsed as 5432). The shell-side ^[0-9]+$ regex catches this as defense-in-depth, so it's not a security issue. For consistency, a strict /^\d+$/ check on the CLI side (as Copilot suggested) would be cleaner, but not required for merge.

CI Status

All completed checks pass. Integration tests are still running -- no failures observed so far.

Verdict

All requested changes are properly addressed. No new issues introduced by the fixes. The implementation is clean and ready to merge once CI completes green.

Copy link
Copy Markdown
Collaborator Author

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

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

Second-Round Security Review: --allow-host-service-ports

Reviewed the full PR diff, the updated containers/agent/setup-iptables.sh, src/cli.ts, src/docker-manager.ts, and src/types.ts on the PR branch.

First-Round Suggestions — All Addressed

Suggestion Status
Shell-side numeric port validation (^[0-9]+$ regex) Addressed — lines 233-236 and 251-254 in setup-iptables.sh
CLI warning about security implications Addressed — logger.warn() at lines 1694-1695 in cli.ts
HSP_PORTS scoping bug (was set inside first if block only) Fixed — IFS=',' read -ra HSP_PORTS now at line 223, before both gateway IP blocks
Test using non-dangerous port 15432 Fixed — tests now use actual dangerous ports (6379, 5432)
Redundant NAT RETURN rules in new block Resolved — new block only adds FILTER ACCEPT rules, with comments explaining that NAT bypass is handled by the blanket RETURN in the host access block above

Rule Ordering Verification

The iptables rule chain is correctly ordered:

  1. Localhost/DNS/Squid/API-proxy RETURN/ACCEPT rules (unchanged)
  2. Host access blanket NAT RETURN + FILTER ACCEPT for ports 80/443 + --allow-host-ports (unchanged)
  3. New: Host service port FILTER ACCEPT rules (lines 216-262) — destination-scoped to gateway IPs only
  4. Dangerous port NAT RETURN rules (lines 264-294) — these cause traffic to fall through to the final DROP
  5. HTTP/HTTPS DNAT to Squid (lines 300-301)
  6. FILTER chain: DNS ACCEPT, Squid ACCEPT, LOG, DROP (lines 329-379)

This ordering is correct: host service port ACCEPT rules are evaluated before the dangerous-port RETURN+DROP, so traffic to host.docker.internal:5432 hits the ACCEPT before it can be dropped. Traffic to external-ip:5432 does not match the destination-scoped ACCEPT and falls through to the DROP.

Destination Scoping Verification

All FILTER ACCEPT rules for host service ports remain properly destination-scoped:

iptables -A OUTPUT -p tcp -d "$HSP_HOST_GW_IP" --dport "$port" -j ACCEPT
iptables -A OUTPUT -p tcp -d "$HSP_NET_GW_IP" --dport "$port" -j ACCEPT

The -d constraint ensures these rules only match traffic to the resolved host gateway IPs. External destinations on dangerous ports still hit the final DROP rule. The integration test should block dangerous port to non-host destinations (internet) validates this.

No New Security Concerns

  • No new injection vectors: CLI validates port range (1-65535), shell validates numeric-only regex, iptables rejects non-numeric --dport values. Three layers of defense.
  • No privilege changes: iptables-init container still the only one with NET_ADMIN; agent still drops SYS_CHROOT and SYS_ADMIN.
  • Auto-enable of --enable-host-access is correctly placed before the validateAllowHostPorts check, so the existing validation logic still runs.
  • The AWF_HOST_SERVICE_PORTS env var is correctly passed to the iptables-init container via docker-manager.ts (line 1222).

Recommendation

Security posture is satisfactory. All first-round issues have been addressed. The feature correctly and narrowly relaxes dangerous-port restrictions for host-gateway-only traffic with proper destination scoping, input validation at multiple layers, and clear warnings. Recommend approval.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 82.69% 82.31% 📉 -0.38%
Statements 82.35% 81.96% 📉 -0.39%
Functions 81.11% 80.83% 📉 -0.28%
Branches 75.88% 75.48% 📉 -0.40%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 60.5% → 58.9% (-1.57%) 60.9% → 59.2% (-1.67%)
src/docker-manager.ts 86.3% → 86.6% (+0.33%) 85.7% → 86.0% (+0.32%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions
Copy link
Copy Markdown
Contributor

Smoke test results (run 23564909311)

✅ GitHub MCP — #1414 fix: write apiKeyHelper to ~/.claude/settings.json for Claude Code v2.1.81+; #1350 docs: Fix proxy env var docs and add missing CLI flags
✅ Playwright — github.com title contains "GitHub"
✅ File write — /tmp/gh-aw/agent/smoke-test-claude-23564909311.txt created
✅ Bash verify — file contents confirmed

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results — PASS ✅

Test Result
GitHub MCP (last 2 merged PRs) #1420 "fix: restore GITHUB_API_URL..." / #1419 "fix: exclude GITHUB_API_URL..."
Playwright (github.com title) ✅ Title contains "GitHub"
File write /tmp/gh-aw/agent/smoke-test-copilot-23564909334.txt ✅ Created & verified
Bash cat verification ✅ File read back successfully

Overall: PASS@Mossaka (no assignees)

📰 BREAKING: Report filed by Smoke Copilot for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1436

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 82.69% 82.52% 📉 -0.17%
Statements 82.35% 82.20% 📉 -0.15%
Functions 81.11% 81.25% 📈 +0.14%
Branches 75.88% 75.78% 📉 -0.10%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 60.5% → 60.1% (-0.37%) 60.9% → 60.6% (-0.32%)
src/docker-manager.ts 86.3% → 86.6% (+0.33%) 85.7% → 86.0% (+0.32%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

✅ GitHub MCP — Last 2 merged PRs:

✅ Playwright — github.com title contains "GitHub"
✅ File write — /tmp/gh-aw/agent/smoke-test-copilot-23565351730.txt created and verified
✅ Bash — file content confirmed via cat

Overall: PASS | PR by @Mossaka, assignees: @Mossaka

📰 BREAKING: Report filed by Smoke Copilot for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1436

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

Documentation Preview

Documentation build failed for this PR. View logs.

Built from commit 5934456

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.77% 82.77% ➡️ +0.00%
Statements 82.41% 82.42% 📈 +0.01%
Functions 81.25% 81.44% 📈 +0.19%
Branches 75.99% 76.03% 📈 +0.04%
📁 Per-file Coverage Changes (3 files)
File Lines (Before → After) Statements (Before → After)
src/host-iptables.ts 92.4% → 91.9% (-0.57%) 92.1% → 91.6% (-0.55%)
src/docker-manager.ts 85.8% → 85.8% (+0.08%) 85.2% → 85.3% (+0.08%)
src/cli.ts 60.5% → 61.4% (+0.98%) 60.9% → 61.9% (+0.99%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

🚬 Smoke Test Results

Overall: PASS | Author: @Mossaka | No assignees

📰 BREAKING: Report filed by Smoke Copilot for issue #1436

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall: ❌ NOT ALL TESTS PASSED — Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1436

@github-actions

This comment has been minimized.

@Mossaka Mossaka force-pushed the fix/allow-host-service-ports branch from 7b97939 to c1fdc26 Compare March 25, 2026 22:46
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Smoke Test Results

GitHub MCP: #1435 "fix: allow host gateway traffic for localhost/Playwright" (@Mossaka), #1432 "Pre-install commonly needed system packages in agent container image" (@Copilot)
Playwright: github.com title contains "GitHub"
File Write: /tmp/gh-aw/agent/smoke-test-copilot-23568015741.txt created and verified
Bash Tool: File read back successfully

Overall: PASS

PR author: @Mossaka

📰 BREAKING: Report filed by Smoke Copilot for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results — PASS

✅ GitHub MCP: #1435 fix: allow host gateway traffic for localhost/Playwright, #1432 Pre-install commonly needed system packages in agent container image
✅ Playwright: github.com title contains "GitHub"
✅ File write/read: /tmp/gh-aw/agent/smoke-test-claude-23568015725.txt created and verified

💥 [THE END] — Illustrated by Smoke Claude for issue #1436

@Mossaka Mossaka force-pushed the fix/allow-host-service-ports branch from c1fdc26 to 75166eb Compare March 25, 2026 22:52
Adds --allow-host-service-ports CLI flag that allows TCP connections to
the host gateway IP on ports normally blocked as dangerous (e.g.,
PostgreSQL 5432, Redis 6379). Designed for GitHub Actions services:
containers that publish to the host — the agent can reach these
services but still cannot reach databases on the internet.

Changes:
- New CLI flag with strict port validation (single numeric ports 1-65535)
- Host-level FW_WRAPPER ACCEPT rules for service ports on gateway IPs
- Container-level iptables ACCEPT rules for service ports
- Auto-enables --enable-host-access with explicit warning about 80/443
- Safety net in docker-manager: enables host access if not already set
- SSH risk documented in help text
- Bash validation uses single-port regex (no ranges), aligned with TS
- Integration tests with actual dangerous ports (5432, 6379)

Security: Service port traffic is restricted to host gateway IPs only
(172.17.0.1 and 172.30.0.1). Dangerous port restrictions are bypassed
intentionally since traffic cannot reach the internet. Both host-level
(FW_WRAPPER) and container-level iptables enforce the restriction.

Closes gh-aw#22939

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Mossaka Mossaka force-pushed the fix/allow-host-service-ports branch from 75166eb to d4dd7d7 Compare March 25, 2026 22:54
@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

✅ GitHub MCP — Last 2 merged PRs:

✅ Playwright — github.com title contains "GitHub"
✅ File write — /tmp/gh-aw/agent/smoke-test-copilot-23568297618.txt created and verified
✅ Bash — file read back successfully

Overall: PASS | Author: @Mossaka | No assignees

📰 BREAKING: Report filed by Smoke Copilot for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ No
Node.js v24.14.0 v20.20.1 ❌ No
Go go1.22.12 go1.22.12 ✅ Yes

Warning

Not all versions match. Python and Node.js versions differ between host and chroot. Go versions match.

Tested by Smoke Chroot for issue #1436

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia 1/1 passed ✅ PASS
Bun hono 1/1 passed ✅ PASS
C++ fmt N/A ✅ PASS
C++ json N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world N/A ✅ PASS
.NET json-parse N/A ✅ PASS
Go color 1/1 passed ✅ PASS
Go env 1/1 passed ✅ PASS
Go uuid 1/1 passed ✅ PASS
Java gson 1/1 passed ✅ PASS
Java caffeine 1/1 passed ✅ PASS
Node.js clsx All passed ✅ PASS
Node.js execa All passed ✅ PASS
Node.js p-limit All passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Generated by Build Test Suite for issue #1436 ·

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

PR titles: fix: allow host service ports for GitHub Actions services containers; fix: allow host gateway traffic for localhost/Playwright

  1. GitHub MCP last 2 merged PRs: ✅
  2. safeinputs-gh PR query: ❌ (tool unavailable; used GitHub MCP fallback)
  3. Playwright title contains "GitHub": ✅
  4. Tavily web search: ❌ (tool unavailable)
  5. File write + 6) cat verify: ✅
  6. Discussion query/comment: ❌ (tool unavailable)
  7. npm ci && npm run build: ✅
    Overall status: FAIL

🔮 The oracle has spoken through Smoke Codex

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • ab.chatgpt.com
  • registry.npmjs.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"
    - "registry.npmjs.org"

See Network Configuration for more information.

@Mossaka Mossaka merged commit 284e5f9 into main Mar 26, 2026
59 checks passed
@Mossaka Mossaka deleted the fix/allow-host-service-ports branch March 26, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants