✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - #226
Conversation
- Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 17 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 (9)
📝 WalkthroughWalkthroughThe proxy now mediates Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 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: 10
🧹 Nitpick comments (8)
app/internal/buildkitproxy/framing.go (1)
81-84: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRead once into the frame buffer instead of allocating twice.
The payload is allocated, then copied into a second buffer of the same size. Read straight into the frame and slice the payload.
♻️ Single allocation
- payload = make([]byte, length) - if _, err := io.ReadFull(r, payload); err != nil { + frame = make([]byte, grpcMessageHeaderLen+length) + copy(frame, header[:]) + payload = frame[grpcMessageHeaderLen:] + if _, err := io.ReadFull(r, payload); err != nil { return nil, nil, fmt.Errorf("%w: reading message payload: %w", errUnaryFrameProtocolError, err) }Then drop the trailing reassembly block and
return frame, payload, nil.🤖 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/buildkitproxy/framing.go` around lines 81 - 84, Update the framing read logic around the frame construction to allocate only the complete frame buffer, read the payload directly into its payload region after the gRPC header, and derive the payload slice from that region. Remove the separate payload allocation and copy/reassembly block while preserving the existing header handling and return values.app/internal/buildkitproxy/framing_test.go (1)
140-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse table-driven tests for the new cases.
Both sites add one-case tests. Put each case in a test case table.
app/internal/buildkitproxy/framing_test.go#L140-L153: add a reader factory and expected error substring toTestReadUnaryGRPCMessage.app/internal/config/buildkit_config_test.go#L119-L130: add build flags and expectedSolvePolicyfields to a policy-translation test table.As per coding guidelines,
**/*_test.gorequires table-driven tests;httptest.NewServerapplies when a test is an integration test.🤖 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/buildkitproxy/framing_test.go` around lines 140 - 153, Convert the new trailing-probe error case in app/internal/buildkitproxy/framing_test.go#L140-L153 into a table entry within TestReadUnaryGRPCMessage, adding a reader factory and expected error substring while preserving the existing assertions. Also convert the new policy-translation case in app/internal/config/buildkit_config_test.go#L119-L130 into the existing table, adding its build flags and expected SolvePolicy fields; both sites require direct changes.Source: Coding guidelines
app/internal/config/buildkit_config_test.go (1)
327-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd fuzz coverage for Solve allowlist parsing.
Add a
Fuzz...test forrequest_body.buildkit.control.solveconfiguration parsing. Feed arbitrary cache type, registry, exporter type, and exporter registry values through theLoadboundary. Assert that parsing does not panic and that invalid values fail validation.As per coding guidelines,
app/**/*_test.gorequires fuzz tests for config parsing.🤖 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/config/buildkit_config_test.go` around lines 327 - 359, Add a Fuzz... test alongside the existing Buildkit configuration tests, targeting request_body.buildkit.control.solve parsing through Load. Feed arbitrary cache type, cache registry, exporter type, and exporter registry values via environment-backed configuration, assert Load never panics, and verify invalid values fail validation while valid values are accepted.Source: Coding guidelines
app/internal/buildkitproxy/bridge_controlmediated_test.go (1)
115-137: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winThe ref-cap subtest does not assert the daemon was spared.
Every other denial subtest asserts
daemonCalledstays false. This one usesechoDaemonHandler()and checks only the status code. A regression that registers the ref, denies the client, but still forwards the second Solve to the daemon would pass.Add the same daemon-not-called assertion for the second Solve, or assert
tb.registry.OwnsRef(tb.session.Key, "ref-2")is false.🤖 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/buildkitproxy/bridge_controlmediated_test.go` around lines 115 - 137, The ref-cap test must verify that the denied second Solve is not forwarded to the daemon. In the “ref cap exceeded denies the second solve” subtest, add the existing daemon-called assertion after validating the second response, or verify via tb.registry.OwnsRef that ref-2 was not registered.Source: Path instructions
app/internal/buildkitproxy/bridge.go (1)
400-403: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueAdmitted refs are registered before the daemon accepts the Solve.
PutRefruns first, thenforwardWithBody. If the daemon rejects the Solve (bad definition, unsupported frontend, transport failure), the ref stays inrefOwnersfor the session lifetime. It holds aMaxRefsPerSessionslot and keepsControl/Statusadmitted for a ref no build ever started.Same trust boundary, so this is not an escalation. It does let a client burn the whole 256-slot cap with Solves the daemon refuses, and it makes the cap a weaker DoS bound than the doc comment on
Limits.MaxRefsPerSessionclaims. Consider releasing the ref whenforwardWithBodyends in a transport failure.🤖 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/buildkitproxy/bridge.go` around lines 400 - 403, The Solve path registers the admitted ref via PutRef before forwarding, but failed transport forwarding leaves it retained in refOwners. Update the flow around forwardWithBody to release that ref when forwarding ends with a transport failure, while preserving the admission for successfully accepted Solves and existing audit behavior.app/internal/buildkitproxy/session_test.go (1)
261-312: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd concurrent coverage for
PutRefagainstClose.
tryPutRef's doc comment justifies its locking against "concurrent PutRef call on the same session from another HTTP/2 stream", andClosedeliberately dropsr.mubetween the session delete and the ref release. No test drives either path concurrently, so-racenever exercises the ordering these comments claim to handle.Add a subtest that runs N goroutines calling
reg.PutRef(s, ...)while another callsreg.Close(s.ID), then assertsreg.refOwnersholds no entry for the key afterwards. That assertion is what fails under the leak described onsession.golines 174-208.🤖 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/buildkitproxy/session_test.go` around lines 261 - 312, Add a concurrent subtest to TestSessionRegistryCloseReleasesRefs that starts N goroutines calling PutRef on the same session while another goroutine calls Close(s.ID), wait for all goroutines to finish, and assert refOwners has no entry for the session key. Use synchronization to coordinate completion and exercise the Close/tryPutRef ordering under the race detector.Source: Coding guidelines
app/internal/buildkitproxy/session.go (1)
225-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the exported
Session.PutRef/Session.OwnsRefhelpers.Production callers use
registry.PutRefandregistry.OwnsRef;Session.PutRef/refandSession.OwnsRefonly work ons.Refs, skipMaxRefsPerSession, and do not updaterefOwners/ the identity+profile ownership index. Delete them unless they must remain for tests; if kept, unexport them asputRefLocal/ownsRefLocalwith a tests-only dependency path.🤖 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/buildkitproxy/session.go` around lines 225 - 257, Remove the exported Session.PutRef and Session.OwnsRef helpers and update any remaining callers to use SessionRegistry.PutRef and SessionRegistry.OwnsRef, preserving MaxRefsPerSession and refOwners/index bookkeeping. If tests require local-only behavior, rename them to unexported putRefLocal and ownsRefLocal and keep their use confined to test support.Source: Path instructions
app/internal/buildkitproxy/solve_test.go (1)
33-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a fuzz test for
evaluateSolveRequest.
evaluateSolveRequestis the filter-matching entry point for an untrusted wire payload, and the table covers only well-formed inputs. The coding guidelines require fuzz tests for filter matching.♻️ Proposed addition
func FuzzEvaluateSolveRequest(f *testing.F) { f.Add(malformedPayload) f.Add(mustMarshal(f, &control.SolveRequest{Ref: "r", Frontend: "dockerfile.v0"})) policy := Policy{Control: ControlPolicy{Solve: SolvePolicy{Allow: true}}} f.Fuzz(func(t *testing.T, payload []byte) { req, d := evaluateSolveRequest(payload, policy) if (req == nil) == (d == nil) { t.Fatalf("exactly one of req and denial must be non-nil; req=%v denial=%v", req != nil, d != nil) } }) }
mustMarshaltakes*testing.T; change its parameter totesting.TBso both the table test and the fuzz seed corpus can call it.As per coding guidelines: "Use fuzz tests for filter matching and config parsing."
🤖 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/buildkitproxy/solve_test.go` around lines 33 - 43, Add a FuzzEvaluateSolveRequest fuzz test that seeds malformed and valid marshaled SolveRequest payloads, invokes evaluateSolveRequest with an allowing policy, and asserts exactly one of the returned request or denial is non-nil. Update mustMarshal to accept testing.TB so it can be reused by both regular and fuzz tests.Source: Coding guidelines
🤖 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 `@app/internal/buildkitproxy/bridge_controlmediated_test.go`:
- Around line 34-35: Replace the plain daemonCalled booleans with atomic.Bool at
all five test sites, update the daemon handlers to atomically store true, and
update the assertions to atomically load the value. Add the required atomic
package import and preserve the existing test behavior and failure messages.
In `@app/internal/buildkitproxy/bridge.go`:
- Around line 366-383: Add an explicit default branch to the switch on method in
the mediation logic, assigning d to a permission-denied result for unsupported
mediated methods. Preserve the existing Solve and Status evaluation paths,
ensuring any method admitted by isControlMediatedMethod but missing a switch
case is denied rather than forwarded with nil denial state.
- Around line 392-399: Update the Solve admission path around PutRef to reject
an empty SolveRequest.Ref before registering it, preventing empty refs from
passing later ownership checks; preserve valid-ref registration and existing
resource-exhausted handling. When PutRef denies admission because
MaxRefsPerSession is exceeded, record the distinct audit code
buildkit_ref_limit_exceeded instead of buildkit_policy_denied.
In `@app/internal/buildkitproxy/session.go`:
- Around line 174-208: Make SessionRegistry.Close hold r.mu continuously while
removing the session and decrementing all refs in refOwners, preventing PutRef
from publishing after closure; update app/internal/buildkitproxy/session.go
lines 174-208 accordingly. In app/internal/buildkitproxy/session_test.go lines
261-312, add a concurrent subtest that races PutRef(s, ...) with Close(s.ID) and
asserts refOwners has no entry for the session key afterward.
In `@app/internal/buildkitproxy/solve.go`:
- Around line 339-344: Update checkSolveSourcePolicy to also deny
SolveRequest.SourcePolicySession when it is set, alongside the existing
non-empty inline SourcePolicy.Rules check. Preserve the current
permission-denied response and allow requests only when neither policy source is
present.
- Around line 289-291: Replace the interpolated client-provided type in the
denial returned by the cache-type validation near entry.GetType() with fixed
text, and make the corresponding exporter-type denial near exp.GetType() fixed
as well. Remove the fmt import if it is no longer used, while preserving the
existing permission-denied code and policy keys.
- Around line 118-125: Update isRemoteContextRef to recognize scp-like Git
references in the form <user>@<host>:<path>, while preserving the existing URL
and prefix checks. Add the requested regexp import, use it for this host/path
pattern, and add a solve_test.go case covering bob@example.com:org/repo.git so
checkSolveFrontend enables AllowRemoteContext.
- Around line 174-189: Extend the SolveRequest validation flow around
checkSolveEntitlements, checkSolveFrontend, checkSolveCache,
checkSolveExporters, and checkSolveSourcePolicy to explicitly gate
FrontendInputs, SourcePolicySession, CompatibilityVersion, and ProxyNetwork
before forwarding. Add dedicated deny helpers/checks for these untrusted fields,
or document clear per-field forwarding justifications in the file-level comment
and cover them with tests.
- Around line 318-324: Update the image-export validation around exp.GetType()
to parse attrs["push"] with strconv.ParseBool and deny parse failures as a
policy/schema error; only continue registry validation when the parsed value is
true. Split attrs["name"] on commas and validate every image reference with
registryHostFromImageRef against solvePolicy.AllowedExporterRegistries, denying
if any reference is invalid or disallowed.
In `@app/internal/config/validate.go`:
- Around line 1597-1599: Update normalizeAllowedRegistryHost and the related
allowlist comparison to use registryHostFromImageRef semantics: lowercase
hostnames while preserving ports, such as localhost:5000. Apply the same
normalization to configured cache and exporter registry entries and referenced
image hosts so equivalent values like registry:5000 and Registry:5000 pass
validation.
---
Nitpick comments:
In `@app/internal/buildkitproxy/bridge_controlmediated_test.go`:
- Around line 115-137: The ref-cap test must verify that the denied second Solve
is not forwarded to the daemon. In the “ref cap exceeded denies the second
solve” subtest, add the existing daemon-called assertion after validating the
second response, or verify via tb.registry.OwnsRef that ref-2 was not
registered.
In `@app/internal/buildkitproxy/bridge.go`:
- Around line 400-403: The Solve path registers the admitted ref via PutRef
before forwarding, but failed transport forwarding leaves it retained in
refOwners. Update the flow around forwardWithBody to release that ref when
forwarding ends with a transport failure, while preserving the admission for
successfully accepted Solves and existing audit behavior.
In `@app/internal/buildkitproxy/framing_test.go`:
- Around line 140-153: Convert the new trailing-probe error case in
app/internal/buildkitproxy/framing_test.go#L140-L153 into a table entry within
TestReadUnaryGRPCMessage, adding a reader factory and expected error substring
while preserving the existing assertions. Also convert the new
policy-translation case in app/internal/config/buildkit_config_test.go#L119-L130
into the existing table, adding its build flags and expected SolvePolicy fields;
both sites require direct changes.
In `@app/internal/buildkitproxy/framing.go`:
- Around line 81-84: Update the framing read logic around the frame construction
to allocate only the complete frame buffer, read the payload directly into its
payload region after the gRPC header, and derive the payload slice from that
region. Remove the separate payload allocation and copy/reassembly block while
preserving the existing header handling and return values.
In `@app/internal/buildkitproxy/session_test.go`:
- Around line 261-312: Add a concurrent subtest to
TestSessionRegistryCloseReleasesRefs that starts N goroutines calling PutRef on
the same session while another goroutine calls Close(s.ID), wait for all
goroutines to finish, and assert refOwners has no entry for the session key. Use
synchronization to coordinate completion and exercise the Close/tryPutRef
ordering under the race detector.
In `@app/internal/buildkitproxy/session.go`:
- Around line 225-257: Remove the exported Session.PutRef and Session.OwnsRef
helpers and update any remaining callers to use SessionRegistry.PutRef and
SessionRegistry.OwnsRef, preserving MaxRefsPerSession and refOwners/index
bookkeeping. If tests require local-only behavior, rename them to unexported
putRefLocal and ownsRefLocal and keep their use confined to test support.
In `@app/internal/buildkitproxy/solve_test.go`:
- Around line 33-43: Add a FuzzEvaluateSolveRequest fuzz test that seeds
malformed and valid marshaled SolveRequest payloads, invokes
evaluateSolveRequest with an allowing policy, and asserts exactly one of the
returned request or denial is non-nil. Update mustMarshal to accept testing.TB
so it can be reused by both regular and fuzz tests.
In `@app/internal/config/buildkit_config_test.go`:
- Around line 327-359: Add a Fuzz... test alongside the existing Buildkit
configuration tests, targeting request_body.buildkit.control.solve parsing
through Load. Feed arbitrary cache type, cache registry, exporter type, and
exporter registry values via environment-backed configuration, assert Load never
panics, and verify invalid values fail validation while valid values are
accepted.
🪄 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: 4a25306f-8adb-4af8-80cd-677ac88422ab
⛔ Files ignored due to path filters (1)
CHANGELOG.mdis excluded by!CHANGELOG.md
📒 Files selected for processing (23)
app/internal/buildkitproxy/bridge.goapp/internal/buildkitproxy/bridge_controlmediated_test.goapp/internal/buildkitproxy/bridge_test.goapp/internal/buildkitproxy/framing.goapp/internal/buildkitproxy/framing_test.goapp/internal/buildkitproxy/grpcstatus.goapp/internal/buildkitproxy/limits.goapp/internal/buildkitproxy/mediator.goapp/internal/buildkitproxy/mediator_test.goapp/internal/buildkitproxy/policy.goapp/internal/buildkitproxy/protowalk.goapp/internal/buildkitproxy/protowalk_test.goapp/internal/buildkitproxy/session.goapp/internal/buildkitproxy/session_test.goapp/internal/buildkitproxy/solve.goapp/internal/buildkitproxy/solve_test.goapp/internal/cmd/rules.goapp/internal/cmd/serve.goapp/internal/config/buildkit_config_test.goapp/internal/config/buildkit_options.goapp/internal/config/config.goapp/internal/config/filter_options.goapp/internal/config/validate.go
…tion - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy.
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events * fix(website): remove Go Report Card badge Remove the Go Report Card badge and CSP allowance from the website. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events * fix(website): remove Go Report Card badge Remove the Go Report Card badge and CSP allowance from the website. * ci: migrate to reusable workflows with temporary bridges (#258) * ci: migrate to reusable workflows with temporary bridges Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so lint/test/release logic is shared with the rest of the org. Reusable check runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which doesn't match the plain context names the branch-protection ruleset currently requires, so seven "legacy-*" bridge jobs mirror those exact names, fail-closed on the reusable call's result, until the ruleset itself is migrated separately to require the new reusable contexts. CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs javascript-typescript coverage the central workflow doesn't provide); so do dependency-review and commit-message, which aren't part of the required context set. The qlty OIDC coverage-upload step is dropped rather than routed through go-ci's qlty job, matching portwing's precedent. Go Lint and Go Test now additionally run on the weekly schedule trigger since go-ci has no per-job schedule gate — an accepted strict superset of prior behavior. * fix(ci): close a file-system race in the reusable-workflow contract test assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync and then re-opened it by path with readFileSync — a TOCTOU window where the file on disk could change between the check and the read (CodeQL js/file-system-race, alert #278). Open the script once and reuse that file descriptor for both the executable-bit stat and the content read. * fix(ci): address CodeRabbit findings on the reusable-workflow migration - fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test, build-workspaces) with `github.event_name != 'schedule'`, matching node-ci's own schedule skip, so scheduled runs don't fail these always()-gated bridges closed - test(ci): update the bridge contract test to require the schedule guard on the node-ci bridges while keeping the go-ci bridges on plain always() - chore(lefthook): drop the obsolete `command -v goreleaser` skip guard from goreleaser-snapshot; the adapter runs goreleaser via `go run` with a pinned module and never needs the global binary - fix(ci): tighten the quarantine-removal assertion in go-release-check.sh to match the exact generated Cask command instead of a loose substring --------- Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events * fix(website): remove Go Report Card badge Remove the Go Report Card badge and CSP allowance from the website. * ci: migrate to reusable workflows with temporary bridges (#258) * ci: migrate to reusable workflows with temporary bridges Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so lint/test/release logic is shared with the rest of the org. Reusable check runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which doesn't match the plain context names the branch-protection ruleset currently requires, so seven "legacy-*" bridge jobs mirror those exact names, fail-closed on the reusable call's result, until the ruleset itself is migrated separately to require the new reusable contexts. CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs javascript-typescript coverage the central workflow doesn't provide); so do dependency-review and commit-message, which aren't part of the required context set. The qlty OIDC coverage-upload step is dropped rather than routed through go-ci's qlty job, matching portwing's precedent. Go Lint and Go Test now additionally run on the weekly schedule trigger since go-ci has no per-job schedule gate — an accepted strict superset of prior behavior. * fix(ci): close a file-system race in the reusable-workflow contract test assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync and then re-opened it by path with readFileSync — a TOCTOU window where the file on disk could change between the check and the read (CodeQL js/file-system-race, alert #278). Open the script once and reuse that file descriptor for both the executable-bit stat and the content read. * fix(ci): address CodeRabbit findings on the reusable-workflow migration - fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test, build-workspaces) with `github.event_name != 'schedule'`, matching node-ci's own schedule skip, so scheduled runs don't fail these always()-gated bridges closed - test(ci): update the bridge contract test to require the schedule guard on the node-ci bridges while keeping the go-ci bridges on plain always() - chore(lefthook): drop the obsolete `command -v goreleaser` skip guard from goreleaser-snapshot; the adapter runs goreleaser via `go run` with a pinned module and never needs the global binary - fix(ci): tighten the quarantine-removal assertion in go-release-check.sh to match the exact generated Cask command instead of a loose substring --------- Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261) PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host straight off event properties and silently drops the event with a cookieless_missing_user_agent/cookieless_missing_host warning if either is absent. before_send's allowlist rebuild stripped both, so every event was being dropped at ingestion. Require and forward both fields (and only those two); $ip stays server-filled by the PostHog proxy. docs/ carries the same change via the existing analytics-runtime sync. * ci: remove temporary status bridges (#260) * ci: remove temporary status bridges The main branch-protection ruleset now requires the reusable "Go CI / ..." and "Node CI / ..." contexts directly, so the seven legacy-* jobs that mirrored the old plain context names are no longer needed. Updates the contract tests to assert the bridges stay removed and pins the ruleset's nine required contexts. * test(ci): detect unquoted retired context names --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events * fix(website): remove Go Report Card badge Remove the Go Report Card badge and CSP allowance from the website. * ci: migrate to reusable workflows with temporary bridges (#258) * ci: migrate to reusable workflows with temporary bridges Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so lint/test/release logic is shared with the rest of the org. Reusable check runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which doesn't match the plain context names the branch-protection ruleset currently requires, so seven "legacy-*" bridge jobs mirror those exact names, fail-closed on the reusable call's result, until the ruleset itself is migrated separately to require the new reusable contexts. CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs javascript-typescript coverage the central workflow doesn't provide); so do dependency-review and commit-message, which aren't part of the required context set. The qlty OIDC coverage-upload step is dropped rather than routed through go-ci's qlty job, matching portwing's precedent. Go Lint and Go Test now additionally run on the weekly schedule trigger since go-ci has no per-job schedule gate — an accepted strict superset of prior behavior. * fix(ci): close a file-system race in the reusable-workflow contract test assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync and then re-opened it by path with readFileSync — a TOCTOU window where the file on disk could change between the check and the read (CodeQL js/file-system-race, alert #278). Open the script once and reuse that file descriptor for both the executable-bit stat and the content read. * fix(ci): address CodeRabbit findings on the reusable-workflow migration - fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test, build-workspaces) with `github.event_name != 'schedule'`, matching node-ci's own schedule skip, so scheduled runs don't fail these always()-gated bridges closed - test(ci): update the bridge contract test to require the schedule guard on the node-ci bridges while keeping the go-ci bridges on plain always() - chore(lefthook): drop the obsolete `command -v goreleaser` skip guard from goreleaser-snapshot; the adapter runs goreleaser via `go run` with a pinned module and never needs the global binary - fix(ci): tighten the quarantine-removal assertion in go-release-check.sh to match the exact generated Cask command instead of a loose substring --------- Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261) PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host straight off event properties and silently drops the event with a cookieless_missing_user_agent/cookieless_missing_host warning if either is absent. before_send's allowlist rebuild stripped both, so every event was being dropped at ingestion. Require and forward both fields (and only those two); $ip stays server-filled by the PostHog proxy. docs/ carries the same change via the existing analytics-runtime sync. * ci: remove temporary status bridges (#260) * ci: remove temporary status bridges The main branch-protection ruleset now requires the reusable "Go CI / ..." and "Node CI / ..." contexts directly, so the seven legacy-* jobs that mirrored the old plain context names are no longer needed. Updates the contract tests to assert the bridges stay removed and pins the ruleset's nine required contexts. * test(ci): detect unquoted retired context names * docs(readme): add warpchart growth chart (#263) * docs(readme): add warpchart growth chart * docs(readme): keep warpchart chart inside centering container - docs(readme): inline HTML anchor/img so the chart stays in the centered div - docs(changelog): record the README addition --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
… main (#268) * ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events * fix(website): remove Go Report Card badge Remove the Go Report Card badge and CSP allowance from the website. * ci: migrate to reusable workflows with temporary bridges (#258) * ci: migrate to reusable workflows with temporary bridges Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so lint/test/release logic is shared with the rest of the org. Reusable check runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which doesn't match the plain context names the branch-protection ruleset currently requires, so seven "legacy-*" bridge jobs mirror those exact names, fail-closed on the reusable call's result, until the ruleset itself is migrated separately to require the new reusable contexts. CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs javascript-typescript coverage the central workflow doesn't provide); so do dependency-review and commit-message, which aren't part of the required context set. The qlty OIDC coverage-upload step is dropped rather than routed through go-ci's qlty job, matching portwing's precedent. Go Lint and Go Test now additionally run on the weekly schedule trigger since go-ci has no per-job schedule gate — an accepted strict superset of prior behavior. * fix(ci): close a file-system race in the reusable-workflow contract test assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync and then re-opened it by path with readFileSync — a TOCTOU window where the file on disk could change between the check and the read (CodeQL js/file-system-race, alert #278). Open the script once and reuse that file descriptor for both the executable-bit stat and the content read. * fix(ci): address CodeRabbit findings on the reusable-workflow migration - fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test, build-workspaces) with `github.event_name != 'schedule'`, matching node-ci's own schedule skip, so scheduled runs don't fail these always()-gated bridges closed - test(ci): update the bridge contract test to require the schedule guard on the node-ci bridges while keeping the go-ci bridges on plain always() - chore(lefthook): drop the obsolete `command -v goreleaser` skip guard from goreleaser-snapshot; the adapter runs goreleaser via `go run` with a pinned module and never needs the global binary - fix(ci): tighten the quarantine-removal assertion in go-release-check.sh to match the exact generated Cask command instead of a loose substring --------- Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261) PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host straight off event properties and silently drops the event with a cookieless_missing_user_agent/cookieless_missing_host warning if either is absent. before_send's allowlist rebuild stripped both, so every event was being dropped at ingestion. Require and forward both fields (and only those two); $ip stays server-filled by the PostHog proxy. docs/ carries the same change via the existing analytics-runtime sync. * ci: remove temporary status bridges (#260) * ci: remove temporary status bridges The main branch-protection ruleset now requires the reusable "Go CI / ..." and "Node CI / ..." contexts directly, so the seven legacy-* jobs that mirrored the old plain context names are no longer needed. Updates the contract tests to assert the bridges stay removed and pins the ruleset's nine required contexts. * test(ci): detect unquoted retired context names * docs(readme): add warpchart growth chart (#263) * docs(readme): add warpchart growth chart * docs(readme): keep warpchart chart inside centering container - docs(readme): inline HTML anchor/img so the chart stays in the centered div - docs(changelog): record the README addition * fix(deps): update golang.org/x/mod to v0.40.0 (#265) Clears OpenSSF Scorecard alert #8's two actionable findings, GO-2026-6179 and GO-2026-6180 (sumdb transparency-log verification bypasses), fixed in x/mod v0.40.0. The third finding, GO-2026-5932 (unmaintained x/crypto/openpgp), has no fixed version and is already a documented, narrowly-scoped exception in .grype.yaml. govulncheck reports none of the three as reachable; this is a manifest-level cleanup. * docs(changelog): backfill unreleased entries for CI migration, analytics, and bench fix (#266) Three merged, changelog-worthy changes were missing from [Unreleased]: the reusable-workflow CI migration (#258/#260), cookieless PostHog analytics with its ingestion-property fix (#253/#261), and the monthly benchmark ReadMemStats stall fix (#243). Surfaced by a repo audit; CLAUDE.md wants changelog updates atomic with each change, so this closes the gap in one pass. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
* ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223)
* ✨ feat(filter): per-field endpoint-config gates on network connect
request_body.network.allow_endpoint_config previously admitted Docker's
whole EndpointSettings object as one opt-in. New EndpointConfigOptions
gates each Engine API 1.55-era field independently (static addressing,
link-local IPs, MAC pinning, GwPriority, aliases) on both
POST /networks/*/connect and POST /containers/create's
NetworkingConfig.EndpointsConfig, so operators can allow benign
settings like Aliases without also allowing address pinning.
- denyEndpointConfigReason now checks the legacy flag first (unchanged
behavior when true), then each granular field in turn, naming the
specific field in the denial reason
- endpointHasStaticIPConfig split into endpointHasStaticAddressFields
and endpointHasLinkLocalIPs so static-IP and link-local gating can
vary independently, without changing endpointHasStaticIPConfig's own
behavior (mutation_kill_test.go pins it)
- denyRootMacAddressReason respects AllowMACPinning independent of the
legacy flag
- Links and DriverOpts keep no granular escape hatch — still denied
without the legacy allow_endpoint_config flag
- libpod_network.go doc comment notes there's no libpod-native
network-connect endpoint to gate
Refs: #186
* 🧪 test(filter): cover per-field endpoint-config precedence
Table-driven coverage for #186: granular allows each field
individually and still denies the others, static-addressing and
link-local-IPs gate independently of each other, Links/DriverOpts stay
denied even with every other granular field allowed, root MacAddress
respects AllowMACPinning independent of the legacy flag, and the
legacy allow_endpoint_config: true flag takes precedence over a
granular block that would otherwise deny.
* ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion
Adds request_body.network.endpoint_config (and its libpod_network
mirror, kept for default-posture symmetry though never consulted):
allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and
allow_gw_priority default false; allow_aliases defaults true to
reproduce allow_endpoint_config's historical unconditional-allow
behavior for Compose aliases.
- ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions'
DenyAliases so ~15 pre-existing filter-package tests that construct
NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep
seeing aliases allowed, while the config layer's documented default
stays true
- Cross-wired into ContainerCreateOptions.EndpointConfig alongside the
existing AllowEndpointConfig cross-wire, matching the established
"network config, not duplicated under container_create" pattern
- New Config.explicitNetworkEndpointConfig field + explicitKeysSet
helper (generalized from the #149 explicitLegacyListenSet pattern)
track whether the endpoint_config block was actually set via YAML or
a SOCKGUARD_* env var, since its merged value is never the Go zero
value (AllowAliases defaults true) and so can't be detected by
zero-value comparison
- validateNetworkEndpointConfig rejects allow_endpoint_config: true set
together with an explicit endpoint_config block: the legacy flag
already admits every field the granular block could, so the two are
mutually exclusive by design (fail-closed on ambiguous intent)
Refs: #186
* 🧪 test(config): cover endpoint_config defaults, env vars, and validation
- filter_options_test.go: extends the existing full-policy mapping
test with EndpointConfig, plus a dedicated test proving the granular
block cross-wires into ContainerCreateOptions
- network_endpoint_config_test.go (new): Defaults() has AllowAliases
true and every other granular field false; all 5 new
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are
honored; setting allow_endpoint_config: true together with an
explicit endpoint_config block is a validation error whether set via
YAML or environment variables; the legacy flag alone and the
granular block alone each validate cleanly; a pure-defaults config
is never flagged as having an explicit endpoint_config block despite
AllowAliases defaulting true
* 📝 docs(network): document endpoint_config granular gates (#186)
- configuration.mdx: endpoint_config field mapping/precedence prose,
updated container_create/network policy table rows, and 5 new
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows
- security.mdx: mentions the granular alternative alongside the
existing allow_endpoint_config description, notes Links/DriverOpts
have no per-field escape hatch
- migration.mdx: new note under the v1.4->v1.5 endpoint-config item
explaining the granular block is opt-in and backward compatible,
and that setting both forms is a validation error
- CHANGELOG.md: new Unreleased section with the full feature writeup
- README.md, website/src/lib/site-content.ts: roadmap bullet updated
to reflect the granular gates are implemented ahead of the v1.7.0
BuildKit-mediation epic
* 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes
Comment-only change to the drydock/drydock-with-compose/drydock-with-build
presets' existing allow_endpoint_config macvlan/static-IP callout,
pointing at the new per-field endpoint_config gates (#186) as an
alternative to the all-or-nothing flag. Comments are exempt from
compose_example_sync_test.go's structural (parsed policy) comparison,
so this doesn't require touching the example compose files.
* 🧪 test(filter): fuzz per-field endpoint-config gates
Add FuzzNetworkConnectEndpointConfigGates and
FuzzContainerCreateEndpointConfigGates, generating network-connect /
container-create endpoint payloads plus granular EndpointConfigOptions
combinations and asserting the fail-closed invariant CodeRabbit flagged
on #223: with AllowEndpointConfig off, a field whose own granular gate
is also off must produce a non-empty denial reason, and Links/DriverOpts
(no granular gate at all) must always be denied.
* 🧪 test(config): fuzz endpoint_config YAML parsing
Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the
#186 request_body.network.endpoint_config block (empty block, a single
granular field, and the allow_endpoint_config/endpoint_config mutual-
exclusion combination) and fuzzing LoadBytes then Validate on success —
addresses a CodeRabbit finding on #223 that config parsing for the new
block had no fuzz coverage.
* 📝 docs(presets): correct endpoint_config wording in drydock comments
The network-policy comment block in drydock.yaml / drydock-with-compose.yaml
/ drydock-with-build.yaml described endpoint-config policy as legacy-flag-only,
which was already stale against #186: Aliases default to allowed but can be
denied via endpoint_config.allow_aliases: false, and static IP addressing
and MAC pinning each have their own granular gate now. Only Links and
DriverOpts remain ungated and still require allow_endpoint_config: true.
* 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts
The #186 granular endpoint_config migration note had landed inside the
"From Sockguard v1.4 to v1.5" section even though the feature ships in
v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add
the missing caveat that Links and DriverOpts have no granular gate on
either network connect or container-create: workflows that need either
must keep allow_endpoint_config: true and accept whole-object access.
* 📝 docs(readme): correct endpoint_config narrowing description
README.md and site-content.ts's v1.7.0 roadmap item both said
allow_endpoint_config "has already been narrowed to per-field gates" /
implied network-connect-only scope. Correct both to describe what #186
actually ships: request_body.network.endpoint_config.* provides the
per-field gates, covering endpoint settings on both network connect and
container-create, while allow_endpoint_config: true remains the broad
whole-object override.
* 📝 docs(presets): name all five granular endpoint gates in preset comments
* ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224)
* ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation
Part of issue #185's phase 1 (schema + policy foundation). Vendors a
curated subset of BuildKit v0.32.0's protobuf schema under
internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/
ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory
kept as bare method names only), the full LLB op graph, session auth/
secrets/ssh/filesync/upload, and the gRPC health service — generated via
a pinned, reproducible buf+protoc-gen-go pipeline
(scripts/generate-buildkit-proto.sh) with no protoc or grpc-go
dependency anywhere in the chain.
- Every vendored file's go_package is retargeted into this module so
generated imports resolve without requiring github.com/moby/buildkit
as an actual Go dependency.
- PROVENANCE.md records source URLs, upstream tags, curation notes, and
sha256 checksums for every vendored file, plus what was deliberately
left out (LLBBridge, Exporter, PolicyVerifier, containerd content,
OTLP trace) and why.
- provenance_test.go golden-tests the vendored tree against the
manifest in both directions: any drift in a listed file's checksum,
or any unlisted .proto file on disk, fails the build.
Package is internal, protobuf-only, and never imports grpc-go — see
doc.go.
* ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types
Part of issue #185's phase 1. internal/buildkitproxy holds the
committed disposition table (mediate / passthrough / deny) for every
gRPC method either opaque tunnel endpoint (POST /grpc's
moby.buildkit.v1.Control, POST /session's callback services) can carry,
matching the #185 synthesis inventory exactly: Control's Solve/Status
are mediate, Info/ListWorkers are passthrough, everything else on
Control has no enabling knob at all. Classify(endpoint, service,
method) denies anything not explicitly listed — LLBBridge, nested
Control/Session, containerd content, OTLP trace, Exporter,
PolicyVerifier, and FileSync/TarStream are all captured in
DeniedExamples and asserted deny.
policy.go defines the runtime-facing Policy type (Control/Session
sub-policies with per-RPC Allow flags and allowlists) that
internal/config's BuildkitRequestBodyConfig.ToPolicy will translate
into — buildkitproxy never imports config, so translation only flows
one direction, same convention internal/filter's options types use.
registry_test.go asserts every registry entry and DeniedExamples entry
classifies as expected plus unknown-method deny; descriptor_manifest_test.go
cross-checks both against the real vendored FileDescriptors from
internal/buildkitproto via protoreflect, so the registry can't silently
drift from the schema it's meant to describe.
* ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate
Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's
existing config/filter/startup-validation machinery, with no change in
runtime behavior for any endpoint sockguard currently allows.
- config.go: new nested request_body.buildkit block (control.{allow_info,
allow_list_workers,allow_status,solve.allow} and session.{health,auth,
secrets,ssh,file_sync,file_send,upload}), available at the top level and
per client profile like every other request_body.* surface. Presence,
not an enabled flag, is what matters — the Go zero value denies
everything, identical to a config that never mentions "buildkit:".
- buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into
buildkitproxy.Policy, one direction only (buildkitproxy never imports
config), mirroring RequestBodyConfig.ToFilterOptions.
- validate.go: validateBuildkitConfig checks the allowlist fields
(registry hosts via the existing normalizer; realms/scopes/IDs for
non-empty, unpadded entries). validateBuildkitAckMutualExclusion
rejects insecure_accept_opaque_buildkit_tunnels=true combined with a
configured request_body.buildkit block, top-level or per profile —
mediation supersedes the wholesale ack.
- filter/buildkit.go + middleware.go: a new critical-severity inspector
denies POST /session and POST /grpc unconditionally whenever
request_body.buildkit is configured, since phase 1 ships no mediator to
act on the richer policy yet — configuring the block must never look
like it opened an opaque pass-through.
- cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a
/session or /grpc rule at startup when request_body.buildkit is
configured (previously only the insecure ack satisfied this), with the
admission/runtime split documented explicitly at the call site and
pointing at filter.buildkitPolicy.inspect for the actual guarantee.
* 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion
Table-driven coverage for issue #185 phase 1's request_body.buildkit
surface: zero-value defaults deny everything and pass Validate;
ToPolicy() translates every field into buildkitproxy.Policy correctly;
every allowlist validator (allowed_registries, allowed_realms,
allowed_scopes, allowed_ids) rejects malformed entries with a
dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual
exclusion is checked both top-level and per client profile, in both
directions (rejected together, accepted alone); and
SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing
Viper reflection-based default registration for both a nested bool
(control.allow_info) and a nested string-slice
(session.secrets.allowed_ids), confirming no map/slice caveat blocks
env override for this config surface.
Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML
(well-formed, mistyped fields, the ack/mediation conflict, whitespace/
empty allowlist entries, unicode) so mutation spends its budget
exploring the new schema's key space specifically, per the repo
convention that config parsing gets fuzz coverage. Ran a 15s local fuzz
pass with no crashes.
* 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation
Unreleased entry for issue #185 phase 1: the vendored protobuf subset,
the method classification registry, and the request_body.buildkit
config surface, with an explicit note that runtime mediation itself
(and any change in what /session and /grpc actually do) lands in later
phases of the epic.
* 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency
internal/buildkitproto's generated code imports google.golang.org/protobuf
directly (issue #185 phase 1) rather than pulling it in transitively
through another dependency, so go mod tidy correctly reclassifies it
from indirect to direct in go.mod. No other dependency changed; go.sum
is unaffected.
* 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate
Verified locally: production total with the exclusion is 96.1% (vs 80.7%
with 9.5k generated protobuf statements counted); the hand-written
buildkitproxy package stays in the gate.
* 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1
- 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector
- 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count)
- 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test
The ops.proto trimmed-provenance finding was refuted: the vendored file is
byte-identical to upstream v0.32.0 except the documented go_package retarget
(upstream sha256 matches PROVENANCE.md's recorded value).
* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225)
* ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing
* ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams
Adds the real BuildKit gRPC mediator on top of phase 1's classification
registry. bridge.go/upgrade.go/mediator.go terminate the hijacked
POST /session and POST /grpc connections as h2c using
golang.org/x/net/http2 (Server.ServeConn against the Docker client,
Transport{AllowHTTP:true} dialing the daemon) and bridge them at the
http.Handler level — one gRPC stream per *http.Request, original bytes
preserved verbatim (no protobuf re-encode; that's phases 3-5).
POST /session keeps buildkitd's reversed client/server roles, wired
through a single symmetric bridgeLegs abstraction shared with /grpc.
Every stream is classified through Classify() before being forwarded;
a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays
open. limits.go adds defense-in-depth DoS caps (max concurrent
streams, a denied-stream budget over a rolling window) — numeric
defaults are sockguard's own judgment call, not specified by the #185
synthesis. session.go adds a session registry keyed by client identity
+ profile, never the client-supplied UUID alone (recorded only as
advisory metadata). Any genuine bridge/transport error tears the whole
tunnel down; a Deny or size-cap trip does not.
* 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed
Deviation from the literal task text ("Passthrough and Mediate both
forward in Phase 2"): forwarding a method's *category* isn't enough on
its own, or configuring any single request_body.buildkit field would
silently forward every other Mediate/Passthrough method too, defeating
the point of the granular per-field switches. policy.go adds
Policy.Allowed(endpoint, service, method) as a second, necessary gate
consulted alongside Classify() in bridge.go's handleStream — audited
under a distinct buildkit_policy_denied reason code, separate from
Classify's own buildkit_method_denied.
* 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright
filter/buildkit.go's inspect() no longer unconditionally denies
POST /session and POST /grpc once request_body.buildkit is configured
— those now pass through to internal/buildkitproxy's mediator
(wired into the hijack tier via cmd/serve.go's new
withBuildkitMediator layer). The literal
/moby.buildkit.v1.Control/<Method> probe path stays hard-denied
regardless of the method's own classification: it carries no h2c
upgrade, so there's no tunnel for any mediator to terminate.
* 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge
net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive
integration-style tests of the bridge/mediator without mocking http2
internals. A small clientLegConn interface on bridge (satisfied
structurally by *http2.ClientConn) lets forward()'s harder-to-reach
error branches — a genuine RoundTrip failure vs. a response-body copy
failure vs. the request size-cap trip — be exercised deterministically
via a hand-rolled fake instead of racing real connection teardown.
internal/buildkitproxy is at 100% statement coverage.
* 📦 deps(go.mod): promote golang.org/x/net to a direct dependency
Approved exception for the mediation path only (golang.org/x/net/http2).
No grpc-go anywhere in the chain; google.golang.org/protobuf was
already direct.
* 📝 docs(changelog): document phase 2
Fixes: #185
* 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge
- 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter
- 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy
- 🔒 validate the daemon's 101 response headers before bridging
- 🧪 convert standalone buildkitproxy tests to table-driven form
- 📝 correct the stale opaque-pass-through comment in cmd/rules.go
* 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter
A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic
in limitedReadCloser.Read wrap negative and panic on the slice; compare
by subtraction instead and pin it with a table row.
* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226)
* ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership
- Control/Solve and Control/Status now get per-message gRPC decode and
policy mediation instead of phase 2's blanket byte-verbatim relay:
entitlements, frontend/frontend-attrs, cache import/export, and
exporter checks against new request_body.buildkit.control.solve
allowlists, with a strict protobuf-reflection unknown-field walk
denying anything the vendored schema doesn't recognize.
- Beyond the checklist: deprecated singular cache-ref fields,
EnableSessionExporter, and non-empty SourcePolicy are denied outright
with no enabling knob — each would otherwise let a structurally
"known" field bypass the allowlists above or reach other hard-denied
surface.
- Session registry gains a ref-ownership index keyed by client identity
+ profile (never a client-supplied ref alone); Status denies
buildkit_ref_not_owned unless its ref was registered by an admitted
Solve from the same identity+profile, refs release on session close,
and a per-session cap bounds registration.
- On admission the client's original frame bytes forward untouched —
no re-encoding. internal/buildkitproxy stays at 100% statement
coverage; repo-wide coverage holds at 96.2%.
* 🎨 style(buildkit): gofmt solve_test.go
* 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation
- checkSolveExporters now parses "push" with strconv.ParseBool (matching
BuildKit's own image exporter) instead of a bare == "true" check, denies
values it can't evaluate, and validates every comma-separated ref in
"name" against the exporter registry allowlist instead of just the whole
string
- SourcePolicySession and every other previously-unexamined SolveRequest
field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal,
Session, Definition) now has an explicit disposition: denied outright,
forwarded with a documented justification, or checked — see solve.go's
header comment for the full field-by-field audit
- fixed a SessionRegistry.PutRef/Close race that could let a ref survive
its session's teardown with no live owner, leaving OwnsRef permanently
true for that client identity/profile; added a concurrent regression test
- isRemoteContextRef now detects scp-like git remotes (bob@host:path, no
"://") the same way BuildKit's own gitutil does, closing a remote-context
policy bypass for that syntax
- forwardControlMediated's method switch fails closed (Internal status,
audited) on an unrecognized method instead of forwarding with zero policy
evaluation if it and isControlMediatedMethod ever drift
- SolveRequest.Ref == "" is now rejected before ref registration, closing a
path where an empty ref could pass Control/Status's ownership check for
free; the per-session ref-cap denial gets its own audit reason
(buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied
- denial messages no longer echo client-supplied cache/exporter type
strings into the gRPC status message
- buildkitproxy's registry-host allowlists (allowed_cache_registries,
allowed_exporter_registries) are now normalized to the same lowercase/
canonical form registryHostFromImageRef compares against, so a
mixed-case or index.docker.io config entry actually matches at runtime
- bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool,
fixing a data race between the daemon handler goroutine and the test
goroutine
Refuted: reordering isRemoteContextRef's http:// prefix check and
suggesting a normalizeAllowedRegistryHost signature change were not part of
the findings as filed; normalizeAllowedRegistryHost itself already
lowercases and canonicalizes correctly — the bug was that its output was
never applied to the config values ToPolicy hands to buildkitproxy.
* ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227)
* ✨ feat(buildkit): #185 phase 4 — credential session mediation
- moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken,
GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and
policy mediation instead of phase 2's blanket byte-verbatim relay: every
RPC's Host is checked against request_body.buildkit.session.auth's
already-existing allowed_registries (now actually consumed for the first
time, and normalized the same lowercase/index.docker.io->docker.io way as
Solve's registry allowlists so config-side case can't silently fail to
match); FetchToken additionally requires its Realm to match allowed_realms
and every requested scope to be a member of allowed_scopes, since a
registry's token realm can live on a different host than the registry
itself and the registry-host allowlist alone can't bound where a resulting
bearer token is minted for.
- moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact
allowed_ids match; a non-empty annotations map is denied outright —
researched against BuildKit v0.32.0's own secret request/response path,
which never populates or reads that field, so there's no reviewed
semantics to admit instead of silently forwarding it.
- moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids.
ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no
unary message to decode) is gated on the agent ID carried in gRPC metadata
(buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is
relayed; the stream itself is never decoded, buffered as a message, or
logged in either direction.
- Every admitted RPC forwards the client's original frame bytes verbatim,
exactly like phase 3's Solve/Status; every response — which for these RPCs
carries the actual credential/secret/token content — is never decoded on
any path.
- A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512)
throttles Auth/Secrets/SSH call volume independent of the connection-wide
denied-stream abuse budget, since an admitted call never trips that
budget; exceeding it denies RESOURCE_EXHAUSTED/
buildkit_credential_call_limit_exceeded without counting against the abuse
budget.
- Audit events for these RPCs carry the registry host in the clear
(low-cardinality, operator-facing) but only a truncated SHA-256 of secret/
SSH-agent IDs, never the raw identifier.
- FileSync/FileSend/Upload are untouched (phase 5's scope). internal/
buildkitproxy stays at 100% statement coverage; repo-wide production
coverage holds at 96.3%.
* 🔒 security(buildkit): log normalized registry host in auth audit events
normalizeAuthHost trims surrounding whitespace before the allowlist
comparison, so a Host like "ghcr.io\n" is admitted while the raw field —
CR/LF included — reached the audit log's registry_host attr, letting an
admitted client forge audit log lines. Log the normalized host (the value
policy actually compared) wrapped in logging.SafeString, matching every
other attr audit emits. Regression test asserts an admitted
whitespace-padded host audits the canonical form through a live bridge.
* 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators
Real-world secret/SSH IDs are short guessable operator labels ("default",
"npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline
dictionary in milliseconds, leaking the very identifier shortHash exists
to withhold. A per-process crypto/rand salt keeps same-ID correlation
within one process while breaking dictionary recovery, with a regression
test pinning the salted-vs-unsalted difference. Also adds
FuzzEvaluateGetSecretRequest over the credential decode/match surface
(never panic, never admit outside policy), widening mustMarshal to
testing.TB for fuzz seeding.
* ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228)
Mediates the three remaining byte-verbatim streaming Mediate methods
(moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy,
moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a
new streaming primitive (streamRelayReader/relayValidatedFrames) that
decodes each gRPC length-prefixed frame, validates it, and forwards the
original bytes verbatim on admission.
FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets
structural validation only; response direction (PACKET_STAT/DATA) gets
path-traversal/absolute-path/NUL checks on every Stat path and symlink
target, plus file-count/per-file-byte/total-byte/path-length caps. The
"dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA
is buffered independently by its fsutil-assigned ID (never per-stream —
fsutil's real sender interleaves multiple files across four parallel
workers) until that file's own EOF, then the same RUN-instruction/
syntax-frontend inspection the classic /build path uses runs against the
reassembled bytes before release; a denial discards the held bytes
without releasing anything. Holding is skipped when allow_run_instructions
is already set.
FileSend/DiffCopy requires an admitted Control/Solve from the same
SessionKey and gets a pure byte-count cap with no decode at all —
moby/buildkit's own server dispatches FileSend to one of two different
wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI)
depending on the negotiated exporter, with no signal sockguard can see to
tell them apart.
Upload/Pull is bound to a one-use token: an admitted Solve's
FrontendAttrs naming a context/context:<name> value shaped like
http://buildkit-session/<id> registers <id> as a one-use key, and a Pull
must present the matching urlhost/urlpath metadata and consume it
atomically. Upload's BytesMessage frames are unambiguous and get the
same decode-plus-unknown-fields check as every other mediated message.
New request_body.buildkit.session.{file_sync,file_send,upload} cap
config (max_files, max_total_bytes, max_path_length, max_file_bytes,
max_bytes), threaded through Limits/Policy via a new per-session
effectiveLimits merge. FileSync/TarStream stays hard-denied with no new
knob. internal/buildkitproxy stays at 100% statement coverage.
* 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233)
Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185.
* ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232)
Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185.
* chore(commits): retire gitmoji for plain Conventional Commits (#235)
- chore(commits): 11 plain types, no emoji; hook + CI validator updated
- fix(release): release-level inference restricts legacy prefixes to emoji only
- ci(verify): commit check diffs against the PR base branch, not origin/main
- docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING
- test(release): word-prefix rejection and composed emoji coverage
* fix(ci): survive podman's socket unlink/rebind in the integration setup (#234)
- fix(ci): integration setup tolerates podman's socket unlink/rebind race
- fix(ci): rootful podman socket restricted to runner primary group (0750/0660)
* fix(deps): update non-major (npm) (#229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update non-major (github-actions) (#230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(release): roll release metadata forward to v1.7.0 (#236)
* chore(release): roll release metadata forward to v1.7.0
Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase
1-6b BuildKit gRPC mediation work plus #186's endpoint-config
narrowing), bumps the website version, Helm chart, and README
latest-stable banner to match, flips the v1.7.0 roadmap milestone to
released, and adds a v1.8.0 placeholder covering the one gap phase
1-6b left open (RUN-instruction inspection for frontend-less Solve
requests, per #185). Updates page-data.test.mjs's hardcoded
next/latest-released assertions to match.
* chore(deps): regenerate next-env.d.ts after the Next upgrade
The npm bumps in #229 moved Next to a version that emits an extra
root-params.d.ts reference. The generated files were never refreshed,
so every local build dirtied the tree and tripped the clean-tree
pre-push hook.
* ci: make the soak harness and its gates actually honest
The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios.
quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too.
The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed.
* docs(readme): fix lying and dead badges
Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568.
The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score.
* chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242)
* build(docker): add .dockerignore to trim the build context
docker build runs with context: . in three workflows (including the
required Docker Build check), shipping the full ~2GB repo root —
node_modules, website/docs node builds, worktrees — when the Dockerfile
only needs app/.
* ci(quality-mutation): guard the tee pipeline with shell: bash
The step piped gremlins output through tee without shell: bash, so it
ran under the default bash -e {0} (no -o pipefail) and reported tee's
exit code instead of gremlins'. Matches the guard already used in the
soak and benchmark workflows.
* chore(lint): add markdownlint and yamllint config for CodeRabbit
.coderabbit.yaml enables both tools and CodeRabbit reviews every PR,
but nothing in CI or lefthook runs either one locally, so they've been
running on stock defaults. Tunes them the same way drydock does: skip
line-length, duplicate-heading, inline-HTML, and first-line-heading
noise for markdownlint; skip document-start, truthy, and line-length
noise for yamllint.
* docs(readme): mark v1.7.0 shipped in the roadmap section
The roadmap blurb still called v1.7.0 "the first committed epic in
the v1.7.0 milestone" in future tense with no shipped subsection,
while the top note and Recent Updates described it as already
released. v1.7.0 GA published 2026-08-11 (#185, #186); all three
places now agree.
* build(deps): dedupe lockfile drift on magic-string
npm dedupe found fumadocs-mdx's nested magic-string could converge to
the newer 1.1.1 patch already satisfying its ^1.1.0 range; the
pre-push lockfile-dedupe gate blocks on any drift, unrelated to the
hygiene fixes in this branch.
* fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243)
benchmarkInspectPolicy wrapped per-iteration request setup in
b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats()
calls per loop iteration outside the timed window. Those calls are
invisible in ns/op but scale with b.N, and since the wrapped
benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop()
calibrated N into the hundreds of thousands to over a million,
turning a ~1s measurement into 40-90s of real wall time per rep.
That's why quality-go-bench-monthly.yml has been getting cancelled at
its 30m ceiling every month since these benchmarks landed in May
(the May 1 run predates the file and succeeded): the job was never
actually hung, just quietly paying millions of hidden ReadMemStats
calls with nothing to show for it in the logs.
Drop the StopTimer/StartTimer calls to match every other benchmark
in the repo, which already builds per-iteration fixtures inline
inside the timed loop. Verified locally: the full `-count=5 ./...`
suite that used to blow past 30 minutes without finishing one
package now completes in ~13 minutes. Bump timeout-minutes to 40 for
headroom on slower CI hardware.
* ci(fuzz): cover every fuzz target across tiers (#244)
* ci(fuzz): cover every fuzz target across tiers
* chore(deps): dedupe magic-string lock entry
* test(fuzz): reject stale workflow targets
* chore(deps): pin Renovate to dev/v1.7 (#245)
* ci(checks): remove emoji from required job names (#246)
* fix(build): align Go module root with repository (#248)
* fix(build): establish repository-root Go module
* refactor(go): stage canonical import migration
* ci(security): activate canonical Gosec scan
* fix(module): address staged review findings
* fix(module): close staged review gaps
* refactor(go): migrate service package imports
* build(go): update toolchain to 1.26.6
* fix(module): keep staged package boundaries coherent
* refactor(go): complete canonical import migration
* refactor(go): remove staged compatibility packages
* test(go): harden final layout checks
* test(go): detect lowercase Docker instructions
* fix(deps): update nanoid to 3.3.18 (#251)
* fix(deps): update nanoid to 3.3.18
* test(security): guard nanoid lock versions
* ci(review): expose bounded promotion review gaps (#250)
* ci(review): expose unreviewed promotion ranges
* fix(ci): mirror bridge eligibility
* ci(review): restore reviewed promotion changes (#252)
* ci(review): restore reviewed promotion changes
* fix(ci): report incomplete mutation results
* test(ci): strengthen incomplete-report contract
* feat(website): add cookieless PostHog analytics (#253)
* feat(website): add cookieless PostHog analytics
* fix(website): make route manifest checks race safe
* fix(website): preserve pre-init analytics events
* fix(website): remove Go Report Card badge
Remove the Go Report Card badge and CSP allowance from the website.
* ci: migrate to reusable workflows with temporary bridges (#258)
* ci: migrate to reusable workflows with temporary bridges
Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA
go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so
lint/test/release logic is shared with the rest of the org. Reusable check
runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which
doesn't match the plain context names the branch-protection ruleset
currently requires, so seven "legacy-*" bridge jobs mirror those exact
names, fail-closed on the reusable call's result, until the ruleset itself
is migrated separately to require the new reusable contexts.
CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs
javascript-typescript coverage the central workflow doesn't provide); so do
dependency-review and commit-message, which aren't part of the required
context set. The qlty OIDC coverage-upload step is dropped rather than
routed through go-ci's qlty job, matching portwing's precedent. Go Lint and
Go Test now additionally run on the weekly schedule trigger since go-ci has
no per-job schedule gate — an accepted strict superset of prior behavior.
* fix(ci): close a file-system race in the reusable-workflow contract test
assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync
and then re-opened it by path with readFileSync — a TOCTOU window where the
file on disk could change between the check and the read (CodeQL
js/file-system-race, alert #278). Open the script once and reuse that file
descriptor for both the executable-bit stat and the content read.
* fix(ci): address CodeRabbit findings on the reusable-workflow migration
- fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test,
build-workspaces) with `github.event_name != 'schedule'`, matching
node-ci's own schedule skip, so scheduled runs don't fail these
always()-gated bridges closed
- test(ci): update the bridge contract test to require the schedule
guard on the node-ci bridges while keeping the go-ci bridges on
plain always()
- chore(lefthook): drop the obsolete `command -v goreleaser` skip guard
from goreleaser-snapshot; the adapter runs goreleaser via `go run`
with a pinned module and never needs the global binary
- fix(ci): tighten the quarantine-removal assertion in
go-release-check.sh to match the exact generated Cask command
instead of a loose substring
---------
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261)
PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host
straight off event properties and silently drops the event with a
cookieless_missing_user_agent/cookieless_missing_host warning if either is
absent. before_send's allowlist rebuild stripped both, so every event was
being dropped at ingestion. Require and forward both fields (and only those
two); $ip stays server-filled by the PostHog proxy. docs/ carries the same
change via the existing analytics-runtime sync.
* ci: remove temporary status bridges (#260)
* ci: remove temporary status bridges
The main branch-protection ruleset now requires the reusable "Go CI / ..."
and "Node CI / ..." contexts directly, so the seven legacy-* jobs that
mirrored the old plain context names are no longer needed. Updates the
contract tests to assert the bridges stay removed and pins the ruleset's
nine required contexts.
* test(ci): detect unquoted retired context names
* docs(readme): add warpchart growth chart (#263)
* docs(readme): add warpchart growth chart
* docs(readme): keep warpchart chart inside centering container
- docs(readme): inline HTML anchor/img so the chart stays in the centered div
- docs(changelog): record the README addition
* fix(deps): update golang.org/x/mod to v0.40.0 (#265)
Clears OpenSSF Scorecard alert #8's two actionable findings, GO-2026-6179
and GO-2026-6180 (sumdb transparency-log verification bypasses), fixed in
x/mod v0.40.0. The third finding, GO-2026-5932 (unmaintained
x/crypto/openpgp), has no fixed version and is already a documented,
narrowly-scoped exception in .grype.yaml. govulncheck reports none of the
three as reachable; this is a manifest-level cleanup.
* docs(changelog): backfill unreleased entries for CI migration, analytics, and bench fix (#266)
Three merged, changelog-worthy changes were missing from [Unreleased]:
the reusable-workflow CI migration (#258/#260), cookieless PostHog
analytics with its ingestion-property fix (#253/#261), and the monthly
benchmark ReadMemStats stall fix (#243). Surfaced by a repo audit;
CLAUDE.md wants changelog updates atomic with each change, so this
closes the gap in one pass.
* ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype (#277)
* ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype
The Dependency Graph repo setting is now on, so the dependency-review job's
hard `if: false` guard comes out and it actually runs on every PR again. A
new Gitleaks job adds the checksum-pinned CLI secrets scan sockguard was
missing (careerrat#72's pattern), with a repo-scoped .gitleaks.toml
allowlisting three verified-synthetic test-fixture strings. A new Actionlint
job lints this repo's own workflows, which zizmor alone didn't cover. Grype
image scanning moves into ci-verify.yml's required Docker Build job so it
gates the image the PR pipeline actually builds, and security-grype.yml's
grype-image job stays scheduled/manual-only to avoid a double scan.
Refs: sockguard#271
* fix(ci): resolve pre-existing shellcheck findings the new actionlint gate surfaces
The Actionlint job added in the prior commit lints every workflow file, not
just the ones it touched, and shellcheck flagged five real issues elsewhere:
an unquoted go install target, two multi-append blocks that collapse into a
single grouped redirect, an ls-based count replaced with a nullglob array,
and one sudo-before-redirect false alarm (the redirect target is /tmp, which
never needed root) resolved with an annotated inline suppression instead of
rerouting the log through `sudo tee`.
* ci: wire knip, shellcheck, and zizmor as real lint gates (#276)
* ci: wire knip, shellcheck, and zizmor as real lint gates
Closes the drift the linters.md registry audit flagged: knip only ran
in lefthook pre-push, never in CI; shellcheck and markdownlint existed
only as CodeRabbit review plugins, never as lefthook/CI gates. A
linter that only runs in one place is drift, not defense in depth.
- node-lint.sh now runs `npx knip` alongside biome, so it rides the
existing required "Node CI / Biome Lint" check — the reusable
node-ci.yml workflow has no dedicated knip input.
- Add scripts/ci/shellcheck.sh, which lints every tracked *.sh file
and hard-fails locally with an install hint if shellcheck is
missing (no silent skip). Wire it into lefthook pre-push and a new
"Shellcheck" job in ci-verify.yml.
- Fix the one shellcheck finding it surfaced: unused loop variables in
benchmarks/run.sh (SC2034), renamed to `_`.
- The lefthook zizmor step now hard-fails with an install hint instead
of silently skipping when the binary is absent, matching the
blocking-local-gate pattern used elsewhere in the org (e.g.
drydock's scan-secrets.sh) for tools CI enforces unconditionally.
* fix(ci): cover shellcheck's pre-0.10 SC2317 code in run-matrix.sh disables
* test(ci): cover shellcheck.sh in the fixed-script contract
CodeRabbit review: the runner is mode 100755 but assertFixedScripts() never
inspected it, so its markers went unchecked. Also corrects the lefthook
comment, which named the node-ci lint job instead of the standalone
shellcheck job.
* ci(review): wire Greptile as a label-gated second-opinion reviewer (#281)
* ci(review): wire Greptile as a label-gated second-opinion reviewer
Adds a thin `.github/workflows/greptile.yml` caller that summons
CodesWhat/.github's reusable greptile-summon.yml only when a PR carries
the `second-opinion` label, and a matching `.coderabbit.yaml`
`labeling_instructions` entry so CodeRabbit can apply that label itself
on security-sensitive diffs, large refactors, or a tiebreaker with the
author's intent. AGENTS.md now explains what the dormant `greptile.json`
config is for, so it doesn't read as dead weight.
Closes #269
* fix(review): enable suggested_labels so auto_apply_labels can fire
suggested_labels: false disables label suggestions entirely, including
labeling_instructions, so CodeRabbit would never apply second-opinion and
the greptile.yml caller would never trigger.
* docs(readme): migrate to the codified house README shape (#278)
* docs(readme): migrate to the codified house README shape
Reorders the header stack (logo/grabber/badge wall/hr/release note
before Contents), regroups the badge wall into three ordered rows
(identity, quality/security, social proof), adds the qlty
maintainability badge, drops all decorative emoji from headings, the
Contents list, and the Features table's icon column, reconciles the
previously split Community sections into one with a single
Issues/Discussions/Discord routing sentence, and fixes a stale
anchor link in the tri-tool example README.
Closes #270
* docs(readme): disambiguate the two Documentation contents entries
The external docs-site link and the in-page section anchor both read
Documentation in the same Contents list. The external one is now Docs
site; the anchor keeps the label that matches its section heading.
* build(deps): dedupe nested magic-string to 1.2.1
The lockfile-dedupe pre-push gate resolves against the live registry, so
the newly published 1.2.1 made it fail on every branch until the nested
copy under fumadocs-mdx is realigned.
* build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2 (#279)
* build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2
- build(release): add a sboms: block to app/.goreleaser.yaml (syft,
CycloneDX per archive), mirroring portwing's existing pattern
- ci(release): install syft via anchore/sbom-action/download-syft in
release-from-tag.yml ahead of the GoReleaser run; skip sbom cataloging
in the shared snapshot/config-check script when syft isn't on PATH so
the local pre-push hook and the CI "GoReleaser Config" gate keep
passing without needing syft, without hiding a real failure
- docs(releasing): qualify "SLSA attestation"/"SLSA build provenance" to
"SLSA Build L2" per the house provenance standard, and note that v*
tags are protected against deletion/rewrite by a repo tag ruleset, so
the Cosign signatures and SLSA Build L2 attestations are the signature
of record instead of a git-tag signature
- docs(changelog): record the SBOM and SLSA-qualification change under
[Unreleased] / Security
* build(release): restore syft --enrich all in the SBOM args
Setting args: replaces GoReleaser's entire default slice, so switching the
output to cyclonedx dropped --enrich all and shipped SBOMs without
dependency license data.
* build(deps): dedupe nested magic-string to 1.2.1
Same registry-driven lockfile-dedupe gate failure as #278; realigns the
nested copy under fumadocs-mdx so pushes pass.
* fix: replace the retired star-history.com chart with Warpchart (#283)
* docs(readme): drop the broken star-history.com chart embed
GitHub restricted stargazer API access, so the star-history.com SVG now
serves an error card reading 'GitHub restricted access to star data'
directly above the working Warpchart embed. Warpchart accumulates its own
time series and is unaffected, so it replaces rather than duplicates the
old chart. Also gives the section the house h2 heading it lacked.
* build(deps): dedupe nested magic-string to 1.2.1
* fix(website): replace the retired star-history.com chart with Warpchart
GitHub restricted the stargazer API that star-history.com replays per
request, so its SVG returns an error card at HTTP 200 — the landing page
was serving that to every visitor. Warpchart accumulates its own time
series and honours theme=dark, so the light/dark swap is preserved. The
CSP img-src allowlist moves with it, in lockstep with the pinned test.
* docs(changelog): reconcile the stale Warpchart entry with the star-history removal
The #264 entry said the Warpchart chart sits alongside the existing
chart. This PR removes that chart, so the clause is now false and
contradicts the entry directly above it. Drop the clause and add the
issue citation the entry was missing.
* fix(deps): update non-major (gomod) (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update non-major (github-actions) (#274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix(deps): update non-major (npm) (#273)
* fix(deps): update non-major (npm)
* test(website): pin posthog-js assertion to 1.417.1
The npm dependency bump upgraded posthog-js from 1.417.0 to 1.417.1
but the analytics integration test still asserted the old exact
version, failing CI.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* ci(security): pin runners, comment reusable pins, and start harden-runner egress block (#282)
* ci(security): pin runners, comment reusable pins, and start harden-runner egress block
Pins all 33 runs-on: ubuntu-latest references to ubuntu-24.04 across every
workflow so a GitHub-side default-image bump can never silently change CI
without a diff to review, and adds freeze-date comments to the two frozen
CodesWhat/.github reusable-workflow SHA pins in ci-verify.yml (that repo
tags no releases, so a SHA is the only anchor). Updates four stale
"ubuntu-latest" prose comments in quality-integration-podman.yml to match.
Starts stage 1 of a 3-stage harden-runner egress rollout, modeled on
drydock's own live posture: 15 jobs across 8 workflow files with a
deterministic, single-vendor egress footprint move from egress-policy:
audit to block with an explicit allowed-endpoints list and
disable-sudo: true, plus ci-verify.yml's go-ci caller now passes
workflow-security-egress-policy: block through to the reusable workflow's
zizmor job. Jobs with variable or multi-registry egress (Docker builds
against Chainguard's CDN, dockerd/podman integration pulling from Docker
Hub, the tri-tool conformance matrix, CodeQL's query-bundle CDN) stay
audit pending real telemetry. DAST (testssl) and OpenSSF Scorecard stay
audit permanently and now say so in-file. Release jobs are untouched
until two audited release-candidate cuts have run clean.
Bumps scripts/required-ci-contexts.test.mjs's REQUIRED_CONTEXTS from 9 to
13, adding Dependency Review, Gitleaks, Actionlint, and Shellcheck, which
already exist as PR-gating jobs in ci-verify.yml but were never added to
the pinned required-context list. Ships ahead of the branch-protection
ruleset update that will actually require them.
* ci(security): keep gosec on audit and allow go.dev for setup-go
Two fixes to the stage-1 egress rollout, both found by running the jobs
rather than reading them.
gosec back to audit: it runs as a container action, and block mode drops
container egress silently instead of rejecting it, so every module lookup
burns a full TCP timeout. The job takes ~2 minutes on every other branch
and exceeded timeout-minutes: 15 twice here, with no denied domain logged
to explain it. Same reasoning already applied to Docker Build and
grype-image, which stay on audit for the container-pull footprint.
govulncheck gains go.dev: setup-go checks the actions/go-versions manifest
first and only falls back to go.dev/dl. A warm runner never hits it, so
the gap stays invisible until a cache miss takes the job down.
* build(deps): tidy go.mod after the gomod bump
Renovate's non-major gomod update (#272) landed without a go mod tidy, so
dev/v1.7 carries go-jose/v4, letsencrypt/boulder and titanous/rocacheck as
indirect requirements nothing resolves to anymore. The goreleaser release
hook runs go mod tidy anyway, so this only decides whether the tree is
tidy before the tag or during it.
* ci(security): add ZAP baseline passive DAST scan for getsockguard.com (#280)
* ci(security): add ZAP baseline passive DAST scan for getsockguard.com
getsockguard.com and its /docs subpath are static surfaces (no auth,
no forms, no server-side handlers), so the DAST tier here is a ZAP
baseline passive scan (headers, CSP, cookie flags) against both
deployed targets on a weekly schedule plus workflow_dispatch. No
pull_request trigger: DAST scans what's already deployed, so it's
scheduled/advisory and never gates a PR. allow_issue_writing is off
so a red run stays a triage signal in the Actions tab instead of an
auto-filed issue, keeping the job's permissions at contents: read.
.zap/rules.tsv suppresses one informational false positive (rule
10049) that the site's immutable, content-hashed _next/static assets
reliably trigger. harden-runner runs egress-policy: audit as a
permanent exception, since the job scans a live Vercel-fronted
third-party endpoint with no stable destination set to allowlist.
* ci(security): withhold the repo token from the ZAP action
The action's token input defaults to ${{ github.token }} and exists only
to file findings as a GitHub issue, which allow_issue_writing: false
already turns off. Passing it empty means this third-party JS action
never receives a repository token at all, instead of relying on the
job's contents:read scope to bound what it could do with one.
* build(deps): realign the lockfile after an upstream magic-string publish
The lockfile-dedupe pre-push gate runs npm dedupe --dry-run against the
live registry, so magic-string 1.2.1 shipping upstream turns every branch
red with no local change. Nothing here depends on it directly; this just
resettles the transitive tree under fumadocs-mdx.
* chore(deps): lock file maintenance (#275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com>
* ci(gating): require the security scanners and drop emoji from job names (#285)
* ci(gating): require the security scanners and drop emoji from job names
- ci(gating): move security-grype.yml's paths filter from the trigger to a
changes job so its contexts always report
- ci(gating): make the Commit Message job blocking, exempting promotion PRs
- ci(style): strip emoji from all workflow and job names
- test(ci): pin the required-context list at 17
A required status check on a job inside a workflow that never triggers
produces no check run at all, so branch protection can never see it go
green. The paths filter therefore has to live in a job, not the trigger.
Closes #284
* ci(gating): restrict the promotion-PR exemption to same-repo branches
github.head_ref is attacker-chosen on a fork, so a fork branch named
dev/anything targeting main would have skipped the commit-message gate.
Gate the exemption on head.repo.full_name matching github.repository.
Caught by CodeRabbit.
* ci(style): strip emoji from the ZAP workflow and pin it with a test
security-zap-baseline.yml landed from #280 one merge after the original
sweep, still carrying emoji in its workflow and job names. A check context
is matched by exact string, so the whole set has to stay clean, not just
the names required today. Test walks every workflow's name:/run-name:.
* chore(release): prepare the v1.7.1 changelog and roadmap (#286)
Renames Unreleased to 1.7.1 per RELEASING.md step 5, backfills the nine
missing PR citations, corrects the now-stale note that Dependency Review,
Gitleaks, and Actionlint aren't required yet, and drops the shipped SBOM
clause from the roadmap's supply-chain row.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
…gning fixes to main (#293) * ✨ feat(filter): per-field endpoint-config gates on network connect (#186) (#223) * ✨ feat(filter): per-field endpoint-config gates on network connect request_body.network.allow_endpoint_config previously admitted Docker's whole EndpointSettings object as one opt-in. New EndpointConfigOptions gates each Engine API 1.55-era field independently (static addressing, link-local IPs, MAC pinning, GwPriority, aliases) on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig, so operators can allow benign settings like Aliases without also allowing address pinning. - denyEndpointConfigReason now checks the legacy flag first (unchanged behavior when true), then each granular field in turn, naming the specific field in the denial reason - endpointHasStaticIPConfig split into endpointHasStaticAddressFields and endpointHasLinkLocalIPs so static-IP and link-local gating can vary independently, without changing endpointHasStaticIPConfig's own behavior (mutation_kill_test.go pins it) - denyRootMacAddressReason respects AllowMACPinning independent of the legacy flag - Links and DriverOpts keep no granular escape hatch — still denied without the legacy allow_endpoint_config flag - libpod_network.go doc comment notes there's no libpod-native network-connect endpoint to gate Refs: #186 * 🧪 test(filter): cover per-field endpoint-config precedence Table-driven coverage for #186: granular allows each field individually and still denies the others, static-addressing and link-local-IPs gate independently of each other, Links/DriverOpts stay denied even with every other granular field allowed, root MacAddress respects AllowMACPinning independent of the legacy flag, and the legacy allow_endpoint_config: true flag takes precedence over a granular block that would otherwise deny. * ✨ feat(config): endpoint_config block + allow_endpoint_config mutual exclusion Adds request_body.network.endpoint_config (and its libpod_network mirror, kept for default-posture symmetry though never consulted): allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority default false; allow_aliases defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Compose aliases. - ToFilterOptions() inverts AllowAliases into filter.EndpointConfigOptions' DenyAliases so ~15 pre-existing filter-package tests that construct NetworkOptions{}/ContainerCreateOptions{} zero-value literals keep seeing aliases allowed, while the config layer's documented default stays true - Cross-wired into ContainerCreateOptions.EndpointConfig alongside the existing AllowEndpointConfig cross-wire, matching the established "network config, not duplicated under container_create" pattern - New Config.explicitNetworkEndpointConfig field + explicitKeysSet helper (generalized from the #149 explicitLegacyListenSet pattern) track whether the endpoint_config block was actually set via YAML or a SOCKGUARD_* env var, since its merged value is never the Go zero value (AllowAliases defaults true) and so can't be detected by zero-value comparison - validateNetworkEndpointConfig rejects allow_endpoint_config: true set together with an explicit endpoint_config block: the legacy flag already admits every field the granular block could, so the two are mutually exclusive by design (fail-closed on ambiguous intent) Refs: #186 * 🧪 test(config): cover endpoint_config defaults, env vars, and validation - filter_options_test.go: extends the existing full-policy mapping test with EndpointConfig, plus a dedicated test proving the granular block cross-wires into ContainerCreateOptions - network_endpoint_config_test.go (new): Defaults() has AllowAliases true and every other granular field false; all 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env vars are honored; setting allow_endpoint_config: true together with an explicit endpoint_config block is a validation error whether set via YAML or environment variables; the legacy flag alone and the granular block alone each validate cleanly; a pure-defaults config is never flagged as having an explicit endpoint_config block despite AllowAliases defaulting true * 📝 docs(network): document endpoint_config granular gates (#186) - configuration.mdx: endpoint_config field mapping/precedence prose, updated container_create/network policy table rows, and 5 new SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_* env var rows - security.mdx: mentions the granular alternative alongside the existing allow_endpoint_config description, notes Links/DriverOpts have no per-field escape hatch - migration.mdx: new note under the v1.4->v1.5 endpoint-config item explaining the granular block is opt-in and backward compatible, and that setting both forms is a validation error - CHANGELOG.md: new Unreleased section with the full feature writeup - README.md, website/src/lib/site-content.ts: roadmap bullet updated to reflect the granular gates are implemented ahead of the v1.7.0 BuildKit-mediation epic * 📝 docs(presets): mention granular endpoint_config in drydock macvlan notes Comment-only change to the drydock/drydock-with-compose/drydock-with-build presets' existing allow_endpoint_config macvlan/static-IP callout, pointing at the new per-field endpoint_config gates (#186) as an alternative to the all-or-nothing flag. Comments are exempt from compose_example_sync_test.go's structural (parsed policy) comparison, so this doesn't require touching the example compose files. * 🧪 test(filter): fuzz per-field endpoint-config gates Add FuzzNetworkConnectEndpointConfigGates and FuzzContainerCreateEndpointConfigGates, generating network-connect / container-create endpoint payloads plus granular EndpointConfigOptions combinations and asserting the fail-closed invariant CodeRabbit flagged on #223: with AllowEndpointConfig off, a field whose own granular gate is also off must produce a non-empty denial reason, and Links/DriverOpts (no granular gate at all) must always be denied. * 🧪 test(config): fuzz endpoint_config YAML parsing Add FuzzLoadBytesEndpointConfig, seeding YAML bodies centered on the #186 request_body.network.endpoint_config block (empty block, a single granular field, and the allow_endpoint_config/endpoint_config mutual- exclusion combination) and fuzzing LoadBytes then Validate on success — addresses a CodeRabbit finding on #223 that config parsing for the new block had no fuzz coverage. * 📝 docs(presets): correct endpoint_config wording in drydock comments The network-policy comment block in drydock.yaml / drydock-with-compose.yaml / drydock-with-build.yaml described endpoint-config policy as legacy-flag-only, which was already stale against #186: Aliases default to allowed but can be denied via endpoint_config.allow_aliases: false, and static IP addressing and MAC pinning each have their own granular gate now. Only Links and DriverOpts remain ungated and still require allow_endpoint_config: true. * 📝 docs(migration): move endpoint_config note to v1.7, warn on Links/DriverOpts The #186 granular endpoint_config migration note had landed inside the "From Sockguard v1.4 to v1.5" section even though the feature ships in v1.7 — move it into a new "From Sockguard v1.6 to v1.7" section. Also add the missing caveat that Links and DriverOpts have no granular gate on either network connect or container-create: workflows that need either must keep allow_endpoint_config: true and accept whole-object access. * 📝 docs(readme): correct endpoint_config narrowing description README.md and site-content.ts's v1.7.0 roadmap item both said allow_endpoint_config "has already been narrowed to per-field gates" / implied network-connect-only scope. Correct both to describe what #186 actually ships: request_body.network.endpoint_config.* provides the per-field gates, covering endpoint settings on both network connect and container-create, while allow_endpoint_config: true remains the broad whole-object override. * 📝 docs(presets): name all five granular endpoint gates in preset comments * ✨ feat(buildkit): #185 phase 1 — schema + policy foundation (deny-only) (#224) * ✨ feat(buildkitproto): vendor pinned BuildKit protobuf subset for gRPC mediation Part of issue #185's phase 1 (schema + policy foundation). Vendors a curated subset of BuildKit v0.32.0's protobuf schema under internal/buildkitproto/ — Control's Solve/Status (trimmed; Info/ ListWorkers/Session/Prune/DiskUsage/ListenBuildHistory/UpdateBuildHistory kept as bare method names only), the full LLB op graph, session auth/ secrets/ssh/filesync/upload, and the gRPC health service — generated via a pinned, reproducible buf+protoc-gen-go pipeline (scripts/generate-buildkit-proto.sh) with no protoc or grpc-go dependency anywhere in the chain. - Every vendored file's go_package is retargeted into this module so generated imports resolve without requiring github.com/moby/buildkit as an actual Go dependency. - PROVENANCE.md records source URLs, upstream tags, curation notes, and sha256 checksums for every vendored file, plus what was deliberately left out (LLBBridge, Exporter, PolicyVerifier, containerd content, OTLP trace) and why. - provenance_test.go golden-tests the vendored tree against the manifest in both directions: any drift in a listed file's checksum, or any unlisted .proto file on disk, fails the build. Package is internal, protobuf-only, and never imports grpc-go — see doc.go. * ✨ feat(buildkitproxy): add BuildKit gRPC method classification registry and policy types Part of issue #185's phase 1. internal/buildkitproxy holds the committed disposition table (mediate / passthrough / deny) for every gRPC method either opaque tunnel endpoint (POST /grpc's moby.buildkit.v1.Control, POST /session's callback services) can carry, matching the #185 synthesis inventory exactly: Control's Solve/Status are mediate, Info/ListWorkers are passthrough, everything else on Control has no enabling knob at all. Classify(endpoint, service, method) denies anything not explicitly listed — LLBBridge, nested Control/Session, containerd content, OTLP trace, Exporter, PolicyVerifier, and FileSync/TarStream are all captured in DeniedExamples and asserted deny. policy.go defines the runtime-facing Policy type (Control/Session sub-policies with per-RPC Allow flags and allowlists) that internal/config's BuildkitRequestBodyConfig.ToPolicy will translate into — buildkitproxy never imports config, so translation only flows one direction, same convention internal/filter's options types use. registry_test.go asserts every registry entry and DeniedExamples entry classifies as expected plus unknown-method deny; descriptor_manifest_test.go cross-checks both against the real vendored FileDescriptors from internal/buildkitproto via protoreflect, so the registry can't silently drift from the schema it's meant to describe. * ✨ feat(config): add request_body.buildkit policy surface and deny-only runtime gate Wires the #185 phase 1 BuildKit gRPC mediation policy into sockguard's existing config/filter/startup-validation machinery, with no change in runtime behavior for any endpoint sockguard currently allows. - config.go: new nested request_body.buildkit block (control.{allow_info, allow_list_workers,allow_status,solve.allow} and session.{health,auth, secrets,ssh,file_sync,file_send,upload}), available at the top level and per client profile like every other request_body.* surface. Presence, not an enabled flag, is what matters — the Go zero value denies everything, identical to a config that never mentions "buildkit:". - buildkit_options.go: BuildkitRequestBodyConfig.ToPolicy() translates into buildkitproxy.Policy, one direction only (buildkitproxy never imports config), mirroring RequestBodyConfig.ToFilterOptions. - validate.go: validateBuildkitConfig checks the allowlist fields (registry hosts via the existing normalizer; realms/scopes/IDs for non-empty, unpadded entries). validateBuildkitAckMutualExclusion rejects insecure_accept_opaque_buildkit_tunnels=true combined with a configured request_body.buildkit block, top-level or per profile — mediation supersedes the wholesale ack. - filter/buildkit.go + middleware.go: a new critical-severity inspector denies POST /session and POST /grpc unconditionally whenever request_body.buildkit is configured, since phase 1 ships no mediator to act on the richer policy yet — configuring the block must never look like it opened an opaque pass-through. - cmd/rules.go: validateBuildkitTunnelRulesForPolicy now also admits a /session or /grpc rule at startup when request_body.buildkit is configured (previously only the insecure ack satisfied this), with the admission/runtime split documented explicitly at the call site and pointing at filter.buildkitPolicy.inspect for the actual guarantee. * 🧪 test(config): cover BuildKit config defaults, env overrides, validation, and mutual exclusion Table-driven coverage for issue #185 phase 1's request_body.buildkit surface: zero-value defaults deny everything and pass Validate; ToPolicy() translates every field into buildkitproxy.Policy correctly; every allowlist validator (allowed_registries, allowed_realms, allowed_scopes, allowed_ids) rejects malformed entries with a dotted-path error; the insecure_accept_opaque_buildkit_tunnels mutual exclusion is checked both top-level and per client profile, in both directions (rejected together, accepted alone); and SOCKGUARD_REQUEST_BODY_BUILDKIT_* env vars bind through the existing Viper reflection-based default registration for both a nested bool (control.allow_info) and a nested string-slice (session.secrets.allowed_ids), confirming no map/slice caveat blocks env override for this config surface. Also adds FuzzLoadYAMLBuildkit, seeded with buildkit-shaped YAML (well-formed, mistyped fields, the ack/mediation conflict, whitespace/ empty allowlist entries, unicode) so mutation spends its budget exploring the new schema's key space specifically, per the repo convention that config parsing gets fuzz coverage. Ran a 15s local fuzz pass with no crashes. * 📝 docs(changelog): document BuildKit gRPC mediation phase 1 foundation Unreleased entry for issue #185 phase 1: the vendored protobuf subset, the method classification registry, and the request_body.buildkit config surface, with an explicit note that runtime mediation itself (and any change in what /session and /grpc actually do) lands in later phases of the epic. * 🔧 chore(deps): promote google.golang.org/protobuf to a direct dependency internal/buildkitproto's generated code imports google.golang.org/protobuf directly (issue #185 phase 1) rather than pulling it in transitively through another dependency, so go mod tidy correctly reclassifies it from indirect to direct in go.mod. No other dependency changed; go.sum is unaffected. * 🔧 chore(ci): exclude generated buildkitproto from the production coverage gate Verified locally: production total with the exclusion is 96.1% (vs 80.7% with 9.5k generated protobuf statements counted); the hand-written buildkitproxy package stays in the gate. * 🐛 fix(buildkit): address CodeRabbit round-1 findings on phase 1 - 🔒 deny direct moby.buildkit.v1.Control method paths in the phase-1 inspector - 🐛 make Policy.Configured an explicit field predicate (empty allowlists no longer count) - 🧪 cross-check PROVENANCE.md rows against the vendored manifest in the integrity test The ops.proto trimmed-provenance finding was refuted: the vendored file is byte-identical to upstream v0.32.0 except the documented go_package retarget (upstream sha256 matches PROVENANCE.md's recorded value). * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing (#225) * ✨ feat(buildkit): #185 phase 2 — h2c termination and stream routing * ✨ feat(buildkitproxy): terminate hijacked h2c connections and bridge gRPC streams Adds the real BuildKit gRPC mediator on top of phase 1's classification registry. bridge.go/upgrade.go/mediator.go terminate the hijacked POST /session and POST /grpc connections as h2c using golang.org/x/net/http2 (Server.ServeConn against the Docker client, Transport{AllowHTTP:true} dialing the daemon) and bridge them at the http.Handler level — one gRPC stream per *http.Request, original bytes preserved verbatim (no protobuf re-encode; that's phases 3-5). POST /session keeps buildkitd's reversed client/server roles, wired through a single symmetric bridgeLegs abstraction shared with /grpc. Every stream is classified through Classify() before being forwarded; a Deny gets an in-stream gRPC PERMISSION_DENIED and the tunnel stays open. limits.go adds defense-in-depth DoS caps (max concurrent streams, a denied-stream budget over a rolling window) — numeric defaults are sockguard's own judgment call, not specified by the #185 synthesis. session.go adds a session registry keyed by client identity + profile, never the client-supplied UUID alone (recorded only as advisory metadata). Any genuine bridge/transport error tears the whole tunnel down; a Deny or size-cap trip does not. * 🔒 security(buildkitproxy): gate Mediate/Passthrough forwarding behind Policy.Allowed Deviation from the literal task text ("Passthrough and Mediate both forward in Phase 2"): forwarding a method's *category* isn't enough on its own, or configuring any single request_body.buildkit field would silently forward every other Mediate/Passthrough method too, defeating the point of the granular per-field switches. policy.go adds Policy.Allowed(endpoint, service, method) as a second, necessary gate consulted alongside Classify() in bridge.go's handleStream — audited under a distinct buildkit_policy_denied reason code, separate from Classify's own buildkit_method_denied. * 🐛 fix(filter): admit the real h2c tunnel to the mediator instead of denying it outright filter/buildkit.go's inspect() no longer unconditionally denies POST /session and POST /grpc once request_body.buildkit is configured — those now pass through to internal/buildkitproxy's mediator (wired into the hijack tier via cmd/serve.go's new withBuildkitMediator layer). The literal /moby.buildkit.v1.Control/<Method> probe path stays hard-denied regardless of the method's own classification: it carries no h2c upgrade, so there's no tunnel for any mediator to terminate. * 🧪 test(buildkitproxy): full unit + integration coverage for the h2c bridge net.Pipe() plus real golang.org/x/net/http2 client/server pairs drive integration-style tests of the bridge/mediator without mocking http2 internals. A small clientLegConn interface on bridge (satisfied structurally by *http2.ClientConn) lets forward()'s harder-to-reach error branches — a genuine RoundTrip failure vs. a response-body copy failure vs. the request size-cap trip — be exercised deterministically via a hand-rolled fake instead of racing real connection teardown. internal/buildkitproxy is at 100% statement coverage. * 📦 deps(go.mod): promote golang.org/x/net to a direct dependency Approved exception for the mediation path only (golang.org/x/net/http2). No grpc-go anywhere in the chain; google.golang.org/protobuf was already direct. * 📝 docs(changelog): document phase 2 Fixes: #185 * 🐛 fix(buildkit): address CodeRabbit findings on phase 2 h2c bridge - 🐛 synchronize closeErr reads and fix exactly-at-cap off-by-one in the stream size limiter - 🔒 make Policy.Allowed method-specific and filter session advertisements through the selected policy - 🔒 validate the daemon's 101 response headers before bridging - 🧪 convert standalone buildkitproxy tests to table-driven form - 📝 correct the stale opaque-pass-through comment in cmd/rules.go * 🐛 fix(buildkit): prevent int64 sentinel overflow in stream size limiter A MaxMessageBytes of math.MaxInt64 made the one-byte-sentinel arithmetic in limitedReadCloser.Read wrap negative and panic on the slice; compare by subtraction instead and pin it with a table row. * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership (#226) * ✨ feat(buildkit): #185 phase 3 — control mediation and ref ownership - Control/Solve and Control/Status now get per-message gRPC decode and policy mediation instead of phase 2's blanket byte-verbatim relay: entitlements, frontend/frontend-attrs, cache import/export, and exporter checks against new request_body.buildkit.control.solve allowlists, with a strict protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize. - Beyond the checklist: deprecated singular cache-ref fields, EnableSessionExporter, and non-empty SourcePolicy are denied outright with no enabling knob — each would otherwise let a structurally "known" field bypass the allowlists above or reach other hard-denied surface. - Session registry gains a ref-ownership index keyed by client identity + profile (never a client-supplied ref alone); Status denies buildkit_ref_not_owned unless its ref was registered by an admitted Solve from the same identity+profile, refs release on session close, and a per-session cap bounds registration. - On admission the client's original frame bytes forward untouched — no re-encoding. internal/buildkitproxy stays at 100% statement coverage; repo-wide coverage holds at 96.2%. * 🎨 style(buildkit): gofmt solve_test.go * 🐛 fix(buildkit): address CodeRabbit findings on phase 3 control mediation - checkSolveExporters now parses "push" with strconv.ParseBool (matching BuildKit's own image exporter) instead of a bare == "true" check, denies values it can't evaluate, and validates every comma-separated ref in "name" against the exporter registry allowlist instead of just the whole string - SourcePolicySession and every other previously-unexamined SolveRequest field (FrontendInputs, ProxyNetwork, CompatibilityVersion, Internal, Session, Definition) now has an explicit disposition: denied outright, forwarded with a documented justification, or checked — see solve.go's header comment for the full field-by-field audit - fixed a SessionRegistry.PutRef/Close race that could let a ref survive its session's teardown with no live owner, leaving OwnsRef permanently true for that client identity/profile; added a concurrent regression test - isRemoteContextRef now detects scp-like git remotes (bob@host:path, no "://") the same way BuildKit's own gitutil does, closing a remote-context policy bypass for that syntax - forwardControlMediated's method switch fails closed (Internal status, audited) on an unrecognized method instead of forwarding with zero policy evaluation if it and isControlMediatedMethod ever drift - SolveRequest.Ref == "" is now rejected before ref registration, closing a path where an empty ref could pass Control/Status's ownership check for free; the per-session ref-cap denial gets its own audit reason (buildkit_ref_limit_exceeded) instead of overloading buildkit_policy_denied - denial messages no longer echo client-supplied cache/exporter type strings into the gRPC status message - buildkitproxy's registry-host allowlists (allowed_cache_registries, allowed_exporter_registries) are now normalized to the same lowercase/ canonical form registryHostFromImageRef compares against, so a mixed-case or index.docker.io config entry actually matches at runtime - bridge_controlmediated_test.go's daemonCalled flags are now atomic.Bool, fixing a data race between the daemon handler goroutine and the test goroutine Refuted: reordering isRemoteContextRef's http:// prefix check and suggesting a normalizeAllowedRegistryHost signature change were not part of the findings as filed; normalizeAllowedRegistryHost itself already lowercases and canonicalizes correctly — the bug was that its output was never applied to the config values ToPolicy hands to buildkitproxy. * ✨ feat(buildkit): #185 phase 4 — credential session mediation (#227) * ✨ feat(buildkit): #185 phase 4 — credential session mediation - moby.filesync.v1.Auth's four RPCs (Credentials, FetchToken, GetTokenAuthority, VerifyTokenAuthority) now get per-message decode and policy mediation instead of phase 2's blanket byte-verbatim relay: every RPC's Host is checked against request_body.buildkit.session.auth's already-existing allowed_registries (now actually consumed for the first time, and normalized the same lowercase/index.docker.io->docker.io way as Solve's registry allowlists so config-side case can't silently fail to match); FetchToken additionally requires its Realm to match allowed_realms and every requested scope to be a member of allowed_scopes, since a registry's token realm can live on a different host than the registry itself and the registry-host allowlist alone can't bound where a resulting bearer token is minted for. - moby.buildkit.secrets.v1.Secrets/GetSecret is gated on an exact allowed_ids match; a non-empty annotations map is denied outright — researched against BuildKit v0.32.0's own secret request/response path, which never populates or reads that field, so there's no reviewed semantics to admit instead of silently forwarding it. - moby.sshforward.v1.SSH/CheckAgent is gated the same way on allowed_ids. ForwardAgent (a bidirectional stream of raw agent-protocol bytes with no unary message to decode) is gated on the agent ID carried in gRPC metadata (buildkit.ssh.id, BuildKit's own wire convention) BEFORE a single byte is relayed; the stream itself is never decoded, buffered as a message, or logged in either direction. - Every admitted RPC forwards the client's original frame bytes verbatim, exactly like phase 3's Solve/Status; every response — which for these RPCs carries the actual credential/secret/token content — is never decoded on any path. - A new per-session quota (Limits.MaxCredentialCallsPerSession, default 512) throttles Auth/Secrets/SSH call volume independent of the connection-wide denied-stream abuse budget, since an admitted call never trips that budget; exceeding it denies RESOURCE_EXHAUSTED/ buildkit_credential_call_limit_exceeded without counting against the abuse budget. - Audit events for these RPCs carry the registry host in the clear (low-cardinality, operator-facing) but only a truncated SHA-256 of secret/ SSH-agent IDs, never the raw identifier. - FileSync/FileSend/Upload are untouched (phase 5's scope). internal/ buildkitproxy stays at 100% statement coverage; repo-wide production coverage holds at 96.3%. * 🔒 security(buildkit): log normalized registry host in auth audit events normalizeAuthHost trims surrounding whitespace before the allowlist comparison, so a Host like "ghcr.io\n" is admitted while the raw field — CR/LF included — reached the audit log's registry_host attr, letting an admitted client forge audit log lines. Log the normalized host (the value policy actually compared) wrapped in logging.SafeString, matching every other attr audit emits. Regression test asserts an admitted whitespace-padded host audits the canonical form through a live bridge. * 🔒 security(buildkit): salt audit ID hashes and fuzz credential evaluators Real-world secret/SSH IDs are short guessable operator labels ("default", "npm_token"); an unsalted SHA-256 truncated to 48 bits falls to an offline dictionary in milliseconds, leaking the very identifier shortHash exists to withhold. A per-process crypto/rand salt keeps same-ID correlation within one process while breaking dictionary recovery, with a regression test pinning the salted-vs-unsalted difference. Also adds FuzzEvaluateGetSecretRequest over the credential decode/match surface (never panic, never admit outside policy), widening mustMarshal to testing.TB for fuzz seeding. * ✨ feat(buildkit): #185 phase 5 — file and upload mediation (#228) Mediates the three remaining byte-verbatim streaming Mediate methods (moby.filesync.v1.FileSync/DiffCopy, moby.filesync.v1.FileSend/DiffCopy, moby.upload.v1.Upload/Pull) with per-message decode/policy checks, on a new streaming primitive (streamRelayReader/relayValidatedFrames) that decodes each gRPC length-prefixed frame, validates it, and forwards the original bytes verbatim on admission. FileSync/DiffCopy: request direction (PACKET_REQ/FIN/ERR) gets structural validation only; response direction (PACKET_STAT/DATA) gets path-traversal/absolute-path/NUL checks on every Stat path and symlink target, plus file-count/per-file-byte/total-byte/path-length caps. The "dockerfile"-named dir-name sync gets hold-and-inspect: each file's DATA is buffered independently by its fsutil-assigned ID (never per-stream — fsutil's real sender interleaves multiple files across four parallel workers) until that file's own EOF, then the same RUN-instruction/ syntax-frontend inspection the classic /build path uses runs against the reassembled bytes before release; a denial discards the held bytes without releasing anything. Holding is skipped when allow_run_instructions is already set. FileSend/DiffCopy requires an admitted Control/Solve from the same SessionKey and gets a pure byte-count cap with no decode at all — moby/buildkit's own server dispatches FileSend to one of two different wire shapes (fsutil.Packet for a local export, BytesMessage for tar/OCI) depending on the negotiated exporter, with no signal sockguard can see to tell them apart. Upload/Pull is bound to a one-use token: an admitted Solve's FrontendAttrs naming a context/context:<name> value shaped like http://buildkit-session/<id> registers <id> as a one-use key, and a Pull must present the matching urlhost/urlpath metadata and consume it atomically. Upload's BytesMessage frames are unambiguous and get the same decode-plus-unknown-fields check as every other mediated message. New request_body.buildkit.session.{file_sync,file_send,upload} cap config (max_files, max_total_bytes, max_path_length, max_file_bytes, max_bytes), threaded through Limits/Policy via a new per-session effectiveLimits merge. FileSync/TarStream stays hard-denied with no new knob. internal/buildkitproxy stays at 100% statement coverage. * 📝 docs(buildkit): #185 phase 6-B — migration, mediated-build presets, opaque-tunnel deprecation (#233) Migration guide, mediated-BuildKit compose presets, security/configuration/roadmap docs, and deprecation (warn-and-keep) of insecure_accept_opaque_buildkit_tunnels now that request_body.buildkit provides full per-message mediation. Part of #185. * ✅ test(buildkit): #185 phase 6-A — fuzzers, race, and DoS-budget hardening (#232) Test-only hardening for the BuildKit gRPC mediator: fuzzers (framing, upgrade, session-advertisement rewrite, Solve, Status), stdlib-only goroutine-leak checks, DoS/backpressure/concurrency-cap tests, and session-registry stress. buildkitproxy stays at 100% coverage. Part of #185. * chore(commits): retire gitmoji for plain Conventional Commits (#235) - chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(release): release-level inference restricts legacy prefixes to emoji only - ci(verify): commit check diffs against the PR base branch, not origin/main - docs(commits): convention + validator exemptions in AGENTS/CLAUDE/CONTRIBUTING - test(release): word-prefix rejection and composed emoji coverage * fix(ci): survive podman's socket unlink/rebind in the integration setup (#234) - fix(ci): integration setup tolerates podman's socket unlink/rebind race - fix(ci): rootful podman socket restricted to runner primary group (0750/0660) * fix(deps): update non-major (npm) (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): roll release metadata forward to v1.7.0 (#236) * chore(release): roll release metadata forward to v1.7.0 Renames the CHANGELOG Unreleased section to 1.7.0 (2026-08-08 phase 1-6b BuildKit gRPC mediation work plus #186's endpoint-config narrowing), bumps the website version, Helm chart, and README latest-stable banner to match, flips the v1.7.0 roadmap milestone to released, and adds a v1.8.0 placeholder covering the one gap phase 1-6b left open (RUN-instruction inspection for frontend-less Solve requests, per #185). Updates page-data.test.mjs's hardcoded next/latest-released assertions to match. * chore(deps): regenerate next-env.d.ts after the Next upgrade The npm bumps in #229 moved Next to a version that emits an extra root-params.d.ts reference. The generated files were never refreshed, so every local build dirtied the tree and tripped the clean-tree pre-push hook. * ci: make the soak harness and its gates actually honest The soak harness never passed --listen-socket/--upstream-socket, so sockguard died ~40s into every run. Fixed, and verified locally at 15s/conc-10 with zero errors across all four scenarios. quality-soak-weekly.yml and quality-go-bench-monthly.yml piped into tee under the default shell, which reports tee's exit code, so a failing run read as green; both now use shell: bash. Benchmark artifacts upload on failure too. The zizmor cache-poisoning ignore claimed the repo was private. ci-verify.yml never publishes (goreleaser runs --skip=publish) so it keeps an accurate ignore; release-from-tag.yml genuinely publishes, so caching is disabled there instead of suppressed. * docs(readme): fix lying and dead badges Removes the Go Report Card and pkg.go.dev badges, which no longer resolve. The mutation-score badge is now generated from the monthly run rather than hardcoded: quality-mutation-monthly.yml commits .github/badges/mutation-score.json and the README reads it through a shields endpoint badge, seeded at 97.8% from run 30692172568. The badge only publishes when all six package reports parse, so a partial run can't overwrite it with a skewed score. * chore: repo hygiene sweep (dockerignore, tee guard, lint config, roadmap tense) (#242) * build(docker): add .dockerignore to trim the build context docker build runs with context: . in three workflows (including the required Docker Build check), shipping the full ~2GB repo root — node_modules, website/docs node builds, worktrees — when the Dockerfile only needs app/. * ci(quality-mutation): guard the tee pipeline with shell: bash The step piped gremlins output through tee without shell: bash, so it ran under the default bash -e {0} (no -o pipefail) and reported tee's exit code instead of gremlins'. Matches the guard already used in the soak and benchmark workflows. * chore(lint): add markdownlint and yamllint config for CodeRabbit .coderabbit.yaml enables both tools and CodeRabbit reviews every PR, but nothing in CI or lefthook runs either one locally, so they've been running on stock defaults. Tunes them the same way drydock does: skip line-length, duplicate-heading, inline-HTML, and first-line-heading noise for markdownlint; skip document-start, truthy, and line-length noise for yamllint. * docs(readme): mark v1.7.0 shipped in the roadmap section The roadmap blurb still called v1.7.0 "the first committed epic in the v1.7.0 milestone" in future tense with no shipped subsection, while the top note and Recent Updates described it as already released. v1.7.0 GA published 2026-08-11 (#185, #186); all three places now agree. * build(deps): dedupe lockfile drift on magic-string npm dedupe found fumadocs-mdx's nested magic-string could converge to the newer 1.1.1 patch already satisfying its ^1.1.0 range; the pre-push lockfile-dedupe gate blocks on any drift, unrelated to the hygiene fixes in this branch. * fix(bench): stop ReadMemStats-per-iteration from stalling the monthly job (#243) benchmarkInspectPolicy wrapped per-iteration request setup in b.StopTimer()/b.StartTimer(), which forces two runtime.ReadMemStats() calls per loop iteration outside the timed window. Those calls are invisible in ns/op but scale with b.N, and since the wrapped benchmarks are cheap (tens to low-hundreds of ns/op), b.Loop() calibrated N into the hundreds of thousands to over a million, turning a ~1s measurement into 40-90s of real wall time per rep. That's why quality-go-bench-monthly.yml has been getting cancelled at its 30m ceiling every month since these benchmarks landed in May (the May 1 run predates the file and succeeded): the job was never actually hung, just quietly paying millions of hidden ReadMemStats calls with nothing to show for it in the logs. Drop the StopTimer/StartTimer calls to match every other benchmark in the repo, which already builds per-iteration fixtures inline inside the timed loop. Verified locally: the full `-count=5 ./...` suite that used to blow past 30 minutes without finishing one package now completes in ~13 minutes. Bump timeout-minutes to 40 for headroom on slower CI hardware. * ci(fuzz): cover every fuzz target across tiers (#244) * ci(fuzz): cover every fuzz target across tiers * chore(deps): dedupe magic-string lock entry * test(fuzz): reject stale workflow targets * chore(deps): pin Renovate to dev/v1.7 (#245) * ci(checks): remove emoji from required job names (#246) * fix(build): align Go module root with repository (#248) * fix(build): establish repository-root Go module * refactor(go): stage canonical import migration * ci(security): activate canonical Gosec scan * fix(module): address staged review findings * fix(module): close staged review gaps * refactor(go): migrate service package imports * build(go): update toolchain to 1.26.6 * fix(module): keep staged package boundaries coherent * refactor(go): complete canonical import migration * refactor(go): remove staged compatibility packages * test(go): harden final layout checks * test(go): detect lowercase Docker instructions * fix(deps): update nanoid to 3.3.18 (#251) * fix(deps): update nanoid to 3.3.18 * test(security): guard nanoid lock versions * ci(review): expose bounded promotion review gaps (#250) * ci(review): expose unreviewed promotion ranges * fix(ci): mirror bridge eligibility * ci(review): restore reviewed promotion changes (#252) * ci(review): restore reviewed promotion changes * fix(ci): report incomplete mutation results * test(ci): strengthen incomplete-report contract * feat(website): add cookieless PostHog analytics (#253) * feat(website): add cookieless PostHog analytics * fix(website): make route manifest checks race safe * fix(website): preserve pre-init analytics events * fix(website): remove Go Report Card badge Remove the Go Report Card badge and CSP allowance from the website. * ci: migrate to reusable workflows with temporary bridges (#258) * ci: migrate to reusable workflows with temporary bridges Route branch CI's Go and Node checks through CodesWhat/.github's frozen-SHA go-ci.yml/node-ci.yml reusable workflows instead of bespoke inline jobs, so lint/test/release logic is shared with the rest of the org. Reusable check runs report as "<caller job> / <called job>" (e.g. "Go CI / Go Lint"), which doesn't match the plain context names the branch-protection ruleset currently requires, so seven "legacy-*" bridge jobs mirror those exact names, fail-closed on the reusable call's result, until the ruleset itself is migrated separately to require the new reusable contexts. CodeQL Analysis and Docker Build stay local and untouched (CodeQL needs javascript-typescript coverage the central workflow doesn't provide); so do dependency-review and commit-message, which aren't part of the required context set. The qlty OIDC coverage-upload step is dropped rather than routed through go-ci's qlty job, matching portwing's precedent. Go Lint and Go Test now additionally run on the weekly schedule trigger since go-ci has no per-job schedule gate — an accepted strict superset of prior behavior. * fix(ci): close a file-system race in the reusable-workflow contract test assertFixedScripts() checked each scripts/ci/*.sh with existsSync/statSync and then re-opened it by path with readFileSync — a TOCTOU window where the file on disk could change between the check and the read (CodeQL js/file-system-race, alert #278). Open the script once and reuse that file descriptor for both the executable-bit stat and the content read. * fix(ci): address CodeRabbit findings on the reusable-workflow migration - fix(ci): gate the three node-ci legacy bridges (biome-lint, ts-test, build-workspaces) with `github.event_name != 'schedule'`, matching node-ci's own schedule skip, so scheduled runs don't fail these always()-gated bridges closed - test(ci): update the bridge contract test to require the schedule guard on the node-ci bridges while keeping the go-ci bridges on plain always() - chore(lefthook): drop the obsolete `command -v goreleaser` skip guard from goreleaser-snapshot; the adapter runs goreleaser via `go run` with a pinned module and never needs the global binary - fix(ci): tighten the quarantine-removal assertion in go-release-check.sh to match the exact generated Cask command instead of a loose substring --------- Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * fix(website): forward $raw_user_agent and $host for cookieless ingestion (#261) PostHog's cookieless server-hash ingestion reads $raw_user_agent and $host straight off event properties and silently drops the event with a cookieless_missing_user_agent/cookieless_missing_host warning if either is absent. before_send's allowlist rebuild stripped both, so every event was being dropped at ingestion. Require and forward both fields (and only those two); $ip stays server-filled by the PostHog proxy. docs/ carries the same change via the existing analytics-runtime sync. * ci: remove temporary status bridges (#260) * ci: remove temporary status bridges The main branch-protection ruleset now requires the reusable "Go CI / ..." and "Node CI / ..." contexts directly, so the seven legacy-* jobs that mirrored the old plain context names are no longer needed. Updates the contract tests to assert the bridges stay removed and pins the ruleset's nine required contexts. * test(ci): detect unquoted retired context names * docs(readme): add warpchart growth chart (#263) * docs(readme): add warpchart growth chart * docs(readme): keep warpchart chart inside centering container - docs(readme): inline HTML anchor/img so the chart stays in the centered div - docs(changelog): record the README addition * fix(deps): update golang.org/x/mod to v0.40.0 (#265) Clears OpenSSF Scorecard alert #8's two actionable findings, GO-2026-6179 and GO-2026-6180 (sumdb transparency-log verification bypasses), fixed in x/mod v0.40.0. The third finding, GO-2026-5932 (unmaintained x/crypto/openpgp), has no fixed version and is already a documented, narrowly-scoped exception in .grype.yaml. govulncheck reports none of the three as reachable; this is a manifest-level cleanup. * docs(changelog): backfill unreleased entries for CI migration, analytics, and bench fix (#266) Three merged, changelog-worthy changes were missing from [Unreleased]: the reusable-workflow CI migration (#258/#260), cookieless PostHog analytics with its ingestion-property fix (#253/#261), and the monthly benchmark ReadMemStats stall fix (#243). Surfaced by a repo audit; CLAUDE.md wants changelog updates atomic with each change, so this closes the gap in one pass. * ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype (#277) * ci(security): close PR-gate gaps for dependency-review, gitleaks, actionlint, and Grype The Dependency Graph repo setting is now on, so the dependency-review job's hard `if: false` guard comes out and it actually runs on every PR again. A new Gitleaks job adds the checksum-pinned CLI secrets scan sockguard was missing (careerrat#72's pattern), with a repo-scoped .gitleaks.toml allowlisting three verified-synthetic test-fixture strings. A new Actionlint job lints this repo's own workflows, which zizmor alone didn't cover. Grype image scanning moves into ci-verify.yml's required Docker Build job so it gates the image the PR pipeline actually builds, and security-grype.yml's grype-image job stays scheduled/manual-only to avoid a double scan. Refs: sockguard#271 * fix(ci): resolve pre-existing shellcheck findings the new actionlint gate surfaces The Actionlint job added in the prior commit lints every workflow file, not just the ones it touched, and shellcheck flagged five real issues elsewhere: an unquoted go install target, two multi-append blocks that collapse into a single grouped redirect, an ls-based count replaced with a nullglob array, and one sudo-before-redirect false alarm (the redirect target is /tmp, which never needed root) resolved with an annotated inline suppression instead of rerouting the log through `sudo tee`. * ci: wire knip, shellcheck, and zizmor as real lint gates (#276) * ci: wire knip, shellcheck, and zizmor as real lint gates Closes the drift the linters.md registry audit flagged: knip only ran in lefthook pre-push, never in CI; shellcheck and markdownlint existed only as CodeRabbit review plugins, never as lefthook/CI gates. A linter that only runs in one place is drift, not defense in depth. - node-lint.sh now runs `npx knip` alongside biome, so it rides the existing required "Node CI / Biome Lint" check — the reusable node-ci.yml workflow has no dedicated knip input. - Add scripts/ci/shellcheck.sh, which lints every tracked *.sh file and hard-fails locally with an install hint if shellcheck is missing (no silent skip). Wire it into lefthook pre-push and a new "Shellcheck" job in ci-verify.yml. - Fix the one shellcheck finding it surfaced: unused loop variables in benchmarks/run.sh (SC2034), renamed to `_`. - The lefthook zizmor step now hard-fails with an install hint instead of silently skipping when the binary is absent, matching the blocking-local-gate pattern used elsewhere in the org (e.g. drydock's scan-secrets.sh) for tools CI enforces unconditionally. * fix(ci): cover shellcheck's pre-0.10 SC2317 code in run-matrix.sh disables * test(ci): cover shellcheck.sh in the fixed-script contract CodeRabbit review: the runner is mode 100755 but assertFixedScripts() never inspected it, so its markers went unchecked. Also corrects the lefthook comment, which named the node-ci lint job instead of the standalone shellcheck job. * ci(review): wire Greptile as a label-gated second-opinion reviewer (#281) * ci(review): wire Greptile as a label-gated second-opinion reviewer Adds a thin `.github/workflows/greptile.yml` caller that summons CodesWhat/.github's reusable greptile-summon.yml only when a PR carries the `second-opinion` label, and a matching `.coderabbit.yaml` `labeling_instructions` entry so CodeRabbit can apply that label itself on security-sensitive diffs, large refactors, or a tiebreaker with the author's intent. AGENTS.md now explains what the dormant `greptile.json` config is for, so it doesn't read as dead weight. Closes #269 * fix(review): enable suggested_labels so auto_apply_labels can fire suggested_labels: false disables label suggestions entirely, including labeling_instructions, so CodeRabbit would never apply second-opinion and the greptile.yml caller would never trigger. * docs(readme): migrate to the codified house README shape (#278) * docs(readme): migrate to the codified house README shape Reorders the header stack (logo/grabber/badge wall/hr/release note before Contents), regroups the badge wall into three ordered rows (identity, quality/security, social proof), adds the qlty maintainability badge, drops all decorative emoji from headings, the Contents list, and the Features table's icon column, reconciles the previously split Community sections into one with a single Issues/Discussions/Discord routing sentence, and fixes a stale anchor link in the tri-tool example README. Closes #270 * docs(readme): disambiguate the two Documentation contents entries The external docs-site link and the in-page section anchor both read Documentation in the same Contents list. The external one is now Docs site; the anchor keeps the label that matches its section heading. * build(deps): dedupe nested magic-string to 1.2.1 The lockfile-dedupe pre-push gate resolves against the live registry, so the newly published 1.2.1 made it fail on every branch until the nested copy under fumadocs-mdx is realigned. * build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2 (#279) * build(release): add GoReleaser SBOMs and qualify SLSA claim to Build L2 - build(release): add a sboms: block to app/.goreleaser.yaml (syft, CycloneDX per archive), mirroring portwing's existing pattern - ci(release): install syft via anchore/sbom-action/download-syft in release-from-tag.yml ahead of the GoReleaser run; skip sbom cataloging in the shared snapshot/config-check script when syft isn't on PATH so the local pre-push hook and the CI "GoReleaser Config" gate keep passing without needing syft, without hiding a real failure - docs(releasing): qualify "SLSA attestation"/"SLSA build provenance" to "SLSA Build L2" per the house provenance standard, and note that v* tags are protected against deletion/rewrite by a repo tag ruleset, so the Cosign signatures and SLSA Build L2 attestations are the signature of record instead of a git-tag signature - docs(changelog): record the SBOM and SLSA-qualification change under [Unreleased] / Security * build(release): restore syft --enrich all in the SBOM args Setting args: replaces GoReleaser's entire default slice, so switching the output to cyclonedx dropped --enrich all and shipped SBOMs without dependency license data. * build(deps): dedupe nested magic-string to 1.2.1 Same registry-driven lockfile-dedupe gate failure as #278; realigns the nested copy under fumadocs-mdx so pushes pass. * fix: replace the retired star-history.com chart with Warpchart (#283) * docs(readme): drop the broken star-history.com chart embed GitHub restricted stargazer API access, so the star-history.com SVG now serves an error card reading 'GitHub restricted access to star data' directly above the working Warpchart embed. Warpchart accumulates its own time series and is unaffected, so it replaces rather than duplicates the old chart. Also gives the section the house h2 heading it lacked. * build(deps): dedupe nested magic-string to 1.2.1 * fix(website): replace the retired star-history.com chart with Warpchart GitHub restricted the stargazer API that star-history.com replays per request, so its SVG returns an error card at HTTP 200 — the landing page was serving that to every visitor. Warpchart accumulates its own time series and honours theme=dark, so the light/dark swap is preserved. The CSP img-src allowlist moves with it, in lockstep with the pinned test. * docs(changelog): reconcile the stale Warpchart entry with the star-history removal The #264 entry said the Warpchart chart sits alongside the existing chart. This PR removes that chart, so the clause is now false and contradicts the entry directly above it. Drop the clause and add the issue citation the entry was missing. * fix(deps): update non-major (gomod) (#272) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update non-major (github-actions) (#274) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update non-major (npm) (#273) * fix(deps): update non-major (npm) * test(website): pin posthog-js assertion to 1.417.1 The npm dependency bump upgraded posthog-js from 1.417.0 to 1.417.1 but the analytics integration test still asserted the old exact version, failing CI. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * ci(security): pin runners, comment reusable pins, and start harden-runner egress block (#282) * ci(security): pin runners, comment reusable pins, and start harden-runner egress block Pins all 33 runs-on: ubuntu-latest references to ubuntu-24.04 across every workflow so a GitHub-side default-image bump can never silently change CI without a diff to review, and adds freeze-date comments to the two frozen CodesWhat/.github reusable-workflow SHA pins in ci-verify.yml (that repo tags no releases, so a SHA is the only anchor). Updates four stale "ubuntu-latest" prose comments in quality-integration-podman.yml to match. Starts stage 1 of a 3-stage harden-runner egress rollout, modeled on drydock's own live posture: 15 jobs across 8 workflow files with a deterministic, single-vendor egress footprint move from egress-policy: audit to block with an explicit allowed-endpoints list and disable-sudo: true, plus ci-verify.yml's go-ci caller now passes workflow-security-egress-policy: block through to the reusable workflow's zizmor job. Jobs with variable or multi-registry egress (Docker builds against Chainguard's CDN, dockerd/podman integration pulling from Docker Hub, the tri-tool conformance matrix, CodeQL's query-bundle CDN) stay audit pending real telemetry. DAST (testssl) and OpenSSF Scorecard stay audit permanently and now say so in-file. Release jobs are untouched until two audited release-candidate cuts have run clean. Bumps scripts/required-ci-contexts.test.mjs's REQUIRED_CONTEXTS from 9 to 13, adding Dependency Review, Gitleaks, Actionlint, and Shellcheck, which already exist as PR-gating jobs in ci-verify.yml but were never added to the pinned required-context list. Ships ahead of the branch-protection ruleset update that will actually require them. * ci(security): keep gosec on audit and allow go.dev for setup-go Two fixes to the stage-1 egress rollout, both found by running the jobs rather than reading them. gosec back to audit: it runs as a container action, and block mode drops container egress silently instead of rejecting it, so every module lookup burns a full TCP timeout. The job takes ~2 minutes on every other branch and exceeded timeout-minutes: 15 twice here, with no denied domain logged to explain it. Same reasoning already applied to Docker Build and grype-image, which stay on audit for the container-pull footprint. govulncheck gains go.dev: setup-go checks the actions/go-versions manifest first and only falls back to go.dev/dl. A warm runner never hits it, so the gap stays invisible until a cache miss takes the job down. * build(deps): tidy go.mod after the gomod bump Renovate's non-major gomod update (#272) landed without a go mod tidy, so dev/v1.7 carries go-jose/v4, letsencrypt/boulder and titanous/rocacheck as indirect requirements nothing resolves to anymore. The goreleaser release hook runs go mod tidy anyway, so this only decides whether the tree is tidy before the tag or during it. * ci(security): add ZAP baseline passive DAST scan for getsockguard.com (#280) * ci(security): add ZAP baseline passive DAST scan for getsockguard.com getsockguard.com and its /docs subpath are static surfaces (no auth, no forms, no server-side handlers), so the DAST tier here is a ZAP baseline passive scan (headers, CSP, cookie flags) against both deployed targets on a weekly schedule plus workflow_dispatch. No pull_request trigger: DAST scans what's already deployed, so it's scheduled/advisory and never gates a PR. allow_issue_writing is off so a red run stays a triage signal in the Actions tab instead of an auto-filed issue, keeping the job's permissions at contents: read. .zap/rules.tsv suppresses one informational false positive (rule 10049) that the site's immutable, content-hashed _next/static assets reliably trigger. harden-runner runs egress-policy: audit as a permanent exception, since the job scans a live Vercel-fronted third-party endpoint with no stable destination set to allowlist. * ci(security): withhold the repo token from the ZAP action The action's token input defaults to ${{ github.token }} and exists only to file findings as a GitHub issue, which allow_issue_writing: false already turns off. Passing it empty means this third-party JS action never receives a repository token at all, instead of relying on the job's contents:read scope to bound what it could do with one. * build(deps): realign the lockfile after an upstream magic-string publish The lockfile-dedupe pre-push gate runs npm dedupe --dry-run against the live registry, so magic-string 1.2.1 shipping upstream turns every branch red with no local change. Nothing here depends on it directly; this just resettles the transitive tree under fumadocs-mdx. * chore(deps): lock file maintenance (#275) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: scttbnsn <80784472+scttbnsn@users.noreply.github.com> * ci(gating): require the security scanners and drop emoji from job names (#285) * ci(gating): require the security scanners and drop emoji from job names - ci(gating): move security-grype.yml's paths filter from the trigger to a changes job so its contexts always report - ci(gating): make the Commit Message job blocking, exempting promotion PRs - ci(style): strip emoji from all workflow and job names - test(ci): pin the required-context list at 17 A required status check on a job inside a workflow that never triggers produces no check run at all, so branch protection can never see it go green. The paths filter therefore has to live in a job, not the trigger. Closes #284 * ci(gating): restrict the promotion-PR exemption to same-repo branches github.head_ref is attacker-chosen on a fork, so a fork branch named dev/anything targeting main would have skipped the commit-message gate. Gate the exemption on head.repo.full_name matching github.repository. Caught by CodeRabbit. * ci(style): strip emoji from the ZAP workflow and pin it with a test security-zap-baseline.yml landed from #280 one merge after the original sweep, still carrying emoji in its workflow and job names. A check context is matched by exact string, so the whole set has to stay clean, not just the names required today. Test walks every workflow's name:/run-name:. * chore(release): prepare the v1.7.1 changelog and roadmap (#286) Renames Unreleased to 1.7.1 per RELEASING.md step 5, backfills the nine missing PR citations, corrects the now-stale note that Dependency Review, Gitleaks, and Actionlint aren't required yet, and drops the shipped SBOM clause from the roadmap's supply-chain row. * ci(release): stop the cut gate timing out before CI can finish (#290) * ci(release): stop the cut gate timing out before CI can finish release-cut.yml overrode the verify-ci-success action's max-attempts down to 20 (5 min), but a CI: Verify run on main takes ~6m45s. The v1.7.1 cut failed on its first attempt with the run it was waiting for succeeding 9s later. Drop the override so both callers inherit the action's 40-attempt default, and make the poll log say whether a run is in progress or absent -- those were printing the same line. Closes #288 * ci(release): say 'has not completed' rather than 'is in progress' The count is status != completed, which covers queued/waiting/requested too, so the old wording claimed more than the number supports. Renamed in_progress_count to noncompleted_count to match. Caught by CodeRabbit. * ci(quality): resolve a current sockguard for the current-* conformance rows (#291) * ci(quality): resolve a current sockguard for the current-* conformance rows current-standard and current-edge floated portwing and drydock to latest but left sockguard on the audited-floor 1.5.1, so 1.6.0, 1.7.0 and 1.7.1 were never covered by the weekly suite. Resolve those two rows from tag history instead, concrete rather than latest so a red run names its own inputs. legacy-floor's pins and the explicit sockguard_image override are unchanged. Refs #289 * fix(conformance): resolve current-* rows to a published sockguard image resolve_latest_sockguard_version stopped at the newest stable git tag, which is not the same thing as the newest pullable image. A tag lands minutes before release-from-tag finishes publishing its image, and a failed publish job leaves a tag with no image behind indefinitely -- either window handed the row an unpullable reference. Walk tags newest-first and stop at the first one docker manifest inspect finds published on Docker Hub (a bare codeswhat/* ref is Hub, not GHCR). Every skipped tag is announced on stderr so a broken publish surfaces instead of quietly passing as an older version, and an unreachable registry is fatal rather than a reason to walk back. - test(conformance): cover all four resolver paths in --self-test with a stubbed registry, keeping the self-test network-free - docs(conformance): correct README and CHANGELOG, which both described resolution as purely local Fixes: #289 * test(conformance): make the resolver self-test independent of tag history The new resolver cases read the real `git tag` list, so they passed locally and failed on the Node test gate, which checks out at the default depth 1 with no tags: every case took the "no stable v* tags found" exit, and none-published still reported PASS -- non-zero for entirely the wrong reason. Stub `git` the same way `docker` is already stubbed, over a fixed synthetic tag list. The self-test stops depending on checkout depth, the expectations become exact constants rather than whichever release is newest this week, and the prerelease filter gets pinned (v2.1.0-rc.1 must never win). * ci(release): sign and attest the per-platform release archives (#292) * ci(release): sign and attest the per-platform release archives Only sockguard-v<tag>.tar.gz carried a .sig/.pem and a provenance attestation, and that file is the git-archive SOURCE snapshot built by the separate `release` job. The archives users actually download, sockguard_<version>_<os>_<arch>.tar.gz, shipped with a CycloneDX SBOM and nothing else. The near-identical filenames are what hid it. Add two keyless cosign `signs:` blocks so every archive and checksums.txt get a .sig + .pem, and attest build provenance over app/dist/*.tar.gz. The goreleaser job gains id-token: write and attestations: write, plus a cosign install, since the signs blocks shell out to it. - docs(verification): document verifying the per-platform archives, and spell out the source-vs-binary filename distinction Verified with a real tagged build against a stubbed cosign (GoReleaser disables signing under --snapshot, so a snapshot run proves nothing): all ten expected signature artifacts are produced. The sigstore round trip itself only exercises on the next release cut. Fixes: #271 * test(release): make QA-6 verify the archives the docs tell people to verify scripts/verify-published-release.sh exists to fail the tag when docs/content/docs/verification.mdx and the pipeline drift apart. It could only ever do that for the source tarball, so the per-platform archive, checksums.txt, and provenance commands this branch documents would have passed QA-6 without being run even once. It now verify-blobs the linux/amd64 archive and checksums.txt alongside the source tarball, runs the documented `sha256sum --check --ignore-missing`, and runs `gh attestation verify` against the compiled archive. One archive stands in for all four: a single signs: block in a single job signs them together, so a config or identity fault fails them together. - test(release): pin the new coverage so it can't silently regress --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: biggest-littlest <zap_inane.2p@icloud.com>
Phase 3 of #185 (BuildKit gRPC mediation): per-message mediation for
moby.buildkit.v1.Controlon the/grpctunnel.What changed
readUnaryGRPCMessage— compressed frames, malformed framing, and multi-message streams all fail closed), decoded with the vendoredbuildkitprotostubs, checked, and on admission the original frame bytes forward untouched — never a re-encode.security.insecuredenied outright,network.hostgated onallow_host_network, unknown entitlements denied), fixed frontend allowlist (""/dockerfile.v0only —gateway.v0would relocate the hard-denied LLBBridge surface), strict FrontendAttrs key allowlist with remote-context andforce-network-mode=hostgating, cache import/export type+registry allowlists, exporter type+push-registry allowlists, and a protobuf-reflection unknown-field walk denying anything the vendored schema doesn't recognize (buildkit_schema_unsupported).EnableSessionExporter, and non-emptySourcePolicyare denied with no enabling knob — each would let a structurally "known" field bypass the allowlists or reach other hard-denied surface.buildkit_ref_not_ownedunless its ref belongs to an admitted Solve from the same identity+profile; refs release on session close;MaxRefsPerSessionbounds registration.request_body.buildkit.control.solve.*allowlist knobs following existing conventions (empty list = deny, per-profile falls out ofClientProfileConfig, env override via existing reflection binding).allow_host_network/allow_remote_contextare threaded from the siblingrequest_body.buildblock rather than duplicated.Validation
go build,go vet,golangci-lint runcleango test -race ./internal/buildkitproxy/...and full suite greeninternal/buildkitproxyat 100.0% statement coverage; repo-wide production profile at 96.2% (gate: 96%)Part of #185 (phases 4–6 remain — do not auto-close).
Changelog
Control/SolveandControl/Status.request_body.buildpermissions for host networking and remote contexts.Statusrequests for refs not owned by the same client identity and profile.Concerns
BuildkitRequestBodyConfig.ToPolicy(build BuildRequestBodyConfig)signature.