Skip to content

BUG: services: ports are unreachable from the sandboxed agent — --allow-host-ports is hardcoded and allowHostServicePorts is never emitted #51433

Description

@sg650

Follow-up to #22939, which reported that an AWF-sandboxed agent cannot reach a database started via GitHub Actions services:. That issue was closed by github/gh-aw-firewall#1436, which added the AWF-side capability. The compiler side was never wired up, and a subsequent change to strict-security-by-default removed the only path that emitted host-access flags at all. The result is that services: is silently unusable from the agent loop today.

Reproduction

Any workflow that declares a service and expects the agent to use it:

runs-on: ubuntu-latest
engine:
  id: copilot
services:
  postgres:
    image: postgres:18
    ports:
      - 5432:5432
    env:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
  • Expected: the agent can connect to the service on the published host port.
  • Actual: every address fails from inside the sandbox — host.docker.internal:5432 times out, localhost:5432 and 127.0.0.1:5432 are refused.

The failure is silent and expensive. The service starts normally, the port publishes normally, and ordinary (non-sandboxed) job steps reach it fine — so DB setup steps in the job succeed and nothing looks wrong. Only the agent is blocked. In practice a capable agent will not conclude "the port is firewalled"; it concludes the database is missing and starts building one, e.g.:

No postgres data cluster present. Try starting one.
$ initdb -D /tmp/.../pgdata -U app --auth=trust
$ pg_ctl -D /tmp/.../pgdata start
$ apt-cache search <extension>
Sandbox has no apt sources configured; can't install it.
$ git clone --branch REL_16_STABLE https://github.com/postgres/postgres.git
$ make -C src/backend generated-headers

That is a full timeout budget spent compiling a database from source, next to a running service container that already had everything. Any workflow whose agent runs tests against a service is affected.

Analysis

1. Host-access flags are emitted only in legacy security mode

pkg/workflow/awf_command_builder.go:

isLegacy := agentConfig != nil && agentConfig.LegacySecurity
if isLegacy {
    ...
    awfArgs = append(awfArgs, "--enable-host-access")
    if awfSupportsAllowHostPorts(firewallConfig) {
        mcpGatewayPort := int(DefaultMCPGatewayPort)
        ...
        hostPorts := fmt.Sprintf("80,443,%d", mcpGatewayPort)
        awfArgs = append(awfArgs, "--allow-host-ports", hostPorts)
    }
} else {
    awfHelpersLog.Print("Strict security: skipping host-access flags (default)")
}

Since strict security became the default, workflows that do not set sandbox.agent.legacy-security: enable get no host-access flags. This regressed silently on upgrade — it is compiler output, so a routine gh aw compile after a version bump drops the flags with no warning and no diagnostic. Observed when upgrading through v0.82.14.

2. The port list is hardcoded, so even legacy mode does not help

hostPorts is always 80,443,<mcp-gateway-port>. There is no input that can add 5432, 6379, or any other service port. Verifiable without running anything: add sandbox.agent.legacy-security: enable to a workflow that declares a services: postgres on 5432, recompile, and the emitted flag is exactly --allow-host-ports 80,443,8080 — the declared service port is absent.

Confirmed at runtime as well. With all three flags active (--legacy-security --enable-host-access --allow-host-ports 80,443,8080), the agent still could not reach the service and fell back to building its own local cluster under ${RUNNER_TEMP}/gh-aw/agent/pgdata. --enable-host-access is therefore not sufficient on its own — the port allowlist is the operative control, and legacy security mode is not a viable workaround for services: access. Opting into legacy-security therefore restores broad host access and still leaves every service port blocked — the worst of both trade-offs: weaker sandbox, same failure.

3. The field designed for this exists in the schema but has no implementation

pkg/workflow/schemas/awf-config.schema.json defines both:

"allowHostPorts": {
  "description": "Host TCP ports the agent may connect to (e.g. local dev services)."
},
"allowHostServicePorts": {
  "description": "Named service ports on the host that the agent may connect to."
}

