✨ feat(filter): validate Engine API 1.55 and gate BuildKit's opaque tunnel (#153) - #184
Conversation
…unnel Closes the scoped gap list from #153: - New insecure_accept_opaque_buildkit_tunnels ack gates any rule that would admit POST /session, POST /grpc, or a moby.buildkit.v1.Control method path (mirrors validateBodyBlindWriteRules / validateReadExfiltrationRules). Tecnativa's GRPC=1/SESSION=1 compat env vars auto-set the new ack with a deprecation warning so existing configs keep working. - New drydock-with-build.yaml / portwing-with-build.yaml presets ship classic-builder-only POST /build support (DOCKER_BUILDKIT=0) on top of the respective -with-compose.yaml baseline, without opening the BuildKit session/gRPC tunnel; the -with-compose.yaml header comments now point to them instead of describing the gap as unresolved. - POST /containers/create denies unknown HostConfig.Mounts types fail-closed, validates VolumeOptions/ImageOptions.Subpath against path-traversal escapes, and gates privilege-escalating TmpfsOptions.Options (exec/dev/suid) behind the new allow_tmpfs_privileged_options. - POST /networks/create denies an explicit EnableIPv4: false unless allow_disable_ipv4 is set; endpoint GwPriority now falls under the existing allow_endpoint_config gate on both /networks/*/connect and containers/create's NetworkingConfig.EndpointsConfig. - GET /images/{name}/attestations?statement=true is denied by default (new response.allow_attestation_statements); new response.redact_host_topology redacts GET /info host-fingerprinting fields independent of Swarm mode. - X-Registry-Auth / X-Registry-Config headers are bounded-decoded (8 KiB cap, standard/URL-safe/unpadded base64) on image pull, image push, and build before use; X-Registry-Auth serveraddress is checked against the configured registry allowlist when one is set. BuildKit gRPC mediation (parsing/enforcing policy inside the tunnel) is deferred to a v1.7 epic, not implemented here. Refs: #153
- internal/cmd: startup-validator coverage for validateBuildkitTunnelRules(ForPolicy) (global + per-profile), plus TestPresetConfigsDenyAttestationStatementsByDefault, which walks every app/configs/*.yaml preset (not a fixed list) and asserts each denies attestation statements by default. - internal/config: compat_test.go covers GRPC/SESSION auto-acking insecure_accept_opaque_buildkit_tunnels with a deprecation warning, and that it does not override an explicit ack or fire without those env vars. - internal/filter: mount type/subpath/tmpfs coverage (container_create_test.go), network EnableIPv4/GwPriority coverage (network_test.go), registry-header decode coverage including base64-variant, duplicate-key, and credential-non-leak cases (registry_auth_test.go, build_test.go, image_pull_test.go), TestContainerUpdateResourceControlFieldsCompleteness pinning the full guarded field set including all five blkio arrays, and new fuzz-seed corpus entries (/v1.55/, /session, /grpc) across FuzzPathMatch/FuzzGlobToRegex/FuzzNormalizePath/FuzzCompileRule. - internal/responsefilter: host-topology redaction and attestation-statement gating coverage. - New TestMaxSupportedEngineAPIVersionPin backed by app/testdata/docker-api/max-supported-version.txt. - New TestServeHandlerRejectsH2CClientPreface integration test proving a raw HTTP/2 client preface is parsed as an ordinary, policy-denied HTTP/1.1 request rather than tunneled or upgraded. Refs: #153
- security.mdx: new "Compose / BuildKit Transport" section under Layer 5 with a supported-transport matrix (classic builder vs. BuildKit session/gRPC vs. native gRPC-over-h2c) and the insecure_accept_opaque_buildkit_tunnels acknowledgment story. - presets.mdx: document drydock-with-build.yaml and portwing-with-build.yaml; the -with-compose.yaml entries now point to them instead of describing the build gap as unresolved. - configuration.mdx: document all five new fields (insecure_accept_opaque_buildkit_tunnels, response.redact_host_topology, response.allow_attestation_statements, request_body.network.allow_disable_ipv4, request_body.container_create.allow_tmpfs_privileged_options) in the YAML sample, prose, reference table, and environment-variable table; also documents the new bounded X-Registry-Auth/X-Registry-Config header decoding and the mount type/subpath/GwPriority validation. - CHANGELOG.md: Unreleased entry covering all of the above. Refs: #153
New quality-api-version-watch.yml fetches Docker's public Engine API version-history page monthly, extracts the highest documented v1.NN, and fails the job — a red, human-visible CI check rather than a quietly-filed bot issue — when it exceeds the pin in app/testdata/docker-api/max-supported-version.txt (currently 1.55). A maintainer then reviews the new API's changelog entry, updates the pin, and files follow-up filter work for any new inspectable fields. Refs: #153
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 44 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
app/internal/filter/build_test.go (1)
1328-1345: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse a table-driven middleware test.
Convert this standalone case to a table-driven test. Keep the malformed Base64 case in the table. This makes the middleware contract ready for malformed JSON and over-limit header cases.
As per coding guidelines,
**/*_test.go: “Write tests as table-driven tests usingtesting.Tandhttptest.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/internal/filter/build_test.go` around lines 1328 - 1345, Convert TestMiddlewareDeniesBuildWithMalformedRegistryConfigHeader into a table-driven test with named cases, retaining the malformed Base64 header scenario and its forbidden response assertion. Structure each case to support adding malformed JSON and over-limit header inputs, while preserving the middleware setup and upstream-denial behavior.Source: Coding guidelines
app/internal/filter/container_create.go (1)
719-727: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
validMountSubpathdoes not reject.or embedded traversal into a cleaned-safe path — confirm the intended posture.Current behavior:
path.Cleancollapses"a/../b"to"b"and allows it. That is safe."."is also allowed, which is equivalent to no subpath. Both are acceptable. One gap remains: an empty-after-trim value such as" "is not"", is not absolute, and cleans to" ", so it passes as a literal directory name. That matches Docker semantics, so no change is required unless you wantstrings.TrimSpacenormalization for parity with the rest of the file.No action needed if the posture is intentional.
Also applies to: 1246-1293
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/internal/filter/container_create.go` around lines 719 - 727, Confirm that validMountSubpath intentionally permits "." and traversal that path.Clean safely normalizes, as well as whitespace-only values treated as literal directory names under Docker semantics. No code changes are required unless the project explicitly wants strings.TrimSpace normalization for parity with nearby validation.app/internal/filter/container_create_test.go (1)
2871-2913: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd case-variant mount-type cases.
denyUnknownMountTypeReasonlowercasesMount.Type. No case tests exist. Add"BIND"and"Volume"cases to pin the intended behavior, and to catch a divergence if the bind allowlist check comparesTypecase-sensitively.Proposed added cases
{ name: "image mount type passes through when trust is off", body: `{"HostConfig":{"Mounts":[{"Type":"image","Source":"alpine:latest"}]}}`, }, + { + name: "uppercase bind mount type still allowlist-checked", + body: `{"HostConfig":{"Mounts":[{"Type":"BIND","Source":"/safe"}]}}`, + }, + { + name: "uppercase bind mount type with non-allowlisted source denied", + body: `{"HostConfig":{"Mounts":[{"Type":"BIND","Source":"/etc"}]}}`, + wantReason: `container create denied: bind mount source "/etc" is not allowed`, + },Adjust
wantReasonto the exact stringdenyBindMountReasonproduces.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/internal/filter/container_create_test.go` around lines 2871 - 2913, Extend TestContainerCreateUnknownMountType with cases for mount types "BIND" and "Volume", verifying they follow the same behavior as their lowercase equivalents. For the bind case, use an allowlisted source and set wantReason to the exact denyBindMountReason output if the source is denied; ensure the assertions pin case-insensitive mount-type handling.app/internal/filter/container_update_test.go (1)
298-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCompare the field set, not the slice order.
The index-by-index assertion fails when a maintainer inserts a new field out of alphabetical position, even though the policy behavior is unchanged. Order carries no meaning for a presence check. Compare as a set and keep the length guard.
Proposed change
- if len(containerUpdateResourceControlFields) != len(want) { - t.Fatalf("containerUpdateResourceControlFields has %d fields, want %d: %v", len(containerUpdateResourceControlFields), len(want), containerUpdateResourceControlFields) - } - for i, field := range want { - if containerUpdateResourceControlFields[i] != field { - t.Fatalf("containerUpdateResourceControlFields[%d] = %q, want %q", i, containerUpdateResourceControlFields[i], field) - } - } + got := slices.Clone(containerUpdateResourceControlFields) + slices.Sort(got) + wantSorted := slices.Clone(want) + slices.Sort(wantSorted) + if !slices.Equal(got, wantSorted) { + t.Fatalf("containerUpdateResourceControlFields = %v, want %v", got, wantSorted) + }Add
"slices"to the imports.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/internal/filter/container_update_test.go` around lines 298 - 305, Update the assertions covering containerUpdateResourceControlFields to compare field membership as an unordered set rather than index-by-index, while retaining the existing length guard. Add the slices import and use its set-equivalence helper so reordered fields pass while additions or removals still fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/content/docs/configuration.mdx`:
- Line 56: Update the redact_host_topology descriptions at
docs/content/docs/configuration.mdx lines 56, 257, 1040, and 1174 to list the
actual GET /info fields cleared by this setting: Containerd, FirewallBackend,
DiscoveredDevices, and NRI, replacing the kernel/OS/arch/name/labels examples.
In `@docs/content/docs/security.mdx`:
- Line 148: Update the description near the classic POST /build comparison to
identify its request body as bounded and inspectable tar data, using the
application/x-tar build-context semantics instead of describing it as
JSON-decodable. Preserve the surrounding contrast with the ongoing binary
session/gRPC transport.
---
Nitpick comments:
In `@app/internal/filter/build_test.go`:
- Around line 1328-1345: Convert
TestMiddlewareDeniesBuildWithMalformedRegistryConfigHeader into a table-driven
test with named cases, retaining the malformed Base64 header scenario and its
forbidden response assertion. Structure each case to support adding malformed
JSON and over-limit header inputs, while preserving the middleware setup and
upstream-denial behavior.
In `@app/internal/filter/container_create_test.go`:
- Around line 2871-2913: Extend TestContainerCreateUnknownMountType with cases
for mount types "BIND" and "Volume", verifying they follow the same behavior as
their lowercase equivalents. For the bind case, use an allowlisted source and
set wantReason to the exact denyBindMountReason output if the source is denied;
ensure the assertions pin case-insensitive mount-type handling.
In `@app/internal/filter/container_create.go`:
- Around line 719-727: Confirm that validMountSubpath intentionally permits "."
and traversal that path.Clean safely normalizes, as well as whitespace-only
values treated as literal directory names under Docker semantics. No code
changes are required unless the project explicitly wants strings.TrimSpace
normalization for parity with nearby validation.
In `@app/internal/filter/container_update_test.go`:
- Around line 298-305: Update the assertions covering
containerUpdateResourceControlFields to compare field membership as an unordered
set rather than index-by-index, while retaining the existing length guard. Add
the slices import and use its set-equivalence helper so reordered fields pass
while additions or removals still fail.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 224bb263-4890-4bf9-be8c-f3dfcc0d7569
⛔ Files ignored due to path filters (1)
CHANGELOG.mdis excluded by!CHANGELOG.md
📒 Files selected for processing (34)
.github/workflows/quality-api-version-watch.ymlapp/configs/drydock-with-build.yamlapp/configs/drydock-with-compose.yamlapp/configs/portainer.yamlapp/configs/portwing-with-build.yamlapp/configs/portwing-with-compose.yamlapp/internal/cmd/rules.goapp/internal/cmd/rules_test.goapp/internal/cmd/serve.goapp/internal/cmd/serve_h2c_test.goapp/internal/config/compat.goapp/internal/config/compat_test.goapp/internal/config/config.goapp/internal/config/filter_options.goapp/internal/filter/build.goapp/internal/filter/build_test.goapp/internal/filter/container_create.goapp/internal/filter/container_create_test.goapp/internal/filter/container_create_types.goapp/internal/filter/container_update_test.goapp/internal/filter/fuzz_test.goapp/internal/filter/image_pull.goapp/internal/filter/image_pull_test.goapp/internal/filter/network.goapp/internal/filter/network_test.goapp/internal/filter/registry_auth.goapp/internal/filter/registry_auth_test.goapp/internal/filter/version_pin_test.goapp/internal/responsefilter/filter.goapp/internal/responsefilter/filter_test.goapp/testdata/docker-api/max-supported-version.txtdocs/content/docs/configuration.mdxdocs/content/docs/presets.mdxdocs/content/docs/security.mdx
…ld body wording (#153) - configuration.mdx said redact_host_topology strips kernel/OS/arch/ name/labels; the implementation clears Containerd, FirewallBackend, DiscoveredDevices, and NRI. All four mentions now list the real fields. - security.mdx called the classic POST /build body JSON-decodable; it is a bounded, inspectable tar build-context stream.
* 🔧 config(coderabbit): review PRs based on dev/* release branches (#171)
Branch discipline routes every change through the active dev branch
(currently dev/v1.6); CodeRabbit's default only reviews PRs into main,
so the whole release train was skipped with 'reviews are disabled for
this base branch'.
* ✨ feat(release): distribute stable releases via Homebrew tap (#159)
* ✨ feat(release): distribute stable releases via Homebrew tap
- ✨ feat(release): publish sockguard cask to CodesWhat/homebrew-tap via GoReleaser on stable tags
- ✨ feat(ci): add verify-homebrew macOS smoke job (Gatekeeper path, version match, quarantine check)
- 🔧 config(ci): render GoReleaser snapshot in branch CI and assert generated cask contents
- 🧪 test(release): add scripts/homebrew-release.test.mjs covering config, workflows, and docs
- 📝 docs: document Homebrew install path and trust boundary in README, getting-started, RELEASING
* 🐛 fix(release): gate Homebrew tap token requirement to stable tags
- Prerelease tags (containing "-") skip the cask upload via
skip_upload: auto, so the "Require Homebrew tap token" step must not
hard-fail on them; add if: !contains(github.ref_name, '-')
- Assert the stable-only condition in homebrew-release.test.mjs
- Clarify README.md and getting-started.mdx docs: Homebrew's SHA-256
checksum verification proves archive integrity, not publisher
identity, and is not a substitute for Apple notarization; point
policy-bound users at the container image or cosign-verified
release binary instead
* 🐛 fix(ci): pre-pull digest-pinned busybox ref in nightly integration (#165)
* 🐛 fix(ci): pre-pull digest-pinned busybox ref in nightly integration
Docker Hub re-pushed the busybox:1.37 tag, so the digest pinned in
helpers_test.go's busyboxPinnedRef no longer matched what a bare
`docker pull busybox:1.37` materialized in the runner's local image
store. dockerd won't pull-on-create for a digest ref that isn't
already local, so all four tests that pull by digest failed with
"No such image". The pre-pull step now greps the pinned ref straight
out of helpers_test.go and pulls that, so it can never drift from the
constant again. Bumped busyboxPinnedRef's digest to the current
busybox:1.37 index digest.
* 🐛 fix(ci): bind sentinel pre-pull to busyboxPinnedRef constant
Extract the named constant instead of the first busybox substring, and
fail before docker pull if the value is missing or not digest-pinned.
* fix(deps): update module github.com/sigstore/sigstore-go to v1.2.2 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update actions/setup-go action to v7 (#167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update actions/setup-node action to v7 (#168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update dependency node to v24 (#170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update non-major (npm) (#164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update cgr.dev/chainguard/static:latest docker digest to 399c8cb (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update non-major (github-actions) (#163)
* chore(deps): update non-major (github-actions)
* 🔧 chore(deps): regenerate npm lockfile to match package.json ranges
dev/v1.6 HEAD carried a package-lock.json out of sync with package.json
(knip, turbo, oxc-parser, and others) from a sibling npm dependency PR
that merged despite failing CI (Biome Lint / TS Test / Build Workspaces
all failed on `npm ci`: lock file did not satisfy package.json ranges).
Regenerate via `npm install --package-lock-only` so this branch's CI can
install cleanly; no package.json ranges changed.
* 🔧 chore(deps): tidy go.mod/go.sum
dev/v1.6 HEAD carried a go.mod/go.sum out of tidy state (unused
go.opentelemetry.io/otel/sdk/metric indirect require, and several
indirect deps resolved to older patch versions than go.mod's graph
now allows). This is the pre-push goreleaser-snapshot hook's `go mod
tidy` step surfacing pre-existing drift, not a change caused by this
PR's own diff. Committing the tidy output so the local clean-tree gate
(and every subsequent Renovate PR rebased on top of dev/v1.6) doesn't
trip over it.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* chore(deps): pin dependencies (#160)
* chore(deps): pin dependencies
* 🔧 chore(deps): regenerate lockfile for pinned dependency versions
npm install --package-lock-only to sync package-lock.json with the
pinned turbo/knip/biome/lefthook/postcss/typescript versions re-applied
on top of current dev/v1.6 (turbo and knip had moved to newer versions
via sibling merges; pinned those instead of downgrading).
* 🔧 chore(deps): dedupe sharp after lockfile regen
npm install --package-lock-only left sharp nested under
next/node_modules at the pre-override 0.34.5 instead of hoisted to the
root override's ^0.35.0 range. npm dedupe fixes it (matches what the
lockfile-dedupe pre-push guard expects).
* 🔒 fix(deps): pin postcss override exactly to close a real CVE regression
Pinning docs/package.json's postcss to an exact "8.5.25" (this PR's own
pin-dependencies intent) left the root override's "^8.5.24" range
inconsistent with it, and npm's incremental lockfile resolution stopped
applying the override to next's own vendored postcss — reintroducing
next/node_modules/postcss@8.4.31 (GHSA-6g55-p6wh-862q and friends,
HIGH). Pinning the override to the same exact "8.5.25" and forcing a
clean re-resolution of the postcss subtree restores the single hoisted,
patched postcss and fixes the Grype dependency-scan failure this PR's
push triggered.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* chore(deps): pin dependency @types/react to 19.2.18 (#161)
* chore(deps): pin dependency @types/react to 19.2.18
* 🔧 chore(deps): regenerate lockfile for @types/react pin
Regen against dev/v1.6's current pinned postcss/tailwindcss versions
after rebase; confirms the #160 override-pin fix resolves postcss
cleanly everywhere (no @8.4.31 regression).
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* chore(deps): update npm to v12 (#174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): lock file maintenance (#175)
* 🔧 chore(deps): lock file maintenance
Full fresh regen (rm package-lock.json && npm install) against
everything currently merged into dev/v1.6. Required
--legacy-peer-deps to complete resolution due to a pre-existing
upstream mismatch (fumadocs-ui@16.10.5 pins an exact peer
fumadocs-core@16.10.5, but ^16.10.5 now resolves to 16.14.0) —
confirmed this blocks a from-scratch install on dev/v1.6 as-is too,
unrelated to this change. npm ci and npm run build both verified
clean against the resulting lockfile.
* 🔧 chore(deps): pin fumadocs-core/fumadocs-ui to matching exact versions
The previous lockfile regen used --legacy-peer-deps to work around an
ERESOLVE against fumadocs-ui's exact peer dep on fumadocs-core, but that
silently dropped fumadocs-ui's own transitive deps (@radix-ui/react-tabs,
rehype-raw, etc.) from the lockfile entirely — passed locally only because
Turbopack was serving a stale cached build, but broke CI's `npm ci` for
real (module-not-found on both packages).
Root cause: fumadocs-ui@16.14.0 (latest matching the old ^16.10.5 range)
requires peer fumadocs-core@16.14.0 exactly. Pinning both packages to the
same exact 16.14.0 lets a plain `npm install`/`npm ci` resolve cleanly
with no flags, and correctly pulls in fumadocs-ui's full dependency tree.
Verified with a fully clean node_modules (including workspace-nested
docs/node_modules and website/node_modules) via npm install, npm ci, and
an uncached `turbo build --force` — all pass.
---------
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* 📝 docs(roadmap): lay out v1.5.2 patch train and v1.6.0 milestone (#173)
- Add v1.5.2 (in flight) roadmap entry: nightly digest-drift fix, Homebrew tap, dependency refresh including majors
- Restructure v1.6.0 into delivery waves with linked issue numbers: Wave 1 (#149, #151, #152) parallel, Wave 2 (#153 then #148) sequential on the route classifier, Wave 3 (#150) gates GA
- Note the roadmap update in CHANGELOG.md Unreleased/Changed
* 🔧 config(next): enable useTypeScriptCli for TypeScript 7 compatibility (#176)
Next 16.2.12 refuses to build under TypeScript 7 with "TypeScript 7.0.2
does not provide the compiler API required by Next.js" unless
experimental.useTypeScriptCli is set, since Next's default type-checking
path depends on TS6's compiler API and TS7 dropped it in favor of a CLI
interface. Add the flag to both website/next.config.ts and
docs/next.config.ts so Renovate's pending TypeScript 7 bump (#172) can
land without a build break.
Verified locally against the current TS 6.0.3 toolchain: root `npm run
build` (both workspaces), `npm test` (92/92 passing), and `npx biome
check .` all pass clean, with only pre-existing warnings unrelated to
this change.
* 🔒 security(filter): close image-mount trust bypass and network Status redaction gap (#178)
- Deny POST /containers/create and POST /services/create|update requests
carrying a Mounts entry of Type "image" when image_trust is in enforce
mode. Docker API 1.48+'s image-type mount source is an unverified image
reference mounted into the container's filesystem, invisible to the
existing bind-mount checks (Type == "bind" only), and previously bypassed
cosign verification entirely even when the top-level Image field passed.
- Strip the Engine API 1.53 network-inspect Status field (per-subnet IPAM
allocation stats) under redact_network_topology, alongside the existing
IPAM.Config/Containers/Peers redaction.
* 🔧 chore(deps): drop obsolete js-yaml override + fix CodeRabbit dev-branch reviews (#177)
* 🔧 chore(deps): drop obsolete js-yaml override
fumadocs-core 16.14.0 / fumadocs-mdx 15.2.2 no longer resolve js-yaml
anywhere in the tree, so the ^4.3.0 override matches nothing. Removing it
instead of bumping it to v5 — supersedes the original intent of this PR.
Lockfile delta is only the embedded overrides mirror.
* 🔧 chore(coderabbit): nest base_branches under auto_review
base_branches at the reviews: level is silently ignored — every dev/v1.6
PR got 'Review skipped: auto reviews are disabled on base/target branches
other than the default branch' despite the config. The key only takes
effect nested under reviews.auto_review.
* 📦 chore(deps): update dependency typescript to v7 (#172)
Pinned to 7.0.2 in website/ and docs/. Builds go through the TypeScript
CLI via experimental.useTypeScriptCli (landed in #176) since TS7 no
longer exposes the compiler API Next.js 16 uses. Supersedes the
Renovate branch which predated the exact-pin convention and the
dev/v1.6 lockfile regeneration.
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* 🔧 chore(release): prepare v1.5.2 (#179)
* ✨ feat(filter): preserve resource-limit guarantees across updates and services (#152) (#182)
* 🔧 config(request-body): add resource-limit require_* flags for container update and service (#152)
- container_update gains require_memory_limit/require_cpu_limit/require_cpu_limit_hard/require_pids_limit
- service gains require_cpu_limit/require_cpu_limit_hard
- all default false, reload-mutable, mirrored on client profiles
- filter.ContainerUpdateOptions/ServiceOptions carry the new fields for
config plumbing; enforcement lands in the new resource-limit guard,
not in the existing pre-ownership inspectors
* ✨ feat(filter): add post-ownership resource-limit guard (#152)
New internal/filter/resource_limit_guard.go enforces the require_*
resource-limit flags added for container_update and service. It runs as
its own middleware layer AFTER ownership (see the serve.go wiring commit)
rather than inside the existing pre-ownership request-body inspectors:
- container update: root-only decode of the update patch (nested
HostConfig/Resources are decoys and never read), a moby-faithful
overlay of patch onto a bounded GET of the container's current
HostConfig, then the shared create-time predicate against the
effective state. Omission cannot grandfather a pre-existing weak
container (the documented "ratchet" edge).
- service create/ordinary update: request TaskTemplate.Resources.Limits.NanoCPUs
must be positive when either CPU flag is set.
- service rollback (manual ?rollback=previous, or an automatic
UpdateConfig.FailureAction: rollback) validates the daemon-owned
PreviousSpec/current Spec that would actually become active, with a
version/CAS check (409 on mismatch) — the request body proves nothing
once the daemon is about to apply a stored spec instead.
container_create.go's denyResourceLimitReason is extracted to a free
function (resourceLimitDenyReason) shared by container-create and this
guard's container-update path, so the two can never drift; the
create-time method becomes a one-line wrapper.
Four new reason codes (resource_limit_request_invalid 400,
resource_limit_policy_denied 403, resource_limit_policy_lookup_failed 502,
resource_limit_policy_state_changed 409). Only the policy_denied path
honors the resolved profile's warn/audit rollout posture; a corrupt
request, a failed daemon lookup, or a stale version are hard errors in
every mode. No body rewriting anywhere — the guard only decides
allow/deny and always forwards the original bytes.
* 🔧 chore(serve): wire the resource-limit guard into the handler chain (#152)
Insert withResourceLimitGuard between withHijack and withOwnership in
buildServeHandlerLayersWithRuntime's append order. Later appends wrap
(execute before) earlier ones, so runtime order becomes
...filter -> visibility -> ownership -> resource-limit guard -> hijack
-> proxy: ownership still decides before any resource-state daemon
lookup, and the guard still sits ahead of hijack/proxy for everything
ownership allows.
withResourceLimitGuard reuses servePolicyConfig/clientProfiles/
clientacl.RequestProfile — the same PolicyConfig, profile map, and
resolver wiring withFilter already uses — plus the two new runtime
inspectors (container/service state GETs) through the shared upstream
resolver.
Also adds a startup/reload warning (mirroring warnIfLabelACLEnabled's
once-per-process pattern) when a require_* container_update flag is set
while allow_resource_updates is false: structurally valid but very
likely operator confusion, since the flag is a no-op until the gate is
open. Updates TestBuildServeHandlerLayers for the new layer.
* ✨ feat(logging): add resource-limit guard audit/access-log fields (#152)
Adds a pooled logging.ResourcePolicyMeta on RequestMeta, populated only
by filter.ResourceLimitGuard when it actually evaluated policy for a
request (the common "no applicable require_* flag" case leaves it nil —
zero allocation, zero log fields). Fields are classification-only:
kind/operation/state-source/requirements/result/violation-class/
state-lookup — never raw current/effective resource values, inspect
JSON, PreviousSpec content, labels, or identifiers.
- access log: resource_policy_* attrs appended alongside the existing
correlation attrs, only when evaluated.
- audit log: an optional "resource_policy" object. The audit event
takes its own value copy (auditResourcePolicyContextFrom) rather than
the pooled pointer — RequestMeta's pool return (and zeroing) happens
before the audit event reaches the async writer goroutine, so holding
the pooled pointer itself would race; the copy is taken synchronously
in AuditLogMiddleware before the event is queued, matching how every
other auditEvent field is already a plain value snapshotted at that
point.
- putRequestMeta returns meta.ResourcePolicy to its own pool before
zeroing RequestMeta, so the guard's per-request allocation is reused.
* 📝 docs(resource-limit): document the container-update & service resource-limit guard (#152)
- CHANGELOG: [Unreleased] entry covering the 6 new require_* flags, the
effective-state merge semantics, the two rollback-path closures, the
4 new reason codes, and the ratchet migration edge.
- README roadmap: sharpen the "Resource parity" v1.6.0 row to name the
actual delivered flags and rollback-path coverage.
- docs/content/docs/configuration.mdx: new bullets under Request Body
Inspection explaining the guard's merge semantics, fail-closed reason
codes, rollout-mode interaction, and the ratchet migration note;
extends the container_update and service rows in the Request Body
Policy Reference table; adds the 6 new SOCKGUARD_* env var rows.
- app/configs/sockguard.yaml: commented example block for
request_body.container_update.* and request_body.service.* resource
flags, since the shipped default config stays deny-all with no
request_body section active.
* 🐛 fix(filter): reject ambiguous rollback query values in the resource-limit guard
?rollback=previous&rollback=none (or any repeated rollback param) let the
guard silently pick one of Docker's parsed values while validating a
different rollback intent than the daemon might act on. serviceManualRollbackQuery
now requires exactly one value, denying with resource_limit_request_invalid
otherwise, and guardServiceManualRollback's helper return is checked so a
duplicate-value 400 can no longer fall through to respondAllow.
Refs #152
* 🧪 test(filter): add resource-limit guard unit test matrix
Table-driven coverage for the #152 post-ownership guard: container-update
omitted/zero/null/negative/weaker/stronger/mixed scalar semantics against
moby's Memory:0-is-unchanged merge, PidsLimit's pointer-clear semantics
(0/-1/null), CpuQuota-alone-satisfies-hard vs CpuShares-alone-fails-hard,
the ratchet (weak current + omission denies, one compliant patch
remediates), nested-decoy/case-variant/duplicate-key rejection, inspect
404/500/timeout/oversize/malformed handling with call-counting stubs, a
nil inspector failing closed, the allow_resource_updates gate keeping the
inspector unreachable (panicking stub), and a zero-requirements pure
pass-through regression. Service coverage: create/update Limits matrix,
manual rollback (safe-body/weak-PreviousSpec denies, weak-body/safe-
PreviousSpec allows, fake PreviousSpec/RollbackConfig ignored, version
mismatch 409), automatic rollback via UpdateConfig.FailureAction, reason
code/status/static-message assertions, and rollout-mode softening that
only ever applies to resource_limit_policy_denied. Adds a decode fuzz
target over the container-update body.
Refs #152
* 🧪 test(config): cover the resource-limit require_* config surface
Verifies all 6 require_* flags (4 container_update + 2 service) default
disabled, load from SOCKGUARD_REQUEST_BODY_* env overrides, load from
both the global request_body block and a client profile's request_body
block, map completely onto filter.Options, and stay present via
mapstructure tag reflection so a future field rename can't silently drop
one from the schema.
Refs #152
* 🧪 test(cmd): cover resource-limit guard wiring and the require_* warning
Asserts the guard's layer index sits after withOwnership in
buildServeHandlerLayers's append order and, wired end-to-end through a
real ownership middleware, that a foreign container is denied by
ownership before the guard's daemon GET ever runs (only ownership's own
lookup fires). Covers warnResourceLimitRequireOnce: fires at most once
per process via sync.Once, checks both the default policy and every
client profile for a require_* flag enabled while allow_resource_updates
is false, and stays silent when the gate is open or no require_* flag is
set. Also checks compileClientProfiles carries all 6 resource-limit
flags into a named profile's compiled policy.
Refs #152
* 🧪 test(logging): cover resource-policy audit and access-log fields
Confirms every ResourcePolicyMeta.Result class (allow/deny/would_deny/
invalid/lookup_failed/state_changed) surfaces its resource_policy_* access
log fields and resource_policy audit context, that both are entirely
absent from requests the guard never evaluated, that the sync.Pool zeroes
metadata before reuse, that the audit deep copy survives concurrent pool
reuse under -race, and that the audit context type is reflectively
limited to the bounded classification fields (kind/operation/source/
requirements/result/violation/lookup) with no raw values, IDs, or labels.
Refs #152
* 🧪 test(reload): assert the 6 resource-limit require_* flags stay reload-mutable
Locks in that ImmutableDiff treats all 4 container_update require_* flags
plus allow_resource_updates and the 2 service require_* flags as
hot-reloadable config, matching the design's reload-mutable contract for
#152.
Refs #152
* 🧪 test(integration): validate the resource-limit guard against a real daemon
Wires filter.ResourceLimitGuardWithOptions into the integration handler
chain (post-hijack/pre-ownership in append order, matching production)
with real Docker-socket-backed container and service inspectors. The
container-update tier creates a legacy unlimited container and confirms
an omitted-field update is denied and leaves HostConfig.Memory at 0 (the
Memory:0-is-unchanged encoding is only checkable against a real daemon,
not a mock), then confirms a weaker-but-positive update is forwarded
verbatim and a subsequent denied update leaves daemon state unchanged.
The Swarm service tier stays behind SOCKGUARD_TEST_ENABLE_SWARM=1, refuses
to run against a docker daemon already in a swarm, and always leaves via
force in t.Cleanup; it covers create/full-replacement-update denial,
manual rollback to a weak PreviousSpec vs. a safe one, and automatic
rollback (UpdateConfig.FailureAction=rollback) validating the current
Spec. helpers_test.go gains a shared dockerSocketRoundTripper and
HostConfig.Memory on the container-create request type so the new suite
and the existing direct-socket helpers use one transport constructor.
Refs #152
* 📝 docs(changelog): keep the #152 entry under Unreleased after the v1.5.2 rebase
* ✨ feat(listeners): multiple independently scoped main listeners (#149) (#183)
* ✨ feat(config): add multi-listener schema and validation (#149)
Config.Listeners []ListenerConfig is additive: legacy listen: stays
untouched forever. EffectiveListeners() is the single read path every
downstream consumer must use — it synthesizes a single "default" entry
with allowed_profiles ["*"] when Listeners is empty, so back-compat is
byte-for-byte for every config that only sets listen:.
- allowed_profiles: required non-empty on explicit entries, "*"
wildcard preserves legacy global behavior, reserved as a profile
name, cannot mix with concrete names.
- listen/listeners mutual exclusivity, provenance-tracked (YAML key,
SOCKGUARD_LISTEN_* env var, or --listen-socket flag all count as
"explicit") via a defaults-free probe Viper pass in load.go.
- Validation rewrite: listener name regex/uniqueness/cap 32/reserved
"admin", exactly-one-of-socket-or-address per explicit entry,
all-pairs bind-target uniqueness across listeners[*] and admin.listen.
- ListenConfig gains optional socket_uid/socket_gid; socket_mode is
0600 (default) or 0660 (requires explicit socket_gid) everywhere a
ListenConfig appears (legacy listen, listeners[*], admin.listen).
- Admin.MountOn: required when admin rides a main listener and there
are 2+ effective listeners, so admin traffic doesn't silently mount
on every listener by default.
* 🔧 chore(config): use tagged switch for profile-name validation
Style/lint fixup on the #149 wildcard-profile-name reservation check —
staticcheck's QF1002 flagged the plain switch over name == X comparisons
as better expressed as a tagged switch on name.
* ✨ feat(reload): per-listener immutable diff projection (#149)
ImmutableDiff now branches on explicit listeners: usage. Legacy configs
(both old/new have an empty Listeners list) keep reporting a single
"listen" key exactly as before. Once either side uses the explicit
listeners: list, diffListeners takes over: the listener set is immutable
by name (add/remove/rename/rebind all reject, reorder is a no-op), and
every per-listener field is immutable except allowed_profiles — the sole
reload-mutable field, consistent with clients.profiles already being
reload-mutable. Switching between legacy and explicit list mode is
immutable even when structurally a no-op, since the two modes bind
through different code paths.
Deviation: diffListenerFields compares TLS as a single whole
(reflect.DeepEqual) rather than per-subfield (cert_file/key_file/etc
individually) — coarser than the source design sketch, but every entry
still names exactly which listener and that "tls" changed.
* ✨ feat(banner): render one entry per bound listener (#149)
Info.Listen (single string) becomes Info.Listeners ([]string) — one
"name unix:<path>" / "name tcp://<addr>" entry per effective listener,
in bind order, followed by the dedicated admin listener's entry when
configured. Render must only ever be called after every listener in the
list has bound and passed the publish barrier, so the banner is a
confirmation rather than a promise.
* ✨ feat(inbound): stamp non-spoofable listener identity + admission gate (#149)
New internal/inbound package stamps every accepted connection with an
Identity{Name, Role, Network} via http.Server.ConnContext, composed
ahead of clientacl's own ConnContext so identity is derived solely from
which net.Listener accepted the connection — never from request
content.
withListenerAdmission enforces each listener's allowed_profiles scope
using that identity plus the profile clientacl already resolved. A
listener with the wildcard AllowedProfiles admits everything, matching
pre-#149 behavior byte-for-byte. The gate is a complete no-op when
cfg.Listeners is empty (legacy singular listen: config, which always
synthesizes one wildcard listener) — the fail-closed missing-identity
check only activates once an operator opts into explicit listeners:.
* ✨ feat(logging): tag access + audit records with listener name (#149)
Both RequestMeta.ListenerName (access log) and auditEvent.ListenerName
(audit log, JSON "listener_name") are populated from the connection's
inbound.Identity — never request-controllable. Additive: audit's
existing TransportListener field keeps its pre-existing "unix"/"tcp"
transport-kind meaning unchanged.
* ✨ feat(cmd): bind + serve multiple independently scoped main listeners (#149)
Adds the serverGroup runtime for Config.EffectiveListeners():
- serve_listeners.go: two-phase bind barrier (bindMainListeners binds
every main listener before any of them serves; a failure partway
through closes everything already bound, in reverse order, with no
window where a strict subset is live), publishMainListeners fans
every member's terminal Serve() error into one buffered channel
(listenerResult), and concurrent shutdownMainListeners tears every
member down in parallel within the shutdown grace period. Unix
socket members capture a (dev, ino) identity right after bind so
shutdown only unlinks a socket path that still resolves to the exact
inode this process created (removeSocketIfOwned) — legacy
listen.socket/admin.listen.socket keep the pre-#149 unconditional
removal, unchanged.
- serve_deps.go: createSocketListener is generalized to take an
explicit file mode + optional uid/gid (socketListenFileMode mirrors
config's own validateSocketOwnership rules), and a new
createNamedListener binds one listeners[*] entry (unix or TCP).
listenUnixSocketWithMode adds a stale-socket guard: any live dial on
the target path refuses to bind (steals nothing); a failed dial
(refused, ENOENT, timeout) proceeds with the pre-existing
remove-then-listen sequence, since only the connect-succeeded case
risks stealing a listener that's actually serving traffic.
- serve.go: wires withListenerAdmission into the shared handler chain
ahead of withClientACL's own position, and replaces the single
bind/serve/shutdown block with the serverGroup — one
reload.SwappableHandler shared by every main listener, the admin
server bound+served sandwiched between the main bind and main serve
phases, and concurrent shutdown of every main member alongside the
admin server.
Existing tests updated for the new shapes: TestBuildServeHandlerLayers
gets the new "withListenerAdmission" layer in its pinned lists: and
TestRunServe_AdminShutdownErrorLogs is rewritten to not assume
admin-then-main shutdown ordering (now concurrent) — the old version's
shared shutdownCalls counter read/written from two goroutines was
itself a data race under -race.
* ✨ feat(observability): per-listener metrics + close admin mount_on gap (#149)
🐛 fix(cmd): every main listener shares one handler chain
(reload.SwappableHandler), so an in-band admin endpoint (admin.enabled,
no dedicated admin.listen) was reachable on every listener regardless
of admin.mount_on — validation already required mount_on once there
are 2+ effective listeners, but nothing enforced it at request time.
New mountOnGate wraps withAdminEndpoint/withPolicyVersionEndpoint and
only forwards to them when the connection's inbound identity matches
Admin.MountOn; with <=1 effective listener it's a pass-through,
preserving today's zero-config "admin rides the sole main listener"
behavior byte-for-byte.
✨ feat(metrics): add a listener label to requestLabels/denyLabels/
durationLabels (sourced from inbound.Identity, falling back to
"default" when absent — matching the legacy synthesized listener
name), and a new sockguard_listener_up{listener,role,network} gauge.
SetListenerUp is called with up=true right after publish for every
main listener and the admin listener, and up=false at the start of
drain in shutdownServers — the series is created once and never
removed, so a stopped listener reads 0 rather than disappearing.
* ✨ feat(health): surface per-listener state in /health (#149)
health.HealthResponse gains a Listeners []ListenerStatus field
(name/role/network/state; states bound -> serving -> draining ->
stopped on a clean run, or failed if Serve() returns unexpectedly
before an intentional drain). Monitor.ListenersFunc, when set, both
populates that field and folds listener state into the 503 decision:
any listener not "serving" or "draining" makes /health unhealthy
alongside (or independently of) an upstream failure.
internal/cmd wires this with a new listenerStatusBoard — a small
mutex-guarded map keyed by listener name, updated at every lifecycle
transition (bind, publish, fan-in failure, shutdown start, shutdown
complete) for both main listeners and the dedicated admin listener.
Both the liveness and readiness monitors share the same board, so
either endpoint reports the same listener state. Threaded as an
explicit board parameter through bindMainListeners,
publishMainListeners, shutdownMainListeners, and shutdownServers.
* 🐛 fix(config): transport-capability checks honor listeners: (#149)
validateClientsListenerExclusions and validateClientsCertificateProfiles
read cfg.Listen directly, so clients.unix_peer_profiles,
clients.client_certificate_profiles, clients.allowed_cidrs,
clients.container_labels, and clients.source_ip_profiles all rejected
valid configs that used the new listeners: list instead of the legacy
listen: block — every listeners[*] entry was invisible to these checks
regardless of its actual transport. New hasEffectiveListener walks
cfg.EffectiveListeners() so "at least one compatible listener" holds
for both legacy and explicit multi-listener configs, matching the
final design's item 12. Found by writing configs/multi-listener.yaml
against the real validator.
* 📝 docs(listeners): document multi-listener config and ship example preset
- CHANGELOG entry for the `listeners:` feature (#149): allowed_profiles
semantics, mutual exclusivity with legacy `listen:`, admin.mount_on
requirement, and observability additions
- README: listeners: config snippet and a link to the new preset under
the bundled-presets list
- app/configs/multi-listener.yaml: working two-unix-socket preset
demonstrating scoped ci/ops listeners with disjoint allowed_profiles
* 🐛 fix(cmd): fail startup on ambiguous stale-socket probe results (#149)
Only a proven ECONNREFUSED probe result, plus a matching Lstat inode/device
identity check taken before and after the probe, may remove an existing unix
socket file as stale. Every other probe outcome (a successful dial, a
timeout, a permission error, or the path changing identity mid-probe) now
fails startup instead of unlinking a socket sockguard can't prove is dead —
closing the gap where a dial timeout was previously treated as "not live"
and removed.
* 🐛 fix(cmd): bind admin listener inside the two-phase bind barrier (#149)
bindAdminServer now binds the dedicated admin listener as the final member
of the same all-or-none bind transaction as the main listeners: mains bind,
then admin binds, and only after every bind in the group has succeeded does
anything start Serve. An admin bind failure rolls back every already-bound
main listener (reverse order); a main listener bind failure never reaches
the admin bind at all. This replaces the old startAdminServer path, which
bound and started serving the admin listener strictly after the main-listener
bind barrier had already published — a window where main listeners could be
live while the admin bind was still pending, or vice versa on failure.
Folded in as part of the same rewrite (interdependent within
runServeWithDeps): any main or admin Serve() return before an intentional
group drain is now fatal (including nil and http.ErrServerClosed, neither of
which can legitimately occur while the group is healthy), draining the whole
group before returning a process error; listener-group shutdown now runs
under a fresh 30s deadline instead of the already-cancelled command context,
force-closes listeners/servers at the deadline, and explicitly tears down
hijacked connections (Docker attach/exec streaming) that http.Server.Shutdown
does not close on its own.
Removes the now-dead startAdminServer function and its direct-unit tests,
replacing them with equivalent bindAdminServer coverage.
* 🐛 fix(listeners): close remaining #149 design-conformance gaps
- config: bind-target uniqueness across listeners/admin now compares a
normalized TCP endpoint (case-insensitive host, canonical IP spelling,
numeric port without leading zeroes) instead of a raw string, so
differently-spelled duplicates of the same literal endpoint are still
caught. Explicit TLS on a unix listener and socket_uid/socket_gid on a
TCP listener are now rejected instead of silently ignored.
- health: the /health response always encodes "listeners" as an array
(empty, not omitted) so clients get one stable response schema whether
or not a ListenersFunc is wired up.
- reload: ImmutableDiff reports individual TLS subfields
(listeners.<name>.tls.cert_file, .key_file, ...) instead of collapsing
any TLS change into a single opaque "tls" entry, so a failed reload's
diagnostic names the exact field an operator needs to restart for.
* 🧪 test(config): cover multi-listener schema, validation, and exclusions (#149)
Table-driven and fuzz coverage for the listeners: schema: name/uniqueness/
reserved-name/cap rules, exactly-one-of-socket-or-address, TLS/ownership
field misuse per transport, allowed_profiles wildcard-vs-concrete semantics,
all-pairs bind-target uniqueness (including the normalized-TCP-endpoint
cases), and validateClientsListenerExclusions' unix-only/TCP-only client
constraint checks. FuzzEffectiveListenerValidation exercises
EffectiveListeners() plus validation against arbitrary listener sets to
guard the never-panics / never-silently-accepts-conflicting-config
invariant.
* 🧪 test(inbound): pin non-spoofable listener identity propagation (#149)
Covers ConnContext composition (identity stamped before the existing
clientacl.ConnContext runs, so both are present together), context
round-tripping via inbound.FromContext, and that identity is derived only
from the listener the connection actually arrived on — never from anything
attacker-controlled in the request itself.
* 🧪 test(observability): cover per-listener health, metrics, and log fields (#149)
health: /health returns 503 whenever any required listener isn't serving
outside an intentional drain, and always encodes "listeners" as an array.
metrics: sockguard_listener_up{listener,role,network} and the listener
label on the request/duration/deny/throttle families. logging: access and
audit records carry listener/listener_name without disturbing the existing
audit transport field.
* 🧪 test(reload): cover per-listener immutable diff and reload rejection (#149)
listeners_diff_test.go/listeners_diff_fuzz_test.go pin ImmutableDiff's
per-name per-field projection: allowed_profiles is the only field a swap
may change, every other field (including individual TLS subfields) is
immutable and reported as listeners.<name>.<field>, and add/remove/rename
are reported distinctly from a same-name field change.
TestReloadCoordinatorFailedListenerReloadLeavesOldGenerationServing (new,
in serve_reload_test.go) proves a failed reload — whether from an invalid
allowed_profiles value or an attempted immutable-field change — keeps the
old handler generation live and serving rather than falling through to a
deny-all handler.
* 🧪 test(cmd): cover bind barrier, listener admission gate, and default-profile warning (#149)
serve_listeners_feature_test.go: the stale-socket probe safety matrix (dead
refused socket replaced; live socket, ambiguous timeout, non-socket file,
and inode-changed-mid-probe all preserved and fail startup); the bind
barrier rolling back main listeners in reverse on an admin bind failure and
never reaching/serving admin on a main bind failure; any premature Serve
return draining and failing the whole group; the listener-status board
state machine and concurrent access; pre-publish gauge registration; and
shutdown using a fresh deadline, force-closing at it, and tearing down
hijacked connections within it.
serve_listener_admission_test.go: withListenerAdmission on the single
shared handler chain denies 403 listener_profile_not_allowed for a resolved
profile outside a listener's allowed_profiles, admits wildcard/matching
listeners, and fails closed (500) on missing/unknown inbound identity.
serve_listener_warning_test.go: the startup warning when clients.default_profile
isn't included in a listener's allowed_profiles, silent for wildcard or
explicitly-included cases.
* 🧪 test(integration): exercise unix multi-listener profile isolation end-to-end (#149)
Builds the real binary and drives it against two unix-socket listeners
(ci/ops) with disjoint allowed_profiles over a mocked upstream: the same
unix-peer-uid-derived ci profile is served on its own listener and denied
403 listener_profile_not_allowed on the other, proving profile-bleed
across listeners is impossible end-to-end, not just at the unit level.
* 📝 docs(changelog): keep the #149 entry under Unreleased after the v1.5.2 rebase
* 🐛 fix(logging): dedupe audit event fields after the #152 logging rebase
* 🧪 test(cmd): cover #149 listener-bind and socket-identity branches under the coverage gate
PR #183 landed at 95.8% production coverage against the 96% CI floor
(run 31004873914). The #149 multi-listener surface left several branches
entirely unexercised because higher-level bind-barrier tests only ever
drive them through stubbed deps fields:
- createNamedListenerImpl, chownSocket, defaultProbeUnixSocket, and
createSocketListener's chown-success/chown-failure paths in
serve_deps.go were all 0% — every existing test stubs
deps.createNamedListener/deps.probeUnixSocket directly instead of
calling through to the real implementation.
- statSocketIdentity and socketIdentityFromFileInfo's guard branches
(empty path, nil lstat func, nil FileInfo, non-*syscall.Stat_t Sys())
and removeSocketIfOwned's inode-match/mismatch happy path were
uncovered because the one integration test that reaches them never
hits a real matching inode.
- hijackedConnTracker.transition's nil-receiver guard, its
already-closed-tracker branch, and the plain StateClosed case, plus
listenerStatusBoard's nil-receiver guards and setState-on-unknown-name
no-op, were never called outside their always-non-nil call sites.
Added direct table-driven and real-socket unit tests for each, matching
the existing shortSocketPath/newServeTestDeps/socketFileInfo test
helpers. Local production coverage: 96.3% (mirrors ci-verify.yml's
coverage.prod.txt gate).
* ✨ feat(policy): fail-closed declarative admission mutations (#151) (#181)
* ✨ feat(policy): add shared JSON codec for admission-mutation writes
Introduces internal/filter/json_mutate.go, a fold-aware JSON document
codec for code that writes into (not just reads) a Docker API request
body: exact-duplicate-key rejection via a token-scan pass (map-decode
silently collapses exact duplicates before any value-tree check could
see them), depth/node-count/EOF/root bounds, and a canonicalizing parse
usable both before and after a mutation is applied.
internal/ownership/middleware.go's nestedObject/nestedObjectPath/
foldedObjects/foldedStrings/foldedArrays/foldedStringEquals were
independently-maintained copies of the same fold/merge logic; they now
call the exported filter.NestedObject/NestedObjectPath/FoldedObjects/
FoldedStrings/FoldedArrays/FoldedStringEquals instead, so there is one
reviewed implementation shared by owner-label stamping and the new
admission-mutation engine (a following commit).
* ✨ feat(policy): add per-request admission-mutation logging record
Adds logging.MutationRuleOutcome/MutationRecord (a pooled per-request
trace of which admission-mutation rules matched and what they did) and
wires it into the existing access/audit pipeline:
- RequestMeta gains a Mutation field; putRequestMeta returns it to a
dedicated sync.Pool before zeroing, matching the existing pooling
pattern for RequestMeta itself.
- AccessLogMiddleware appends mutation_rule_ids/mutation_changed to the
structured log line and elevates an otherwise-allowed request to WARN
when a warn-mode mutation rule was evaluated, mirroring how a
warn-rollout policy deny is already elevated.
- AuditLogMiddleware deep-copies the pooled MutationRecord into a fresh
auditMutationRecord before the event is handed to the async audit
channel — required because the pooled record can be recycled for an
unrelated request before the channel consumer encodes it.
No production code path populates these yet; the admission-mutation
engine that does lands in a following commit.
* ✨ feat(policy): add fail-closed declarative admission-mutation engine
Adds internal/filter/mutation.go: a bounded, config-driven admission
mutation engine (#151) supporting exactly two rule operations
(inject_labels label-map merge, remap_image single string-field
replace) with per-rule enforce/warn/audit rollout modes.
Core invariants:
- enforce-mode rules are applied to the actual document that will be
forwarded; warn/audit-mode rules are evaluated only against an
independent deep-cloned shadow document and never influence what is
committed.
- The request body is only read/rewritten when at least one rule is
configured for the request's surface, and only actually rewritten
(replaceRequestBody) when an enforce rule changed something —
otherwise the original bytes readBoundedBody restored stay in place
byte-for-byte.
- Every mutation result is re-parsed through the same strict scanner
used on the client's input (parseMutationDocument) before it can be
forwarded — canonicalize-before-and-after, not just before.
- remap_image validates its computed result with go-containerregistry's
weak-reference grammar (the same grammar imagefetch.PinnedReference
already applies), invoked only when a remap_image rule is configured
AND matched — mirroring how the existing (also opt-in) image-trust
verifier only ever touches go-containerregistry when image_trust is
configured.
Two supporting pieces in filter's existing infrastructure:
- body_read.go: replaceRequestBody, a transport-safe body-rewrite
helper (Content-Length/Transfer-Encoding/GetBody kept in lockstep;
the proxy layer only ever reads req.ContentLength, never the literal
header).
- request_rejection.go: requestRejectionError gains an optional
reasonCode override so the engine's four new reason codes
(mutation_request_invalid, mutation_request_too_large,
mutation_apply_failed, mutation_postcondition_failed) survive to the
denial response instead of collapsing to the generic status-derived
code.
Nothing in filter's dispatch tables calls this engine yet; that wiring
lands in the next commit so this one stays independently buildable and
reviewable.
* ✨ feat(policy): wire admission mutations into the filter dispatch chain
Wires the mutation engine from the previous two commits into
compileRuntimePolicy's dispatch table (#151):
- filter.Options gains a Mutation field; MiddlewareWithOptions compiles
one mutationEngine per MiddlewareWithOptions call (reload rebuilds it
along with everything else) and shares it identically across the
default policy and every client profile — mutation config is global,
not per-profile (see mutation.go's MutationOptions doc comment).
- compileRuntimePolicy registers the two mutation entries
(newContainerCreateMutationPolicy / newServiceMutationPolicy)
immediately BEFORE the existing container_create/service entries, at
the same (method, matches, severity) tuple. inspectAllowedRequest
buckets matches by severity and runs every policy in the single
matched bucket in slice order — verified by re-reading
middleware.go's own inspectAllowedRequest, not assumed — so this
placement guarantees mutation always applies/canonicalizes before
container_create/service's own body inspection runs against whatever
bytes are in r.Body afterward.
- MiddlewareWithOptions stashes the resolved logging.RequestMeta onto
the request's context (logging.WithMeta) so mutation.go's
recordMutationOutcome — an inspectorFunc, which only ever receives
*http.Request — can reach it without widening that signature for
every other existing inspector.
- inspectAllowedRequest's rejection-handling branch now prefers
requestRejectionError.reasonCode when the inspector set one
explicitly, falling back to the existing status-derived code
otherwise — needed for the mutation engine's four reason codes to
actually reach the denial response.
Existing compileRuntimePolicy(nil, PolicyConfig{}) call sites in
middleware_test.go and middleware_method_dispatch_test.go are updated
for the new third parameter (nil mutation engine — rulesFor(nil) is a
documented no-op).
* ✨ feat(policy): add mutations.rules[] config schema and validation
Adds the operator-facing config surface for #151:
- config.go: MutationsConfig / MutationRuleConfig /
InjectLabelsMutationConfig / ImageRemapMutationConfig, plus
Config.Mutations. Deliberately global (not part of
clients.profiles[]) — v1 has one mutation authority and no
global/profile merge rules.
- load.go: decodeMutationsStrict re-decodes the mutations subtree with
ErrorUnused=true, WeaklyTypedInput=false, and no decode hook, on top
of (and overwriting) the lenient Config-wide decode every other
block still gets. Unlike the rest of this legacy schema, a typo'd key
or a coerced "id: 0" is a load-time error here, not a silently
accepted no-op — called from both Load and LoadBytes so the admin
/admin/validate and signed-policy-bundle paths get the same
strictness as a file-based startup load.
- validate.go: validateMutationsConfig enforces rule/label-count
bounds, id format + uniqueness, mode/surface enums, exactly-one-of
inject_labels/remap_image, label-surface restriction (inject_labels
invalid on service_update), key/value size and control-character
bounds, exact-match image literals parsing via go-containerregistry's
weak-reference grammar, cross-rule overlap rejection (same label key
or overlapping image `from` pattern on a shared surface — which rule
would win is otherwise order-dependent), and rejects injecting the
reserved ownership.label_key when ownership.owner is configured.
- filter_options.go: MutationsConfig.ToFilterOptions converts to
filter.MutationOptions.
- cmd/serve.go: serveFilterOptions attaches
cfg.Mutations.ToFilterOptions() to filter.Options.Mutation.
internal/config now imports go-containerregistry for exact-match image
literal validation at config-load time — outside the request hot path,
and only reached when an exact-match remap_image rule is actually
configured, mirroring how image_trust's use of the same dependency is
conditioned on that feature being configured.
* 📝 docs(policy): document fail-closed admission mutations
- CHANGELOG.md: ### Added entry for mutations.rules[] (#151).
- README.md: policy-surfaces summary line now mentions declarative
admission mutation.
- docs/content/docs/configuration.mdx: new "Admission Mutations"
section — schema table, example, the global-not-per-profile scope
decision, and the strict-decode behavior.
- app/configs/cis-docker-benchmark.yaml: commented example rule tagging
admission-approved containers with a compliance label, off by
default.
* 🐛 fix(policy): harden admission-mutation body handling and validation
Fixes surfaced while writing the mutation test suite:
- config/validate.go: reject empty inject_labels values (spec requires
non-empty label values; previously only whitespace/control chars and
size were checked).
- filter/body_read.go: replaceRequestBody now clones the final byte
slice before installing it as the request body/GetBody closure, so a
caller mutating its buffer after commit can never retroactively alter
bytes already forwarded or replayed.
- filter/mutation.go: stop passing decode/apply errors to
logRequestError — those errors can carry body-derived text (e.g.
offending key/value fragments), which the design's logging invariant
explicitly forbids; only the generic denial message and reason code
are now logged.
* 🧪 test(filter): cover admission-mutation engine and JSON codec
- json_mutate_test.go: ambiguity corpus (exact/folded duplicates at
root/nested/in-array, case-sensitive data-map exemptions, trailing
data, null/array/scalar roots, depth/node caps), numeric-lexeme and
JSON-looking-string preservation across marshal/reparse, and
replaceRequestBody transport-state reset including anti-aliasing of
the caller's buffer.
- mutation_test.go: full-chain integration per surface (container
create, service create, service update) proving injected/remapped
bytes reach the required-label check, registry policy, and
image-trust verifier on the post-mutation reference; warn/audit
byte-for-byte transport non-interference; audit-record outcome
vocabulary with no sensitive value leakage; fail-closed behavior on
malformed/duplicate-key/oversized/unparseable-remap bodies and body
read errors, with zero upstream calls.
- FuzzMutationRoundTrip seed corpus and fuzz test: parse/mutate/
marshal/reparse invariants (forwardable output reparses clean with
no residual ambiguity).
* 🧪 test(config): cover mutations.rules[] schema and validation
Strict-decode rejection of unknown keys and privileged-injection
vectors (set_json/patch/path/exec/webhook) at every level of the
mutations subtree, both via Load and LoadBytes; YAML typing-attack
corpus (wrong scalar/list/object shapes); rule-shape validation (id
pattern/length, mode enum, surfaces enum/dedup, exactly-one-of
inject_labels/remap_image, service_update label rejection); bounds
(64 rules, 32 labels/rule, 256 labels total, key/value/image byte
caps); label key/value character rules; owner-label-key reservation
when ownership is enabled; overlap rejection (same label key on a
shared surface, exact-in-prefix and prefix-in-prefix image matches)
alongside acceptance of genuinely non-overlapping rules; and
ToFilterOptions type-preserving translation to filter.MutationOptions.
* 🧪 test(cmd): cover admission mutations through the built serve chain
Full buildServeHandlerLayers integration per surface (container
create, service create, service update) via a real upstream test
server: label injection satisfying a downstream required-label check
with a single canonical Labels field forwarded, service create
mutating both root and TaskTemplate.ContainerSpec label maps plus the
task image, and service update remapping the image while preserving
the ?version= query string and a >53-bit Version.Index integer
untouched.
* 🧪 test(logging): cover mutation record pooling and audit deep-copy
MutationRecord pool round-trip zeroes Rules/ActualChanged/
HasWarnEvaluation on reuse; newAuditMutationRecord deep-copies rule
outcomes so mutating or reusing the pooled source after enqueue can't
retroactively change an already-emitted audit event; nil/empty
records are omitted from the audit event rather than emitted empty.
* 🧪 test(reload): assert mutations config stays hot-reload mutable
mutations and every mutations.* subtree must be absent from
ImmutableFields — admission-mutation rules are meant to be editable
via config hot-reload, not restart-only.
* 🔧 chore(deps): promote mapstructure to a direct dependency (#151)
decodeMutationsStrict imports go-viper/mapstructure/v2 directly for the
strict mutations-subtree decode; go mod tidy moved it out of indirect.
* 📝 docs(changelog): keep the #151 entry under Unreleased after the v1.5.2 rebase
* 🐛 fix(config): normalize remap_image.match in ToFilterOptions (#151)
Validation accepts remap_image.match case-insensitively but never wrote
the canonical form back onto the config value, so ToFilterOptions was
handing a raw "Exact"/"Prefix" straight to the filter engine and relying
on filter.newMutationEngine's own normalization to save it. Normalize in
ToFilterOptions too, so this package's output is canonical independent of
that internal detail.
Addresses CodeRabbit comment on PR #181 (validate.go:742).
* 📝 docs(mutations): clarify audit-mode behavior and prefix-match scope (#151)
- cis-docker-benchmark.yaml: the compliance-label example comment claimed
sockguard marks approved containers, but the pasted rule is mode: audit,
which only records what would happen and never writes the label.
Restate that plainly instead of leaving it to the trailing note.
- configuration.mdx: remap_image.match is a literal-string match with no
docker.io/library alias expansion, so the pin-internal-registry
docker.io/ example never touches an unqualified alpine:3.21 or
nginx:1.27 reference. Add a second exact-match rule plus a paragraph
spelling that out, and note in the field table that inject_labels keys
and values must both be non-empty (whitespace-only included).
Addresses three CodeRabbit comments on PR #181 (cis-docker-benchmark.yaml:136,
configuration.mdx:669, configuration.mdx:676).
* 🧪 test(filter): cover admission-mutation branches under the coverage floor (#151)
mutation.go and json_mutate.go landed under the 96% production coverage
gate (95.8% on PR #181, CI run 31003217007). Add table-driven and direct
unit tests for the branches that were missing real coverage: default rule
mode, malformed-rule skip, nil-engine/nil-doc/unknown-kind guards, empty
body and non-POST/nil-body/nil-request inspect() short-circuits, remap
no-op paths (unsupported surface, absent target field, from mismatch,
result equal to current), empty-result and non-object-target failure
denials, mutationRemapMatch's full match-kind table, the JSON strict
scanner's depth/node/EOF/truncation error paths for both object and array
nesting, deepCloneJSONValue's array branch, the Folded* helper family,
soleFoldedObject/navigateFoldedObjectPath's not-found paths, and
NestedObject's nil-variant skip.
internal/filter production coverage: 96.1% -> 98.2%.
* 🧪 test(logging): cover mutation record pool fallback and access log fields (#151)
Go coverage is per test-binary: internal/filter's mutation tests already
drive AccessLogMiddleware end to end with mutation records attached, but
that doesn't count toward internal/logging's own coverage since it's a
different package's test binary. Add direct logging-package tests for
joinMutationRuleIDs, the WARN-elevation branch for an allowed request with
a warn-mode mutation evaluation, the omitted-fields case when no rule
matched, and GetMutationRecord's defensive nil-fallback when the pool
returns a wrong-typed value (mirroring the existing requestMetaPool
fallback test's New-override + drain pattern, since sync.Pool's internal
LIFO/victim-cache ordering means Put alone isn't reliable here).
internal/logging production coverage: 97.0% -> 98.5%.
* ✨ feat(filter): validate Engine API 1.55 and gate BuildKit's opaque tunnel (#153) (#184)
* ✨ feat(filter): validate Engine API 1.55 and gate BuildKit's opaque tunnel
Closes the scoped gap list from #153:
- New insecure_accept_opaque_buildkit_tunnels ack gates any rule that
would admit POST /session, POST /grpc, or a moby.buildkit.v1.Control
method path (mirrors validateBodyBlindWriteRules /
validateReadExfiltrationRules). Tecnativa's GRPC=1/SESSION=1 compat
env vars auto-set the new ack with a deprecation warning so existing
configs keep working.
- New drydock-with-build.yaml / portwing-with-build.yaml presets ship
classic-builder-only POST /build support (DOCKER_BUILDKIT=0) on top
of the respective -with-compose.yaml baseline, without opening the
BuildKit session/gRPC tunnel; the -with-compose.yaml header comments
now point to them instead of describing the gap as unresolved.
- POST /containers/create denies unknown HostConfig.Mounts types
fail-closed, validates VolumeOptions/ImageOptions.Subpath against
path-traversal escapes, and gates privilege-escalating
TmpfsOptions.Options (exec/dev/suid) behind the new
allow_tmpfs_privileged_options.
- POST /networks/create denies an explicit EnableIPv4: false unless
allow_disable_ipv4 is set; endpoint GwPriority now falls under the
existing allow_endpoint_config gate on both /networks/*/connect and
containers/create's NetworkingConfig.EndpointsConfig.
- GET /images/{name}/attestations?statement=true is denied by default
(new response.allow_attestation_statements); new
response.redact_host_topology redacts GET /info host-fingerprinting
fields independent of Swarm mode.
- X-Registry-Auth / X-Registry-Config headers are bounded-decoded
(8 KiB cap, standard/URL-safe/unpadded base64) on image pull, image
push, and build before use; X-Registry-Auth serveraddress is checked
against the configured registry allowlist when one is set.
BuildKit gRPC mediation (parsing/enforcing policy inside the tunnel)
is deferred to a v1.7 epic, not implemented here.
Refs: #153
* 🧪 test: cover Engine API 1.55 validation and BuildKit tunnel gating
- internal/cmd: startup-validator coverage for
validateBuildkitTunnelRules(ForPolicy) (global + per-profile), plus
TestPresetConfigsDenyAttestationStatementsByDefault, which walks
every app/configs/*.yaml preset (not a fixed list) and asserts each
denies attestation statements by default.
- internal/config: compat_test.go covers GRPC/SESSION auto-acking
insecure_accept_opaque_buildkit_tunnels with a deprecation warning,
and that it does not override an explicit ack or fire without those
env vars.
- internal/filter: mount type/subpath/tmpfs coverage
(container_create_test.go), network EnableIPv4/GwPriority coverage
(network_test.go), registry-header decode coverage including
base64-variant, duplicate-key, and credential-non-leak cases
(registry_auth_test.go, build_test.go, image_pull_test.go),
TestContainerUpdateResourceControlFieldsCompleteness pinning the
full guarded field set including all five blkio arrays, and new
fuzz-seed corpus entries (/v1.55/, /session, /grpc) across
FuzzPathMatch/FuzzGlobToRegex/FuzzNormalizePath/FuzzCompileRule.
- internal/responsefilter: host-topology redaction and
attestation-statement gating coverage.
- New TestMaxSupportedEngineAPIVersionPin backed by
app/testdata/docker-api/max-supported-version.txt.
- New TestServeHandlerRejectsH2CClientPreface integration test proving
a raw HTTP/2 client preface is parsed as an ordinary, policy-denied
HTTP/1.1 request rather than tunneled or upgraded.
Refs: #153
* 📝 docs: document Engine API 1.55 and BuildKit transport changes
- security.mdx: new "Compose / BuildKit Transport" section under
Layer 5 with a supported-transport matrix (classic builder vs.
BuildKit session/gRPC vs. native gRPC-over-h2c) and the
insecure_accept_opaque_buildkit_tunnels acknowledgment story.
- presets.mdx: document drydock-with-build.yaml and
portwing-with-build.yaml; the -with-compose.yaml entries now point
to them instead of describing the build gap as unresolved.
- configuration.mdx: document all five new fields
(insecure_accept_opaque_buildkit_tunnels, response.redact_host_topology,
response.allow_attestation_statements,
request_body.network.allow_disable_ipv4,
request_body.container_create.allow_tmpfs_privileged_options) in the
YAML sample, prose, reference table, and environment-variable table;
also documents the new bounded X-Registry-Auth/X-Registry-Config
header decoding and the mount type/subpath/GwPriority validation.
- CHANGELOG.md: Unreleased entry covering all of the above.
Refs: #153
* 🔧 chore(ci): add monthly Engine API version watch
New quality-api-version-watch.yml fetches Docker's public Engine API
version-history page monthly, extracts the highest documented v1.NN,
and fails the job — a red, human-visible CI check rather than a
quietly-filed bot issue — when it exceeds the pin in
app/testdata/docker-api/max-supported-version.txt (currently 1.55).
A maintainer then reviews the new API's changelog entry, updates the
pin, and files follow-up filter work for any new inspectable fields.
Refs: #153
* 📝 docs(response): correct redact_host_topology fields and classic-build body wording (#153)
- configuration.mdx said redact_host_topology strips kernel/OS/arch/
name/labels; the implementation clears Containerd, FirewallBackend,
DiscoveredDevices, and NRI. All four mentions now list the real
fields.
- security.mdx called the classic POST /build body JSON-decodable; it
is a bounded, inspectable tar build-context stream.
* ✨ feat(filter): libpod routing groundwork — version-prefix fix, path predicates, hijack parity (#148) (#189)
* 🐛 fix(filter): strip Podman's three-part semver version prefix (#148)
stripVersionPrefix consumed at most one optional ".N" group, so a
versioned Podman libpod client (which sends its full daemon semver,
e.g. /v5.0.0/) fell through with the version prefix still attached and
could never match a /libpod/**-shaped rule. Add a second optional .N
component; Docker's own vN/vN.N prefixes are unaffected.
- Add consumeOptionalDotDigits helper and apply it twice in
stripVersionPrefix.
- Extend TestStripVersionPrefix, TestStripVersionPrefixMatchesLegacyRegex
(regex oracle widened to {0,2} dot-groups), and TestNormalizePath with
three-part/four-part/adversarial-digit-run cases; add a dedicated
three-way equivalence test for /libpod/, /v1.45/libpod/, and
/v5.0.0/libpod/ all normalizing identically.
- Add a redactDeniedPath regression for a three-part-prefixed denied path.
- Add a NormalizePath bench case and FuzzNormalizePath seeds for the
three-part form.
- Pin internal/metrics's independent stripVersionPrefix already handles
three-part versions correctly (no fix needed there).
- Update the differential package's oracle comments and flip the
three-part-version evasion test case now that it's a valid prefix.
* ✨ feat(filter): add libpod path predicates (#148)
New app/internal/filter/libpod_normalize.go: isLibpodPath plus
per-resource matchers (isLibpodContainerCreatePath,
isLibpodPodCreatePath, isLibpodExecCreatePath, isLibpodExecStartPath,
isLibpodContainerAttachPath, isLibpodPlayKubePath) the later PR2+
inspectors will need. Every matcher is exact-prefix-guarded on
"/libpod/" so it can never fire on a Docker-compat path, and vice
versa — zero-alloc string ops…
Implements #153: Engine API 1.55 coverage validation plus an explicit posture for the Compose/BuildKit transport, per the finalized design.
Transport decision
docker compose builduses the/session+/grpchijacked opaque streams (deprecated in API 1.53, still used by Buildx 0.36.0). These stay denied — they carry secrets, SSH agent forwarding, and arbitrary file sync with no body sockguard can inspect.insecure_accept_opaque_buildkit_tunnels: any allow rule admitting/session,/grpc, or amoby.buildkit.v1.Control/*path fails startup without it. Deliberately separate frominsecure_allow_body_blind_writes(that gate models bounded single-resource writes; these tunnels don't fit that threat model).GRPC=1/SESSION=1compat vars still work (auto-ack) but log a deprecation warning naming the new key, preserving drop-in migration.drydock-with-build.yaml/portwing-with-build.yamlpresets take the classic-builder path (DOCKER_BUILDKIT=0) with docs stating plainly that the BuildKit denial is intentional. In-tunnel gRPC mediation is deferred to v1.7 (epic to be filed).API 1.55 gap closures
Unknown mount types now fail closed;
Mount.Subpathvalidated;TmpfsOptions.Optionsprivilege-gated behindallow_tmpfs_privileged_options; networkEnableIPv4gated bynetwork.allow_disable_ipv4andGwPriorityround-trips;GET /images/{name}/attestations?statement=truedenied by default behindresponse.allow_attestation_statements(conformance test walks all shipped presets);X-Registry-Auth/X-Registry-Configbounded-decoded (8 KiB cap, all base64 variants, fail-closed,serveraddresschecked against the registry allowlist, credentials never logged); new opt-inresponse.redact_host_topology; container-update resource-control field-set completeness test;max-supported-version.txtpin (1.55) + regression test + monthlyquality-api-version-watch.ymlworkflow; h2c-preface integration test proving no HTTP/2 upgrade surface; fuzz-seed corpus extended (/v1.55/,/session,/grpc).SwapBytes/MemorySwappiness intentionally not duplicated here — #152's resource-limit guard owns update-path resource semantics.
Follow-ups to file (not in this PR)
BuildKit gRPC mediation epic (v1.7);
allow_endpoint_confignarrowing; pinned multi-Engine CI matrix (26.1/28.0/29.2/29.7); boundingexec.go's inspect decode.Verification
go build,go test ./...,golangci-lint,go vetall clean; full pre-push hook suite green after rebase onto the merged Wave 1 trunk (#149/#151/#152 logging and config integrations resolved and re-tested).Fixes #153
✨ Added
insecure_accept_opaque_buildkit_tunnelsas an explicit opt-in for/session,/grpc, andmoby.buildkit.v1.Control/*access.POST /buildand keep BuildKit tunnels denied.response.redact_host_topologyandresponse.allow_attestation_statementsconfig flags.request_body.container_create.allow_tmpfs_privileged_optionsandrequest_body.network.allow_disable_ipv4config flags.quality-api-version-watch.ymlto check the pinned Engine API ceiling.🔧 Changed
InsecureAcceptOpaqueBuildkitTunnelswhenGRPC=1orSESSION=1is present.1.55.🐛 Fixed
Mount.Subpathvalues.EnableIPv4: falseunless explicitly allowed.GwPrioritywhen endpoint config is not allowed.X-Registry-AuthandX-Registry-Configheaders.🔒 Security
/infohost-topology fields can now be redacted.POST /images/*/attestations?statement=trueis denied by default.insecure_accept_opaque_buildkit_tunnelsis enabled.EnableIPv4: false,GwPriority, tmpfs privileged options, and attestation statements now require explicit policy opt-in.GRPCandSESSIONenv vars now emit a deprecation warning.