Repository navigation
feat!: select the create policy by container name and limit lifecycle actions to covered containers (#85, #86) - #90
Merged
Conversation
…#85, #86) docker_socket_policy.qnt: Policy gets containerNames (empty = unset). createContainer applies the one policy that selectPolicies returns for the image and name, and is disabled for zero or several. New P0 invariant policySelectionUnique and run tests: pre85AmbiguousTest (without containerNames both lodestar policies match), createSelectsByNameTest, createUnknownNameDeniedTest and createNamelessDeniedTest. Selecting from every image match instead fails the last two. router.qnt: flag ALLOW_UNCOVERED for the behaviour before #86. The lifecycle branch allows a covered name (allowKnown) and sends any other target to the daemon lookup (resolve); handle completes it with the table DAEMON_CONTAINERS (names and IDs to container names). Security property lifecycleOnlyCovered: no lifecycle action or DELETE is forwarded for an existing container outside every policy's covered names, by name or ID. realNameDeleteAllowed, execPrefixNameStartAllowed and execPrefixNameDeleteAllowed flip to *Denied; five #86 rows; router_pre86 witnesses the old gap. pre48 and pre53 now see resolve instead of allowUnknown. Makefile test-spec runs router_pre86 and the docker_socket_policy tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
, #86) The same tables in Go, Rust and TypeScript, with the spec row names: - Load-time checks: conflicting policies (shared image namespace, and one without container_names or overlapping names), duplicate service_name, invalid or empty container_names; disjoint names and sibling prefixes load. - Create selection (#85): ?name= picks the one policy matching image and name, a leading slash is stripped, an unaccepted name or a nameless create is denied, an invalid name is denied, ambiguity is denied naming both policies, and the choice is the same over 50 rounds. Through the handler, two policies for one image force their own users on every create. - Lifecycle coverage (#86): a covered name is allowed directly; an uncovered name, an ID or a hex-looking covered name is resolved. Through the handler, a covered container by name, full ID and short ID is forwarded after one GET /containers/{id}/json; an operator container by name or ID and a daemon 404 are denied; a daemon 500 fails closed; the resolved name is audited. realNameDeleteAllowed and execPrefixName*Allowed become *Denied. - deploy/test.sh: two policies for the runner's own image with different container_names and users, five create rounds each, and the deny/allow checks against the real daemon, the runner itself standing for an uncovered operator container. deploy/config gains dsp-it-a and dsp-it-b. - Pulls with shared images: two policies for one image with different tag patterns; a pull is allowed when either accepts the tag (50 rounds). These fail on the code before the fix (main at fd66e22): the Go handler tests (TestHandlerCreateSelectsByName "create ?name=svc-a forwarded User 2222:2222, want 1111:1111", TestHandlerLifecycleCoverage, TestHandlerLifecycleResolveFailsClosed, TestHandlerLifecycleAuditsResolvedName) and 7 load-time rows at runtime; the new router tests and the Rust tests at compile time (container_names, ResolveContainer, select_for_create); and 10 of the 108 integration checks on Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… actions to covered containers (#85, #86) create was whichever came first in map order (Go, Rust: random per request or per process) or load order (TypeScript). Policies get an optional container_names list. POST /containers/create?name=N uses the one policy whose image prefixes match and whose container_names contain N (a policy without container_names accepts any name); none is denied as before or with "container name N not allowed for image X by any policy" / "image X requires a container name", and more than one with "ambiguous policy for image X and name N: a, b". Policies are iterated sorted by service_name. At load, two policies whose prefixes share a namespace and that could both accept one name (either has no container_names, or the lists overlap) fail startup naming both files, as does a duplicate service_name, which used to replace the first file silently, or an invalid or empty container_names. Images and prefixes are compared in the #84 normalised form, the conflict check included, and ?name= is read with the same query parser as the pull (a malformed query is "invalid query string"). A pull has no name and creates no container, so it is allowed when any policy for the image accepts its tag or digest, and denied with the first one's reason. forwarded for any container. A policy now covers its container_names, or its service_name when unset. The router allows a name exactly one policy covers; any other target (an uncovered name, a full or short ID, or a covered name that is all lowercase hex and so could be an ID prefix) is resolved by the handler with GET /containers/{x}/json. A covered .Name is forwarded; an uncovered name or a 404 is denied with "container X is not managed by any policy", and any other daemon error with "cannot resolve container X: ...". The resolved name is audited. --readonly still denies first; reads are unchanged. Docs: README policy schema, endpoint table and Trust model (names are routing, not access control; covered names bound lifecycle actions), config/beacon.yaml example, AGENTS.md counts. BREAKING CHANGE: lifecycle actions and DELETE on a container that no policy covers are denied with 403 (a missing container is now a 403, not the daemon's 404). Policies whose image prefixes share a namespace must set disjoint container_names, and service_name must be unique, or the proxy refuses to start. A create of an image whose policies all set container_names needs a matching ?name=. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eters (#85) The daemon reads the create name with Go's r.Form.Get, which returns the first of a repeated parameter. Selecting a policy on one name while the daemon could name the container after another is avoidable ambiguity, so a create with more than one name parameter (after decoding the key, so n%61me counts) is denied with "duplicate name query parameter", the same rule and wording the image pull uses for fromImage, tag and fromSrc (#83). A query the daemon would reject (a bad escape or a semicolon) is "invalid query string", from the shared parser. Rows for encoded names (%62eacon, validator%2D2), duplicates and invalid queries in Go, Rust and TypeScript, an integration check for ?name=dsp-it-a&name=dsp-it-c, and the test counts in AGENTS.md and README.md (108 integration checks per implementation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abienkowski
force-pushed
the
feat/container-names-routing
branch
from
October 14, 2026 15:45
e7fd772 to
1111812
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Closes #85.
Closes #86.
#85. When several policies allowed one image, the policy applied to a create was effectively random:
GetByImagereturned the first match while iterating a Go map or RustHashMap, and TypeScript always took the first-loaded file. Lodestar's beacon node and validator client both runchainsafe/lodestar, so a validator create could get the beacon user and volumes. Policies had no way to tell two services of one image apart.#86.
start,stop,restart,kill,wait,pause,unpauseandDELETE /containers/{x}were forwarded for any container, so every caller of a socket could stop or remove every container on the host, operator containers included. The README said "Allowed on known containers"; the code, the tests and the Quint model allowed unknown ones on purpose.What changes
container_namesin a policy (Docker name syntax,[a-zA-Z0-9][a-zA-Z0-9_.-]*). A policy covers itscontainer_names, or itsservice_namewhen the list is unset, so existing configs keep their lifecycle behaviour for containers named after the service.POST /containers/create?name=Nuses the one policy whose image prefixes match the image's normalised repository (Image references are not normalized before policy matching (docker.io/library/..., registry ports) #84) and whosecontainer_namescontainN(a policy without the list accepts any name). The query goes through the same parser as the image pull (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), so a query the daemon would reject isinvalid query stringand a repeatednameisduplicate name query parameter(the daemon would take the first); one leading/is stripped and an invalid name isinvalid container name. Zero matches:image X not allowed by any policy(as before),container name N not allowed for image X by any policy, orimage X requires a container namefor a create without?name=. More than one:ambiguous policy for image X and name N: a, b. Policies are iterated sorted byservice_nameeverywhere./-prefix) and where either has nocontainer_namesor the lists overlap; a duplicateservice_name(before, the later file silently replaced the earlier one); an empty or invalidcontainer_names. Prefixes are compared in their Image references are not normalized before policy matching (docker.io/library/..., registry ports) #84 normalised form, sodocker.io/library/alpineandalpineconflict. With this, the runtime ambiguity deny is defence in depth.service_nameorder; the result names the accepting one), and denied with the first one's reason when none does. RowsTestRoutePullSharedImageand peers in all three.DELETEonly on covered containers. The router allows a name exactly one policy covers. Any other target (an uncovered name, a full or short ID, or a covered name that is all lowercase hex and so could be an ID prefix) gets the new actionResolveContainer; the handler asks the daemonGET /containers/{x}/json(path-escaped, nothing from the caller forwarded) and forwards only if.Nameis covered. Uncovered or 404: 403container X is not managed by any policy. Any other daemon error: 403cannot resolve container X: …(fail closed). The resolved name is audited (Go:extra.container/extra.resolved_name; Rust/TS: the auditreason).--readonlystill denies first; reads are unchanged.spec/router.qnt: flagALLOW_UNCOVERED, outcomeresolve,handle= route plus the daemon lookup over a fixed tableDAEMON_CONTAINERS(names and IDs), security propertylifecycleOnlyCovered, the three allow rows flipped torealNameDeleteDenied/execPrefixNameStartDenied/execPrefixNameDeleteDenied, five new rows, androuter_pre86.spec/docker_socket_policy.qnt:containerNamesonPolicy,selectPolicies,createContainerrequires exactly one candidate (beside fix!: default-deny HostConfig on create; check Mounts and read-only binds (#88) #92'screateAllowedHostConfig guard), P0 invariantpolicySelectionUnique(beside fix!: default-deny HostConfig on create; check Mounts and read-only binds (#88) #92'shostConfigInAllowlist/hostMountsInWhitelist), and fourruntests incl. the pre-Policy selection is nondeterministic when several policies allow one image; add container_names routing and deny on ambiguity #85 ambiguity witness. fix!: default-deny HostConfig on create; check Mounts and read-only binds (#88) #92's create scenarios instep(beacon-privileged,mount-root, …) are in the beacon policy'scontainerNames, so they still reach the HostConfig checks; thecreate_modelpolicies set none.make test-specruns both new targets./v1.45/, below the CI daemon's 1.48 limit) (see below). Two existing integration rows that expected the daemon's 404 for an unknown container now expect the proxy's 403.container_names, endpoint table and prose, Trust model: names are routing, not access control, but covered names bound lifecycle actions; Formal Verification counts),config/beacon.yamlexample,spec/README.md, AGENTS.md counts.deploy/configgainsdsp-it-a.yaml/dsp-it-b.yaml(the runner's own image, disjoint names, different users); the existingbeacon,net-host,net-noneandpull-allowedpolicies are unchanged and load without conflict, since none shares an image namespace. Thedsp-it-a/dsp-it-bcreates send noHostConfig, so fix!: default-deny HostConfig on create; check Mounts and read-only binds (#88) #92's default-deny gate passes them.net-host/net-nonecontainers are named after their service, so the Containers created with a policy network_mode do not start: NetworkingConfig still names the "default" network #87 lifecycle checks stay allowed directly.Review notes
feat!:plus aBREAKING CHANGE:footer. Pre-1.0, so merging cuts v0.5.0 (minor;v0.4.0is fix!: default-deny HostConfig on create; check Mounts and read-only binds (#88) #92). Simulated:git log -z --format=%B v0.4.0..HEAD | scripts/release-version.sh v0.4.0→bump=minor tag=v0.5.0.container_namesor the proxy will not start; a create for an image whose policies all setcontainer_namesneeds--name.cafeis still resolved, because the daemon falls back to an ID-prefix match when no container has that name); the create reads?name=from the target thatRouteRequest/route_request/routealready receive after fix: route image pulls on the query string and normalise image references in all three (#83, #84) #91, with no signature change;container_names: []is a load error rather than "covers nothing"; Go caps the inspect body at 4 MiB (Rust's transport already buffers whole responses).start, which answers 304, andDELETEwithout force, which answers 409, are tried on it, so nothing can stop it) and createsdsp-it-a/dsp-it-bfrom its own image through the proxy. Leftovers from an interrupted run are removed at the start; on a pre-fix proxy the creates that should have been denied are removed right after.make test-specgains about 1 s (two more words in the router model, one more instance, and the request-handling tests).Verification
routerwithALLOW_UNCOVERED = truefailscoveredSoundTest, the three flipped rows and four of the five Lifecycle actions and DELETE are allowed on containers that no policy covers #86 rows (coveredNameStartAllowedpasses either way).router_pre86assertslifecycleOnlyCoveredfails and thatDELETE /containers/mycontainer,POST /containers/c0ffee/stopandPOST /containers/exec-runner/startwereallowUnknown. Indocker_socket_policy.qnt, selecting from every image match (the pre-Policy selection is nondeterministic when several policies allow one image; add container_names routing and deny on ambiguity #85oneOf) failscreateUnknownNameDeniedTestandcreateNamelessDeniedTest.fd66e22with this branch's handler and load-time tests:TestHandlerCreateSelectsByName"create ?name=svc-a forwarded User 2222:2222, want 1111:1111", the three lifecycle handler tests, and 7 load-time rows fail; the samples below are from the original base,3130b0a): Go handler and load-time tests fail at runtime, e.g.applies the policy selected by the create name:+ '1111:1111' - '2222:2222'). Rust and the new Go router tests do not compile against the old API (container_names,route_with_query,Action::ResolveContainer). Integration on the pre-fix Go proxy (origin/mainatfd66e22):origin/main(fd66e22) with this branch'sdeploy/. The Policy selection is nondeterministic when several policies allow one image; add container_names routing and deny on ambiguity #85 rounds depend on map order: earlier pre-fix runs got 4/5 and 1/5, and pre-fix Rust picked one policy for the whole process (dsp-it-a 0/5).fd66e22):make test-allGo 131, Rust 158, TS 312 (1 skipped);make lint-all,make build-all,make typecheck,make test-spec(20 runs),make verify BACKEND=typescript,make test-releasepass; integrationALL 108 TESTS PASSEDfor Go, Rust and TS.Type of change
Implementation(s) changed
Testing
make test-all)make verify,make test-spec)Checklist
🤖 Generated with Claude Code