A repo-wide search for allowHostServicePorts returns exactly one hit: that schema file. No Go code reads or emits it. allowHostPorts appears in several files but only via the hardcoded CLI flag above; gh-aw does not write a security block into the generated awf-config.json at all.

So AWF can accept a service-port allowlist, and gh-aw never sends one. That is the gap between #22939's fix and observed behavior.

Proposed fix

Derive the allowlist from the services: block the workflow already declares, and emit it under strict security. The user has declared the service and its ports explicitly; honoring exactly those ports is a narrow, auditable grant that does not require the broad host access of legacy mode.

Two mechanisms, in priority order:

  • Primary (automatic): parse the ports: mappings of each entry under services: and pass the host-side ports through. Zero new syntax; existing workflows start working on recompile.
  • Secondary (explicit escape hatch): a frontmatter field for ports not expressible via services: (e.g. a daemon started by a prior job step).
sandbox:
  agent:
    allow-host-ports: [5432, 6379]

Both should work without legacy-security.

Implementation plan

1. Schema — pkg/parser/schemas/main_workflow_schema.json

Add allow-host-ports alongside legacy-security (same object: /properties/sandbox/.../properties/agent/...):

"allow-host-ports": {
  "type": "array",
  "items": { "type": "integer", "minimum": 1, "maximum": 65535 },
  "uniqueItems": true,
  "description": "Additional host TCP ports the agent may connect to. Ports published by `services:` are allowed automatically; use this only for ports not declared there."
}

2. Parser — wherever LegacySecurity is populated into the agent config

Add AllowHostPorts []int. Validate: reject values outside 1–65535 and reject any port in the AWF dangerousPorts set, following the repo error template [what's wrong]. [what's expected]. [example]:

return fmt.Errorf("invalid allow-host-ports value: %d. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [5432]", port)

3. Service-port derivation — new helper in pkg/workflow

collectServiceHostPorts(workflowData *WorkflowData) []int — walk services:, parse each ports: entry, take the host side of "HOST:CONTAINER" (and the sole port for a bare "5432"). Handle the "127.0.0.1:5432:5432" form by taking the middle field. Ignore /udp suffixes (AWF allows TCP only). Deduplicate against the explicit list.

4. Emission — pkg/workflow/awf_command_builder.go

Move --allow-host-ports out of the isLegacy branch. Build the union of {80, 443, mcpGatewayPort} (existing behavior), derived service ports, and explicit allow-host-ports; sort and dedupe for deterministic lock files. Keep the awfSupportsAllowHostPorts version gate (AWFAllowHostPortsMinVersion, currently v0.25.24) so older pinned AWF versions still do not receive the flag. When the version gate fails but ports were requested, emit a compile warning naming the minimum version — silent omission is what made this hard to spot.

Note --enable-host-access should stay legacy-only. Allowing a declared port list is a narrower grant and should not imply full host access — and the runtime evidence above shows host access without the port is useless anyway, so the port list is the control that matters. If AWF turns out to require the two together, that coupling is worth stating explicitly in the AWF docs rather than leaving callers to infer it.

5. Tests — pkg/workflow/awf_command_builder_test.go

  • strict security + a services: block with 5432:5432 → args contain --allow-host-ports including 5432, and do not contain --enable-host-access
  • strict security, no services, no explicit ports → unchanged from today (no regression for workflows that need nothing)
  • explicit allow-host-ports: [9200] merges with derived service ports, sorted and deduped
  • port-mapping parse table: "5432:5432", "5432", "127.0.0.1:5432:5432", "5432:5432/udp"
  • AWF version below AWFAllowHostPortsMinVersion with ports requested → flag omitted and a warning emitted
  • legacy security + services → still emits --enable-host-access, port list now includes the service ports

6. Docs

Document in the sandbox/security page that services: ports are reachable from the agent, and note the behavior change for anyone who previously worked around this. A short note in the upgrade guide would help others who hit the silent regression.

Workaround for anyone finding this issue

There is currently no configuration that makes a services: port reachable from the agent. legacy-security: enable restores host access but not the port, and has been confirmed at runtime not to help. The only options are to run the dependency inside the agent's own environment, or to move the work that needs it into a non-sandboxed job step.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions