Skip to content

fix: make NetworkingConfig endpoints match a forced network_mode (#87) - #89

Merged
abienkowski merged 4 commits into
mainfrom
fix/network-endpoints
Oct 14, 2026
Merged

abienkowski merged 4 commits into
mainfrom
fix/network-endpoints

Conversation

@abienkowski

Copy link
Copy Markdown
Collaborator

Description

Closes #87.

When a policy sets container_config.network_mode, containers that the Docker CLI creates through the proxy do not start. The mutator overwrote HostConfig.NetworkMode but forwarded NetworkingConfig.EndpointsConfig as sent. Without --network the CLI sends {"default": {}}, so the daemon accepted the create and the start failed with failed to set up container networking: network default not found. On Engine 29.8.2 this happens with host, none and bridge alike, on API 1.41 to 1.56. An extra endpoint entry was also forwarded, so a caller could attach the container to a network the policy does not allow.

What changes

  • All three implementations, only when the policy forces network_mode M:
    • A new gate, NetworkEndpointsGate, denies an EndpointsConfig key other than default and M with network "<key>" is not allowed for service <S>; policy forces network_mode <M>. The gate denies the entry and does not drop it, because the caller asked for a network the policy does not allow. A NetworkingConfig or EndpointsConfig that is not a JSON object is also denied (NetworkingConfig.EndpointsConfig must be a JSON object; ...).
    • ContainerConfigMutator sets EndpointsConfig to {} for host, none and container:<id>. For bridge or a named network, it sets exactly one entry keyed M. That entry keeps the settings (for example, aliases) of the caller's M entry. If there is no M entry, it keeps the settings of the default entry, and otherwise it is {}. NetworkingConfig stays an object with an empty EndpointsConfig, which the daemon accepts on API 1.41 to 1.56 (checked directly against the socket).
    • Gates run after mutators, so the mutator leaves a request that the gate will deny unchanged, and the gate sees what the caller sent. This keeps the existing gate/mutator split: the mutator cannot fail, and the gate does the validation.
    • Without a forced network_mode, NetworkingConfig is forwarded untouched (out of scope).
  • Spec (spec/docker_socket_policy.qnt): a new module network_endpoints_model that sits beside the state machine. It has the flag ENFORCE_ENDPOINTS, the rule extraNetworkDenied, the security property onlyForcedNetwork and the bug property allowedCreatesStart (checked against daemonStarts, which is the daemon's start check as observed). The properties range over 5 modes × 2187 endpoint maps. There is one run per table row (21 rows). network_endpoints_pre87 witnesses the gap. make test-spec runs both instances. spec/README.md documents the boundary: alwaysHostNetwork covers only HostConfig.NetworkMode, and the endpoint rule is a pure function that quint test checks. make verify does not check it.
  • Tests: the same 26-row table in Go, Rust and TS, through Chain.Execute. It has the 21 spec rows, null and malformed NetworkingConfig, and a no-forced-mode control. In deploy/test.sh, there are 9 integration checks. They pull hello-world and registry.k8s.io/pause through the proxy, then create containers the way the CLI does (EndpointsConfig {"default": {}}) and start them under two new deploy/config policies (net-host forces host, net-none forces none). Then they remove the containers. One more check confirms that an extra endpoint is denied.
  • Docs: README middleware table and prose, the formal-verification note and the counts. Also spec/README.md and the AGENTS.md counts.

Review notes

  • Release: a fix: gives a patch bump. Simulated squash: bump=patch tag=v0.3.12. A create that sends an extra endpoint under a forced mode is now denied where it used to be allowed. That is the security fix, and the issue is not labelled Status: Break Change.
  • Integration policies: deploy/config/beacon.yaml already forces host, but its images (chainsafe/lodestar) are large and are not on the CI runner. A start needs a real image, so I added two small policies with their own image prefixes instead. Each policy uses a different prefix, so they do not overlap under policy selection by image. Each container is named after its service. The pull sends fromImage in both the body and the query string, so it works both before and after the docker pull through the proxy is always denied: fromImage is read from the body, but the Engine API sends it in the query string #83 change.
  • Network access: the integration run now needs to pull from Docker Hub and registry.k8s.io (about 10 KB and 300 KB). The pulls are their own checks, so a failure shows up as a pull failure.
  • The deny message quotes the key the same way the flag gate does. If there are several extra keys, the gate reports the first in sorted order, the same in all three implementations.

Verification

  • Spec: network_endpoints passes endpointsSoundTest and all 21 rows. network_endpoints_pre87 asserts that all three properties fail, and that {"default": {}} under host is forwarded and fails daemonStarts.
  • Before the fix (33af9bd), 23 of the 26 unit rows fail in Go, Rust and TS. The rows that still pass are bridgeOwnKept, namedOwnKept and the no-forced-mode control. These are the new integration checks on the pre-fix Go code:
    FAIL: start net-host -> 204 (network_mode host) (expected 204, got 404)
          start body: {"message":"failed to set up container networking: network default not found"}
    FAIL: start net-none -> 204 (network_mode none) (expected 204, got 404)
          start body: {"message":"failed to set up container networking: network default not found"}
    FAIL: create with an extra endpoint under network_mode host -> 403 (expected 403 with "network "bridge" is not allowed for service net-host; policy forces network_mode host", got 201: {"Id":"25301c72…","Warnings":[]})
    69 PASSED, 3 FAILED
    
  • This branch: make test-all gives Go 114, Rust 146 and TS 165 (1 skipped). make lint-all, make build-all, make typecheck, make test-spec (12 runs), make verify BACKEND=typescript and make test-release pass. Integration gives ALL 72 TESTS PASSED for Go, Rust and TS. I did not run the socket suite.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Implementation(s) changed

  • Go
  • Rust
  • TypeScript
  • Quint specification
  • CI / infrastructure

Testing

  • Unit tests pass (make test-all)
  • Integration tests pass (72 per implementation)
  • Quint verification passes (make verify, make test-spec)
  • New tests added for the change

Checklist

  • I have read CONTRIBUTING.md
  • My code follows the project's coding style
  • I have updated documentation as needed

🤖 Generated with Claude Code

abienkowski and others added 4 commits October 14, 2026 08:38
Add network_endpoints_model to the request-handling spec: what the gate
and ContainerConfigMutator do with NetworkingConfig.EndpointsConfig when
the policy forces network_mode, and the daemon's start check. The flag
ENFORCE_ENDPOINTS, the rule extraNetworkDenied, the security property
onlyForcedNetwork, the bug property allowedCreatesStart, one run per
table row, and network_endpoints_pre87, which witnesses the CLI's
{"default": {}} being forwarded under host and failing the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same 26-row table in Go, Rust and TS: the 21 rows of the
network_endpoints spec module (host, none, container:x, bridge and a
named network, each with no NetworkingConfig, the CLI's default entry,
the mode's own entry with aliases, and an extra entry that is denied),
null and malformed NetworkingConfig, and a control row without a forced
mode. 23 rows fail on the pre-fix code in all three.

deploy/test.sh pulls two small images through the proxy and creates
and starts real containers the way the Docker CLI does, with
EndpointsConfig {"default": {}}, under two new deploy/config policies
that force host and none; then it removes them. Before the fix both
starts fail with "network default not found" and an extra endpoint is
created instead of denied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a policy sets container_config.network_mode, ContainerConfigMutator
overwrote HostConfig.NetworkMode but forwarded
NetworkingConfig.EndpointsConfig as sent. Without --network the Docker
CLI sends {"default": {}}, so the create succeeded and the start failed
with "network default not found".

With a forced mode M, in Go, Rust and TS:
- NetworkEndpointsGate denies an endpoint key other than "default" and
  M ("network X is not allowed for service S; policy forces
  network_mode M"), and a NetworkingConfig or EndpointsConfig that is
  not a JSON object. An extra entry would attach the container to a
  network the policy does not allow, so it is denied, not dropped.
- ContainerConfigMutator sets EndpointsConfig to {} for host, none and
  container:<id>, and to exactly one entry keyed M otherwise, keeping
  the settings of the caller's M entry, else its "default" entry. It
  leaves a request the gate denies unchanged, so the gate, which runs
  after it, sees what the caller sent.

Without a forced network_mode, NetworkingConfig is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#87)

README middleware table and prose, formal-verification note and counts; AGENTS.md architecture line, test-spec instances and test counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abienkowski abienkowski added the Type: Bug Added to issues and PRs if they are addressing a bug label Oct 14, 2026
@abienkowski
abienkowski merged commit 4536f9c into main Oct 14, 2026
7 checks passed
@abienkowski
abienkowski deleted the fix/network-endpoints branch October 14, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug Added to issues and PRs if they are addressing a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Containers created with a policy network_mode do not start: NetworkingConfig still names the "default" network

1 participant