Container runner: wildcard-bound host services reachable from containers via host.containers.internal #216

Closed
opened 2026-07-23 13:50:56 +02:00 by dominik.polakovics · 1 comment

Summary

Found while validating PR #214 (containerized runner, ADR-0052). ADR-0052 states pasta gives "full egress with no route back to the host's loopback services ... never a host port." That is true only for services bound to the host loopback (127.0.0.1). A host service bound to a wildcard/LAN address (0.0.0.0) remains reachable from inside a container via host.containers.internal, which podman's --network=pasta maps by default.

This is not a break of the loopback isolation the ADR relies on, and not a merge blocker — filing so we investigate and decide on hardening later.

Why it matters here

lab's own HTTP server runs with --addr :8080 (a wildcard bind — confirmed on the dev host). So a containerized run can reach lab's operator HTTP port at host.containers.internal:8080, and likewise any other 0.0.0.0-bound service on the host (a database, metrics endpoint, another daemon).

Mitigating factors (why it's not urgent):

  • lab's agent API (/agent/v1) is served on the unix socket only, never on :8080.
  • The operator API on :8080 requires a session cookie; a container has no cookie and the run token (LAB_TOKEN) only authenticates the agent socket. So reaching :8080 is low-value.

But the surface is wider than the ADR claims, and it depends on what else the operator runs on 0.0.0.0.

Evidence (reproduced with podman's faithful default pasta args)

Faithful arg set: --config-net --dns-forward 169.254.1.1 -t none -u none -T none -U none --no-map-gw --map-guest-addr 169.254.1.2. From inside the netns, against host services:

Probe Bound to Result
127.0.0.1:PORT (guest loopback) host 127.0.0.1 no answer
<gateway>:PORT host 127.0.0.1 no answer (--no-map-gw)
host.containers.internal:PORT host 127.0.0.1 no answer
host.containers.internal:PORT host 0.0.0.0 REACHED

Mechanism: podman always appends --map-guest-addr 169.254.1.2 for host.containers.internal; per the pasta(1) man page it maps to the host's global address (not loopback). --no-map-gw (podman default) is what blocks the loopback path.

Possible mitigations to evaluate

  1. Add --map-guest-addr none to the pane argv (internal/podmanx/podmanx.go RunArgv) to drop the host.containers.internal mapping entirely — the container has no need for it (it reaches lab via the mounted unix socket).
  2. Bind lab to 127.0.0.1 on hosts where the container runner is enabled (and document that co-located services should too), relying on the reverse proxy for external access.
  3. Document the residual reachability explicitly in ADR-0052 / ops.md (partially done in the PR's ADR caveat) and treat wildcard-bound host services as in-scope for the container threat model.

Notes

  • Verified against podman main/v5.8.x vendored libnetwork/pasta and the pasta(1) man page; reproduced with nixpkgs passt on the dev host (rootless podman is not yet installed there).
  • Related: PR #214 / ADR-0052 §pasta bullet (now carries a short caveat).
## Summary Found while validating PR #214 (containerized runner, ADR-0052). ADR-0052 states pasta gives *"full egress with no route back to the host's loopback services ... never a host port."* That is **true only for services bound to the host loopback (`127.0.0.1`)**. A host service bound to a **wildcard/LAN address (`0.0.0.0`)** remains reachable from inside a container via `host.containers.internal`, which podman's `--network=pasta` maps by default. This is **not** a break of the loopback isolation the ADR relies on, and **not a merge blocker** — filing so we investigate and decide on hardening later. ## Why it matters here lab's own HTTP server runs with `--addr :8080` (a wildcard bind — confirmed on the dev host). So a containerized run **can** reach lab's operator HTTP port at `host.containers.internal:8080`, and likewise any *other* `0.0.0.0`-bound service on the host (a database, metrics endpoint, another daemon). Mitigating factors (why it's not urgent): - lab's agent API (`/agent/v1`) is served on the unix socket only, never on `:8080`. - The operator API on `:8080` requires a session cookie; a container has no cookie and the run token (`LAB_TOKEN`) only authenticates the agent socket. So reaching `:8080` is low-value. But the surface is wider than the ADR claims, and it depends on what else the operator runs on `0.0.0.0`. ## Evidence (reproduced with podman's faithful default pasta args) Faithful arg set: `--config-net --dns-forward 169.254.1.1 -t none -u none -T none -U none --no-map-gw --map-guest-addr 169.254.1.2`. From inside the netns, against host services: | Probe | Bound to | Result | |---|---|---| | `127.0.0.1:PORT` (guest loopback) | host `127.0.0.1` | no answer | | `<gateway>:PORT` | host `127.0.0.1` | no answer (`--no-map-gw`) | | `host.containers.internal:PORT` | host `127.0.0.1` | no answer | | `host.containers.internal:PORT` | host `0.0.0.0` | **REACHED** | Mechanism: podman always appends `--map-guest-addr 169.254.1.2` for `host.containers.internal`; per the pasta(1) man page it maps to the host's *global* address (not loopback). `--no-map-gw` (podman default) is what blocks the loopback path. ## Possible mitigations to evaluate 1. Add `--map-guest-addr none` to the pane argv (`internal/podmanx/podmanx.go` `RunArgv`) to drop the `host.containers.internal` mapping entirely — the container has no need for it (it reaches lab via the mounted unix socket). 2. Bind lab to `127.0.0.1` on hosts where the container runner is enabled (and document that co-located services should too), relying on the reverse proxy for external access. 3. Document the residual reachability explicitly in ADR-0052 / ops.md (partially done in the PR's ADR caveat) and treat wildcard-bound host services as in-scope for the container threat model. ## Notes - Verified against podman `main`/v5.8.x vendored `libnetwork/pasta` and the pasta(1) man page; reproduced with nixpkgs `passt` on the dev host (rootless podman is not yet installed there). - Related: PR #214 / ADR-0052 §pasta bullet (now carries a short caveat).
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: bug
Summary: Drop pasta's host.containers.internal mapping from containerized runs so wildcard-bound host services are no longer reachable from inside a container

Current behavior:
The containerized-run argv renderer (RunArgv in the podmanx package) passes bare --network=pasta. Podman's pasta defaults then append --map-guest-addr 169.254.1.2, which maps host.containers.internal to the host's global address. Result: any host service bound to a wildcard/LAN address (0.0.0.0) — including lab's own operator HTTP server started with --addr :8080 — is reachable from inside every run container via host.containers.internal:<port>. Only loopback-bound host services are isolated (via podman's default --no-map-gw). This contradicts ADR-0052's isolation intent ("no route back to the host's services … never a host port"); the ADR and the flag-rationale comment currently carry a caveat documenting the hole.

Reproduction evidence is in the issue body: with podman's faithful default pasta args, a probe from inside the netns reaches a 0.0.0.0-bound host service via host.containers.internal while all loopback paths correctly fail.

Desired behavior:
Containerized runs get no host.containers.internal mapping at all. Nothing inside the container needs it: the agent reaches lab exclusively through the mounted agent unix socket, and a TCP --agent-url is already deliberately unreachable per ADR-0052. The intended mechanism is passing the pasta option --map-guest-addr none through podman's network option syntax (--network=pasta:--map-guest-addr,none) — verify against the pinned podman's docs that user-supplied pasta options compose with (not replace) podman's other defaults (--no-map-gw, -t none, etc.), and that none suppresses the mapping rather than binding a literal address.

Version floor: --map-guest-addr is a newer passt option (podman started appending it in 5.3, matching passt releases from late 2024). On a host with an older passt, the explicit option would make pasta fail at container start — after the preflight has already passed. Handle this explicitly: extend the podmanx preflight (which already checks podman ≥ 4 and pasta presence) to verify the installed pasta accepts --map-guest-addr, refusing container spawns with an actionable failure naming the too-old passt otherwise. If a cheap probe proves impractical, a documented version floor enforced by the preflight's existing version checks is acceptable — but silent spawn breakage on old hosts is not.

All three run shapes rendered by RunArgv (run session, login pane, non-interactive CLI poke) share the same network flag and should all lose the mapping — none of them talks to host ports.

Key interfaces:

  • RunArgv / RunSpec (podmanx package) — the single renderer of the podman run argv; the network flag changes here, for all shapes
  • The podmanx preflight and its failure catalog — gains (or extends a check with) the passt capability/version verification, following the existing collect-all-failures-with-operator-action pattern
  • The --network=pasta flag-rationale doc comment and ADR-0052's pasta bullet — both currently document the residual reachability as a caveat; both must be updated to state the mapping is dropped and why (the ops guidance to bind co-located services to 127.0.0.1 remains sound as defense-in-depth)

Acceptance criteria:

  • The rendered container argv requests pasta with --map-guest-addr none (exact spelling per podman's network-option syntax) for every shape that uses the pasta network
  • Existing argv-shape tests (podmanx, instance container wiring, provider-login and CLI-poke shapes) are updated and pass
  • Preflight refuses container spawns on a host whose passt does not support --map-guest-addr, with a failure message naming the fix (upgrade passt), following the existing preflight failure conventions — or, if probing is impractical, the preflight version gate is tightened and the floor documented
  • ADR-0052's caveat and the in-code flag rationale are rewritten to reflect the closed hole
  • A live spawn on the dev host still starts and the agent still reaches lab through the unix socket (no regression from the pasta option; verified via the normal test/CI path or a documented manual check if a live container is unavailable in CI)

Out of scope:

  • Changing lab's own --addr bind or any co-located service's bind address — deployment/ops concern; the ADR's advice to bind co-located services to loopback stays as written
  • Any other pasta/network hardening (port-forward policy, egress filtering)
  • The host runner — it has no container boundary; this issue is container-runner only
> *This was generated by AI during triage.* ## Agent Brief **Category:** bug **Summary:** Drop pasta's `host.containers.internal` mapping from containerized runs so wildcard-bound host services are no longer reachable from inside a container **Current behavior:** The containerized-run argv renderer (`RunArgv` in the podmanx package) passes bare `--network=pasta`. Podman's pasta defaults then append `--map-guest-addr 169.254.1.2`, which maps `host.containers.internal` to the host's *global* address. Result: any host service bound to a wildcard/LAN address (`0.0.0.0`) — including lab's own operator HTTP server started with `--addr :8080` — is reachable from inside every run container via `host.containers.internal:<port>`. Only loopback-bound host services are isolated (via podman's default `--no-map-gw`). This contradicts ADR-0052's isolation intent ("no route back to the host's services … never a host port"); the ADR and the flag-rationale comment currently carry a caveat documenting the hole. Reproduction evidence is in the issue body: with podman's faithful default pasta args, a probe from inside the netns reaches a `0.0.0.0`-bound host service via `host.containers.internal` while all loopback paths correctly fail. **Desired behavior:** Containerized runs get no `host.containers.internal` mapping at all. Nothing inside the container needs it: the agent reaches lab exclusively through the mounted agent unix socket, and a TCP `--agent-url` is already deliberately unreachable per ADR-0052. The intended mechanism is passing the pasta option `--map-guest-addr none` through podman's network option syntax (`--network=pasta:--map-guest-addr,none`) — verify against the pinned podman's docs that user-supplied pasta options compose with (not replace) podman's other defaults (`--no-map-gw`, `-t none`, etc.), and that `none` suppresses the mapping rather than binding a literal address. Version floor: `--map-guest-addr` is a newer passt option (podman started appending it in 5.3, matching passt releases from late 2024). On a host with an older passt, the explicit option would make pasta fail at container start — after the preflight has already passed. Handle this explicitly: extend the podmanx preflight (which already checks podman ≥ 4 and pasta presence) to verify the installed pasta accepts `--map-guest-addr`, refusing container spawns with an actionable failure naming the too-old passt otherwise. If a cheap probe proves impractical, a documented version floor enforced by the preflight's existing version checks is acceptable — but silent spawn breakage on old hosts is not. All three run shapes rendered by `RunArgv` (run session, login pane, non-interactive CLI poke) share the same network flag and should all lose the mapping — none of them talks to host ports. **Key interfaces:** - `RunArgv` / `RunSpec` (podmanx package) — the single renderer of the `podman run` argv; the network flag changes here, for all shapes - The podmanx preflight and its failure catalog — gains (or extends a check with) the passt capability/version verification, following the existing collect-all-failures-with-operator-action pattern - The `--network=pasta` flag-rationale doc comment and ADR-0052's pasta bullet — both currently document the residual reachability as a caveat; both must be updated to state the mapping is dropped and why (the ops guidance to bind co-located services to `127.0.0.1` remains sound as defense-in-depth) **Acceptance criteria:** - [ ] The rendered container argv requests pasta with `--map-guest-addr none` (exact spelling per podman's network-option syntax) for every shape that uses the pasta network - [ ] Existing argv-shape tests (podmanx, instance container wiring, provider-login and CLI-poke shapes) are updated and pass - [ ] Preflight refuses container spawns on a host whose passt does not support `--map-guest-addr`, with a failure message naming the fix (upgrade passt), following the existing preflight failure conventions — or, if probing is impractical, the preflight version gate is tightened and the floor documented - [ ] ADR-0052's caveat and the in-code flag rationale are rewritten to reflect the closed hole - [ ] A live spawn on the dev host still starts and the agent still reaches lab through the unix socket (no regression from the pasta option; verified via the normal test/CI path or a documented manual check if a live container is unavailable in CI) **Out of scope:** - Changing lab's own `--addr` bind or any co-located service's bind address — deployment/ops concern; the ADR's advice to bind co-located services to loopback stays as written - Any other pasta/network hardening (port-forward policy, egress filtering) - The host runner — it has no container boundary; this issue is container-runner only
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#216
No description provided.