Skip to content

PER-16045: move the OPA builder to golang:1.26-bookworm (lands before permit-opa go 1.26) - #342

Merged
EliMoshkovich merged 6 commits into
mainfrom
eli/per-16045-opa-builder-go126
Sep 23, 2026
Merged

EliMoshkovich merged 6 commits into
mainfrom
eli/per-16045-opa-builder-go126

Conversation

@EliMoshkovich

@EliMoshkovich EliMoshkovich commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The Dockerfile's opa_build stage moves from golang:1.25-bookworm to golang:1.26-bookworm, sets GOTOOLCHAIN=local itself, and fails the build below the go1.26.6 floor. The x/crypto waiver texts are updated to match. This has to land before permit-opa raises its go directive to 1.26 (permitio/permit-opa#52), because that change hard-fails the current builder.

Why

PER-16045 (Decision 3: fix the expected x/crypto/ssh waiver rather than keep it). The same change was first proposed as PDP#334 under PER-15358, which was closed unmerged on 2026-09-16. No open PR does it, so it is re-proposed here. Before this PR, .docker/scout/pdp-v2.vex.json and the tests.yml comment named PDP#334 as the removal gate.

  • golang.org/x/crypto ≥ 0.56.0 clears CVE-2026-78662 and CVE-2026-56855 (x/crypto/ssh), which are currently waived as not_affected in .docker/scout/pdp-v2.vex.json. Its go.mod declares go 1.26.0, so permit-opa's directive has to move with it.
  • tests.yml and release.yml build permit-opa from an unpinned ref: main checkout. The official golang images set GOTOOLCHAIN=local, so this builder does not fetch a newer toolchain. It fails:
    go: go.mod requires go >= 1.26.0 (running go 1.25.14; GOTOOLCHAIN=local)
    
    That breaks build-pdp-image on every PR, every push to main and every release, from the moment the permit-opa change merges.
  • A 1.26 builder compiling today's go 1.25.0 module is forward-compatible, so this PR is safe on its own.

What changed

  • Dockerfile: FROM golang:1.25-bookworm AS opa_build → FROM golang:1.26-bookworm AS opa_build, with a comment recording the ordering constraint.
    • ENV GOTOOLCHAIN=local in the stage, so the hard-fail does not depend on the base image setting it.
    • A RUN that fails the build if go env GOVERSION is below go1.26.6, the first 1.26 release with the crypto/tls fix for GO-2026-6090 (checked on vuln.go.dev). It has the same shape as the CPython floor check in main. The tag floats its patch version (go1.26.8 today), so this catches a stale local image, or a merge conflict with ci: gate releases on CVEs and unify Dependabot, Trivy and Docker Scout (PER-15358) #338 that restores 1.25.
    • The comment says what changes in /app/bin/opa. Changes that come with the 1.26 toolchain land with this builder (e.g. the Green Tea GC is on by default). GODEBUG defaults follow permit-opa's go.mod instead. The OPA build is CGO_ENABLED=0, so the builder's glibc never reaches the image.
    • It also says why the patch version floats (a Go security release arrives without a bump here) and that the floor covers only the branch that compiles permit-opa, not the prebuilt-OPA fallback.
    • On auditability it no longer says build info is "what scanners read". The exact toolchain survives -s -w in the binary's build info and is readable with go version on an extracted copy, but that is an after-the-fact audit, not a gate: the only scanner in this repo is the pull_request-only docker-scout job pointed at a local tag, so no published pdp-v2 tag is re-scanned today. PDP#338 is what closes that gap.
    • The compile RUN now echoes go env GOVERSION. The floor RUN is the gate, and it caches on the base image digest - in a release log it typically reads CACHED, so it is not the record of what compiled that image. COPY custom* /custom sees a tarball the workflow regenerates every run, so the compile RUN never caches and its echo is always in the log of the build that produced the binary.
  • release.yml: the no-cache-filters comment no longer says opa_build "re-executes unconditionally". It re-executes from COPY custom* on; the two layers before it cache on the base image digest.
  • Dockerfile: dropped two dead USER switches: the USER permit after COPY ./horizon, and the USER permit / USER root pair around COPY kong_routes.json (no RUN between them; a COPY without --chown writes root:root regardless).
  • .docker/scout/pdp-v2.vex.json (version 12) and the tests.yml scout comment: the two x/crypto waivers no longer say the builder is 1.25 or name the closed PDP#334 as their removal gate. The remaining gate is permitio/permit-opa#52. permitio/permit-opa#49 is stated in the past tense (merged 2026-09-16; permit-opa main carries x/crypto v0.55.0 and grpc v1.83.2), and both statements now say why the superseded @0.53.0 subcomponent PURL stays in the list instead of being swapped out.

How validated

All three runs build only the opa_build stage (docker buildx build --platform linux/arm64 --target opa_build), with custom/custom_opa.tar.gz packaged exactly as tests.yml does (find * \( -name '*go*' -o -name 'LICENSE.md' \) … tar -czf):

Builder permit-opa source Result
this PR (golang:1.26-bookworm) today's main (go 1.25.0) builds; /opa version → OPA 1.14.1, go1.26.8
this PR (golang:1.26-bookworm) permit-opa#52 (go 1.26.0, x/crypto v0.57.0) builds; /opa version → go1.26.8. Trivy (CRITICAL/HIGH/MEDIUM) on the binary: 0 Critical/High, 1 Medium (containerd CVE-2026-53495; permit-opa#52 has since bumped containerd/v2 to v2.2.8, and the binary built in round 4 carries it)
origin/main Dockerfile (golang:1.25-bookworm) permit-opa#52 fails: go: go.mod requires go >= 1.26.0 (running go 1.25.14; GOTOOLCHAIN=local). This is the failure this PR prevents.

After the review follow-up (139a4c1), with permit-opa#52's source: opa_build builds; /opa is go1.26.8, x/crypto v0.57.0, CGO_ENABLED=0. Mutation check: with the builder set back to golang:1.25-bookworm, the new floor check fails the build (go1.25.14 is below the go1.26.6 floor). The floor expression passes 1.26.6, 1.26.8, 1.26.10 and 1.27.1, and fails 1.25.14, 1.26.0 and 1.26.5. pre-commit is clean on the changed files; actionlint on tests.yml reports the same 3 pre-existing findings as before.

Round 3 (f867aad) is comments plus two one-liners, checked without a build: permit-opa#52's check-pdp-builder.sh still parses the FROM … AS opa_build line (that parse is now called out in the comment above it, because an ARG or a line split would turn permit-opa's CI red); the floor RUN chain run through /bin/sh prints on go1.26.8 and go1.26.6 and still exits 1 on go1.26.5 and go1.25.14; and the derived ensurepip path evaluates correctly through /bin/sh with the exact quoting used in the file. No Docker daemon was available on this machine, so the image itself was not rebuilt this round.

After the second review follow-up (5d84477), with permit-opa#52 at 31d5dda (x/crypto v0.57.0, containerd v2.2.8) packaged the same way: opa_build (linux/arm64) builds and the floor check passes. /opa is go1.26.8, x/crypto v0.57.0, containerd/v2 v2.2.8, CGO_ENABLED=0, with no interpreter or NEEDED entry. go version -m reads all of that from the stripped (-s -w) binary. docker buildx build --check reports the same single pre-existing warning (SecretsUsedInArgOrEnv) before and after. actionlint on release.yml reports the same 2 pre-existing findings before and after. pre-commit is clean.

Round 4 (273ca8f) is Zeev's six accuracy threads: five comment/PR-body corrections plus one one-line echo. A Docker daemon WAS available this round. docker buildx build --platform linux/arm64 --target opa_build against permit-opa#52 at 8d44183 (go 1.26.0, x/crypto v0.57.0) builds; the floor check prints opa_build: building with go1.26.8 and the new compile-RUN echo prints opa_build: compiling permit-opa with go1.26.8. Cache split reproduced: re-running with a regenerated tarball gives #6 CACHED for the floor layer (no version printed) while the compile RUN re-executes and prints the toolchain - which is the claim the reworded comment makes. go version -m on the extracted binary: go1.26.8, golang.org/x/crypto v0.57.0, github.com/containerd/containerd/v2 v2.2.8, CGO_ENABLED=0, GOARCH=arm64. docker buildx build --check --target opa_build reports no warnings. jq -e . on the VEX doc is valid (version 12, 7 statements). pre-commit is clean on both changed files.

Not run locally: the full multi-arch image or the scout gate. CI's build-pdp-image, docker-scout and pdp-tester cover them.

Merge order / dependencies

  1. This PR, first.
  2. permitio/permit-opa#51 (pinned release toolchain), any time before [Snyk] Security upgrade aiohttp from 3.7.2 to 3.8.0 #52. [Snyk] Security upgrade aiohttp from 3.7.2 to 3.8.0 #52 is stacked on it and retargets to main after it merges.
  3. permitio/permit-opa#52 (go 1.26 + x/crypto v0.57.0), which says DO NOT MERGE before this one. Its pdp-builder CI check reads this repo's Dockerfile on main and fails until this PR has merged. PDP builds permit-opa from ref: main, so the next PDP build after [Snyk] Security upgrade aiohttp from 3.7.2 to 3.8.0 #52 merges has x/crypto v0.57.0. No permit-opa release is needed for pdp-v2. The release and the cloud-pdp .permit-opa-version bump in [Snyk] Security upgrade aiohttp from 3.7.2 to 3.8.0 #52's step 4 are for the Nexus/edge image.
  4. Then delete the two x/crypto statements in .docker/scout/pdp-v2.vex.json and the matching tests.yml comment.

Merging this fixes main, not the tags. release.yml checks THIS repo out with no ref: (:25-26) while still taking permit-opa from ref: main (:40-43), and tests.yml also builds on any push to a v* branch (:6). The newest tag, v0.9.15, is d3da8b9 - this PR's base - whose opa_build is still golang:1.25-bookworm. So the lasting rule, for as long as the permit-opa checkout is unpinned: once permit-opa#52 merges, every pdp-v2 release and every v* branch build must come from a commit containing this FROM line. Backport it before cutting a hotfix, or re-running a release, from an older tag. permit-opa#52's check-pdp-builder.sh hands the releaser exactly this rule and says it cannot enforce it, because it only ever reads this repo's Dockerfile on main. Pinning the permit-opa checkout to a tag removes the class.

Conflict note: #338 (open) pins this same FROM line by digest (golang:1.25-bookworm@sha256:…). Whichever PR lands second should keep 1.26 and take #338's digest convention: docker buildx imagetools inspect golang:1.26-bookworm gives the manifest-list digest.

Related, same ticket: permitio/permit-opa#51 (pinned release toolchain), permitio/cloud-pdp#161, permitio/cloud-pdp#162.

Risks / follow-ups

  • Low risk: the stage output is a static Go binary built from the same source by the Go 1.26 toolchain. Runtime changes that come with that toolchain (e.g. the Green Tea GC on by default) reach pdp-v2 with this PR. pdp-tester passes on the built image; watch memory on the first release.
  • Out of scope, follow-ups (pre-existing, none changed by this PR):
    • pinning the permit-opa checkout (ref: main) in tests.yml/release.yml, and release.yml's actions/checkout@v3;
    • an arm64 opa_build PR job (this change was validated on linux/arm64 locally; CI builds amd64);
    • the vanilla-OPA fallback download (latest, no --fail, no checksum), and OPA_BUILD not deciding which OPA ships;
    • -a defeating the Go build cache;
    • pipefail for the tarball pipeline in build_opal_bundle.sh, tests.yml and release.yml;
    • test_offline_mode's unversioned python:alpine base.
  • Fixed in round 3, no longer a follow-up: the hardcoded python3.13 ensurepip path. It contradicted the CPython floor check 34 lines above, which branches for 3.13 and 3.14; with rm -r (no -f) the build would have failed the day the base tag moved. The path now comes from sysconfig.get_paths()["stdlib"], which resolves to the same location on a stock CPython under /usr/local and tracks the base.
  • Once permit-opa#52 lands, the directive floor is 1.26. Any other consumer that builds permit-opa on Go < 1.26 with GOTOOLCHAIN=local breaks the same way. permit-opa's own Dockerfile is bumped in that PR.

🤖 Generated with Claude Code

Lands ahead of permit-opa raising its go directive to 1.26, which
golang.org/x/crypto >= 0.56.0 forces (its own go.mod declares go 1.26.0).
That x/crypto version clears CVE-2026-78662 / CVE-2026-56855, today waived
in .docker/scout/pdp-v2.vex.json.

Ordering only works one way: tests.yml and release.yml build permit-opa
from an unpinned `ref: main` checkout, and the official golang images set
GOTOOLCHAIN=local, so a 1.25 builder facing a go 1.26 module hard-fails
instead of fetching a toolchain - breaking build-pdp-image everywhere the
moment the permit-opa change merges. A 1.26 builder on today's go 1.25.0
module is forward-compatible.

Same change as the closed PDP#334 (PER-15358), re-proposed under PER-16045.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 16, 2026 19:10
@linear-code

linear-code Bot commented Sep 16, 2026

Copy link
Copy Markdown

PER-16045

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

🔍 Vulnerabilities of permitio/pdp-v2:next

📦 Image Reference permitio/pdp-v2:next
digestsha256:91da910734a9dc9dfb7e1c72eeeba85e97390a8b1d1e2c2f4ee43cec26f1f022
vulnerabilitiescritical: 0 high: 5 medium: 4 low: 5 unspecified: 1
platformlinux/amd64
size136 MB
packages247
📦 Base Image python:026d26881d2e1ebad06e4f309b0fc5f03c0471c5230d325d7b571be802586ee6
also known as
  • 3.13-alpine3.23
  • 3.13.15-alpine3.23
digestsha256:f282f385cfce21b0a644094330930704424a48d59afe8f08052e0f8e0b7a35c6
vulnerabilitiescritical: 0 high: 3 medium: 2 low: 0
critical: 0 high: 2 medium: 2 low: 1 starlette 0.50.0 (pypi)

pkg:pypi/starlette@0.50.0

high 7.5: CVE--2026--54283 Allocation of Resources Without Limits or Throttling

Affected range>=0.4.1
<1.3.1
Fixed version1.3.1
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.397%
EPSS Percentile34th percentile
Description

Summary

request.form() accepts max_fields and max_part_size to bound resource consumption while parsing form data. These limits are enforced for multipart/form-data, but silently ignored for application/x-www-form-urlencoded. An unauthenticated attacker can therefore send a urlencoded body with an arbitrarily large number of fields or an arbitrarily large field, even when the application configured limits it believed would apply.

Details

request.form() dispatches to a different parser depending on the Content-Type. For multipart/form-data the max_files, max_fields, and max_part_size limits are forwarded to the parser, but for application/x-www-form-urlencoded the parser is constructed without them. It has no max_fields or max_part_size parameter to receive them, and it appends every field with no count check and accumulates each field's name and value with no size check. The configured limits are therefore both unreachable and unenforced for url-encoded bodies.

Because the url-encoded parser does its work synchronously between stream reads, the two attack shapes have different effects:

  • Field count drives CPU and event-loop blocking. A body of ~1,000,000 fields (a sub-10MB payload such as f0=v&f1=v&...) blocks the worker's event loop for several seconds while parsing, during which the worker serves no other request.
  • Field size drives memory. A single large field value (e.g. a 50MB value) is buffered in full to build the FormData, forcing memory allocation proportional to the request body.

The equivalent multipart/form-data request is correctly rejected with 400 Too many fields / 400 Field exceeded maximum size.

Impact

This Denial of service (DoS) vulnerability affects all applications built with Starlette (or FastAPI) that call request.form() on application/x-www-form-urlencoded requests. A single request with a very large number of fields blocks the event loop for several seconds, and a single request with a very large field forces unbounded memory allocation; in either case, parallel requests can render the service unusable. A reverse proxy that enforces a request body size limit reduces but does not eliminate the exposure, since a sub-10MB body is already enough to block the event loop.

Mitigation

Upgrade to a patched version, which forwards max_fields and max_part_size to the url-encoded parser and enforces them while parsing, raising before the oversized field or excess fields are accumulated. The defaults match multipart/form-data (max_fields=1000, max_part_size=1MB) and can be customized via request.form(max_fields=..., max_part_size=...).

high 7.5: CVE--2026--48818 Server-Side Request Forgery (SSRF)

Affected range<1.1.0
Fixed version1.1.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
EPSS Score0.368%
EPSS Percentile31st percentile
Description

Summary

When serving static files on Windows, StaticFiles resolves the requested path with os.path.realpath. If a UNC path (such as \\attacker.com\share) reaches the resolver, realpath causes the process to open a connection to the remote host over SMB (port 445). This is a server-side request forgery (SSRF) that leaks the service account's NTLMv2 credentials to the attacker-controlled host, which can then be cracked offline or relayed to other hosts.

Details

StaticFiles.lookup_path() joins the requested path onto the served directory and calls os.path.realpath on the result before checking containment with os.path.commonpath. On Windows, a UNC path is absolute, so os.path.join discards the served directory and realpath resolves the bare UNC path, triggering the outbound SMB connection and NTLM authentication before the containment check rejects the path. The HTTP response is a benign 404, but the credential disclosure has already happened. POSIX systems are not affected.

This only affects the default configuration (follow_symlink=False), which uses os.path.realpath. The follow_symlink=True branch uses os.path.abspath, which performs no I/O.

Impact

Applications running on Windows that serve files with StaticFiles (directly, or via a framework built on Starlette such as FastAPI) in the default configuration are affected. StaticFiles is typically unauthenticated, so any client can trigger the SMB connection and leak the service account's NTLMv2 hash. A secondary impact is discovering internal hosts reachable over SMB by timing responses for valid versus invalid addresses.

Mitigation

Applications not running on Windows are not affected. On Windows, serving static files through a dedicated web server (such as nginx or IIS) instead of StaticFiles avoids the issue. Blocking outbound SMB (port 445) from the application host prevents the credential disclosure even if a UNC path is resolved.

medium 6.5: CVE--2026--48710 Improper Validation of Unsafe Equivalence in Input

Affected range<=1.0.0
Fixed version1.0.1
CVSS Score6.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
EPSS Score36.257%
EPSS Percentile98th percentile
Description

Summary

In affected versions, the HTTP Host request header was not validated before being used to reconstruct request.url. Because the routing algorithm relies on the raw HTTP path while request.url is rebuilt from the Host header, a malformed header could make request.url.path differ from the path that was actually requested. Middleware and endpoints that apply security restrictions based on request.url (rather than the raw scope path) could therefore be bypassed.

Details

When a client requests http://example.com/foo, it sends:

GET /foo HTTP/1.1
Host: example.com

Affected versions reconstructed the URL by concatenating http://{host}{path} and re-parsing the result. The Host value is only valid as a uri-host [ ":" port ] per RFC 9112 §3.2, where uri-host follows the restricted host grammar of RFC 3986 §3.2.2. When it contains characters outside that grammar - notably /, ?, or # - those characters move the path/query/fragment boundaries during re-parsing, so the parsed request.url.path no longer matches the path the server actually received. For example:

GET /foo HTTP/1.1
Host: example.com/abc?bar=

reconstructs to http://example.com/abc?bar=/foo, whose parsed path is /abc - even though routing used the real path /foo. The router still dispatches to /foo and the endpoint executes, but any middleware or code that reads request.url.path sees /abc, so path-based authorization checks can be bypassed.

Impact

Any application running an affected version that relies on request.url (or request.url.path) for security-sensitive decisions is affected. The most common case is middleware that gates access to certain path prefixes based on request.url.path. Deployments fronted by a proxy or load balancer are mitigated only if that proxy rejects or normalizes the malformed Host header before forwarding and the application does not trust attacker-controlled host headers (e.g. X-Forwarded-Host) elsewhere.

Mitigation

Upgrade to a patched version, which validates the Host header against the grammar of RFC 9112 §3.2 / RFC 3986 §3.2.2 when constructing request.url and falls back to scope["server"] for malformed values.

medium 5.3: CVE--2026--48817 Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')

Affected range<1.1.0
Fixed version1.1.0
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score0.213%
EPSS Percentile12th percentile
Description

Summary

When dispatching a request, HTTPEndpoint selects the handler by lowercasing the HTTP method and looking it up as an attribute with getattr, without restricting the lookup to a known set of HTTP verbs.

When an HTTPEndpoint subclass is registered through Route(...) without an explicit methods= argument, the route does not constrain the method and every method reaches the endpoint. If a non-standard HTTP method whose lowercased name matches an attribute on the endpoint subclass reaches the endpoint, that attribute is invoked as if it were a request handler. An attacker can use this to reach methods that were never meant to be HTTP handlers, such as internal helpers, without the authorization checks applied by the intended public handler.

Details

HTTPEndpoint uses the client-supplied method name to resolve an instance attribute, without validating it against the set of HTTP verbs the endpoint supports. A method such as _DO_DELETE therefore resolves an attribute like _do_delete and invokes it. Non-standard methods are valid RFC 9110 token methods, so an endpoint must not treat the method name as a trusted attribute selector.

Impact

An application is affected when all of the following hold:

  • It defines an HTTPEndpoint subclass and registers it via Route(...) without an explicit methods= argument.
  • The subclass defines additional methods whose names match a non-standard HTTP-method token shape and that accept a single request argument and return a response.

This also affects frameworks built on Starlette, like FastAPI.

Mitigation

Register HTTPEndpoint subclasses with an explicit methods= argument on the Route, listing only the HTTP verbs the endpoint supports. The route then rejects any other method with 405 Method Not Allowed before it reaches the endpoint, so non-standard methods cannot resolve an attribute.

low 3.7: CVE--2026--54282 Improper Input Validation

Affected range<1.3.0
Fixed version1.3.0
CVSS Score3.7
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score0.187%
EPSS Percentile9th percentile
Description

Summary

In affected versions, the HTTP request path is not validated before being used to reconstruct request.url. Because request.url is rebuilt by concatenating {scheme}://{host}{path} and re-parsing the result, a path that does not begin with / (for example @<!-- -->google.com) moves the authority boundary during re-parsing, so request.url.hostname and request.url.netloc become attacker-controlled. Code that reads request.url.hostname (rather than the Host header or scope) can therefore be misled into trusting an attacker-supplied host.

Details

When a client requests a path that does not start with /:

GET @<!-- -->google.com HTTP/1.1
Host: localhost

affected versions reconstruct the URL as http://localhost@<!-- -->google.com. Per RFC 3986 §3.2.1, the substring before @ in the authority is userinfo, so re-parsing yields username = "localhost" and hostname = "google.com", with an empty path:

request.url          == "http://localhost@<!-- -->google.com"
request.url.hostname == "google.com"
request.url.path     == ""

The root cause is that the path is concatenated directly after the host without a separating /, and without validating that it begins with one. Only the Host header was validated when constructing request.url; the path was not.

This requires an ASGI server that forwards a request-target lacking a leading / into scope["path"].

Impact

Any application running an affected version that uses request.url, request.url.netloc, or request.url.hostname for a security-sensitive decision (host-based authorization, redirect/callback base, SSRF target, cache key, audit log) may be affected, when no fronting proxy or load balancer rejects the malformed request-target first.

Note that this is less exploitable than GHSA-86qp-5c8j-p5mr: there, the poison is carried in the Host header, so the real path still routes to a valid endpoint while request.url.path lies. Here, the poison must be carried in the path itself, and that path (@<!-- -->google.com) does not match any registered route, so routing returns 404 and no endpoint handler runs. The exposure is limited to code that reads request.url before routing - notably middleware - or in 404/exception handlers.

Mitigation

Upgrade to a patched version, which prevents the request path from crossing into the URL authority. The request above instead yields http://localhost/@<!-- -->google.com with request.url.hostname == "localhost".

critical: 0 high: 2 medium: 0 low: 0 unspecified: 1golang.org/x/crypto 0.55.0 (golang)

pkg:golang/golang.org/x/crypto@0.55.0

high : CVE--2026--78662

Affected range<0.56.0
Fixed version0.56.0
EPSS Score0.315%
EPSS Percentile25th percentile
Description

Previously, a channel registered in the mux's chanList is not usable until it is established. A malicious peer was able flood the channel's incomingRequests, deadlocking the entire connection.

Now, we add an atomic established state, set when a channel becomes usable. Until such a time, handlePacket drops every packet other than the open confirmation/failure, without blocking and without tearing down the connection.

high : CVE--2026--56855

Affected range<0.56.0
Fixed version0.56.0
EPSS Score0.378%
EPSS Percentile32nd percentile
Description

Previously, after a channel has been established, a malicious peer could send crafted messages that would deadlock the entire connection.

Now, we handle all RFC 4254 channel messages; global requests are handled explicitly. Then, treat all other messages as a protocol error and tear the connection down instead of buffering and blocking.

unspecified : GO--2026--5932

Affected range>=0
Fixed versionNot Fixed
Description

The golang.org/x/crypto/openpgp package is unsafe by design, has numerous known security issues, is not maintained, and should not be used.

If you are required to interoperate with OpenPGP systems and need a maintained package, consider github.com/ProtonMail/go-crypto/openpgp which is a maintained fork that aims to be a drop-in replacement for this package.

critical: 0 high: 1 medium: 0 low: 0 ddtrace 3.19.8 (pypi)

pkg:pypi/ddtrace@3.19.8

high 7.5: CVE--2026--50271 Uncontrolled Resource Consumption

Affected range<4.8.2
Fixed version4.8.2
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.793%
EPSS Percentile55th percentile
Description

Impact

Datadog tracing libraries that implement W3C baggage propagation parse incoming baggage HTTP headers without enforcing item-count or byte-size limits on the extract path. The DD_TRACE_BAGGAGE_MAX_ITEMS (default 64) and DD_TRACE_BAGGAGE_MAX_BYTES (default 8192) limits were applied only to baggage injection, not extraction. A remote, unauthenticated attacker can send a request whose baggage header contains an arbitrarily large number of comma-separated key-value pairs (or a single very large value). The tracer allocates a hash-map entry for each pair on every request, causing unbounded CPU and memory consumption and enabling a remote Denial of Service against any HTTP service that has the baggage propagation style enabled.
The baggage propagation style is enabled by default in most affected tracers, so any internet-facing service that has been instrumented with an affected tracer version is exposed unless the propagation style has been explicitly narrowed.

Patches

This is resolved in version 4.8.2 and later of the dd-trace-py library

Workarounds

If users cannot upgrade immediately:

  1. Disable baggage extraction by removing baggage from DD_TRACE_PROPAGATION_STYLE (or DD_TRACE_PROPAGATION_STYLE_EXTRACT if set independently).
  2. Cap the maximum HTTP request header size at an upstream proxy or web server (for example, Apache LimitRequestFieldSize, Nginx large_client_header_buffers, Envoy max_request_headers_kb).

Resources

Related upstream advisories:
opentelemetry-go GHSA-mh2q-q3fh-2475
opentelemetry-dotnet GHSA-g94r-2vxg-569j

critical: 0 high: 0 medium: 1 low: 0 busybox 1.37.0-r30 (apk)

pkg:apk/alpine/busybox@1.37.0-r30?os_name=alpine&os_version=3.23

medium : CVE--2025--60876

Affected range<=1.37.0-r30
Fixed versionNot Fixed
EPSS Score0.291%
EPSS Percentile22nd percentile
Description
critical: 0 high: 0 medium: 1 low: 0 github.com/containerd/containerd/v2 2.2.5 (golang)

pkg:golang/github.com/containerd/containerd/v2@2.2.5

medium 6.8: CVE--2026--53495 Uncontrolled Resource Consumption

Affected range>=2.2.0
<2.2.8
Fixed version2.2.8
CVSS Score6.8
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.193%
EPSS Percentile9th percentile
Description

Impact

A bug in containerd's CRI ExecSync implementation allows exec probes and lifecycle hooks with background child processes to keep containerd's stdio-drain goroutines indefinitely blocked. Because the I/O drain phase lacks a default timeout or context cancellation handling, repeated ExecSync invocations (like probes) that include long-lived background processes against a container can cause containerd to leak goroutines and host memory. Over time, this resource exhaustion can cause the containerd daemon to be terminated by the OOM killer, rendering containerd unavailable until it is restarted. This issue affects containerd on Linux systems running with the CRI plugin enabled. Users not using containerd's CRI implementation or not running containers on Linux are not affected.

Patches

This bug has been fixed in containerd 2.3.5, 2.2.8, 2.0.12, and 1.7.35. Users should update to these versions to resolve the issue.

Workarounds

Ensure exec probes and lifecycle hooks do not launch long-lived background child processes.

Credits

The containerd project would like to thank XlabAI Team of Tencent Xuanwu Lab (xlabai@tencent.com), including Guannan Wang, Zhanpeng Liu, Jiashuo Liang, and Guancheng Li, and @IamwhatIamSY who independently discovered and responsibly disclosed this issue in accordance with the containerd security policy.

For more information

If there are any questions or comments about this advisory:

  • Open an issue in containerd
  • Send an email to [security@containerd.io](mailto:security@containerd.io)

To report a security issue in containerd:

critical: 0 high: 0 medium: 0 low: 1 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc 1.43.0 (golang)

pkg:golang/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@1.43.0

low 2.0: CVE--2026--81870 Exposure of Sensitive Information to an Unauthorized Actor

Affected range>=1.5.0
<=1.44.0
Fixed version1.45.0
CVSS Score2
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
EPSS Score0.187%
EPSS Percentile9th percentile
Description

Summary

OpenTelemetry Go versions 1.5.0 through 1.44.0 can include trace exporter endpoint configuration in an internal diagnostic log emitted when an SDK TracerProvider is created. The default OpenTelemetry logger does not emit this event. Exposure requires an application to install a logger that enables OpenTelemetry's internal Info-level diagnostics and for someone other than the intended audience to have access to those logs.

The logged configuration can disclose the address of the trace collector and whether the OTLP/HTTP connection is configured as insecure. The Zipkin exporter logs its complete collector URL, so credentials in URL userinfo or tokens in the query string are also disclosed if an application embeds them there. OTLP authentication headers, TLS key material, and exported span data are not included in this log.

Exporter MarshalLog implementations that caused this configuration to be included in internal logs were introduced by a1fff3c.

Details

When sdk/trace.NewTracerProvider constructs a provider, it records a TracerProvider created internal Info event containing the provider configuration. In affected versions, the configuration's MarshalLog methods recursively include:

  1. the provider's span processors;
  2. each processor's span exporter; and
  3. for the OTLP trace exporter, its client configuration.

This causes the following values to be present in the event:

  • OTLP trace gRPC: the configured endpoint;
  • OTLP trace HTTP: the configured endpoint and the Insecure flag; and
  • Zipkin: the complete collector URL.

OpenTelemetry Go does not emit this event with its default logger, which only emits errors. An application must explicitly configure a sufficiently verbose logger with otel.SetLogger. The required logr verbosity is version-dependent:

  • versions 1.5.0 through 1.14.x use V(1) for this Info event; and
  • versions 1.15.0 through 1.44.0 use V(4).

OTLP header configuration is not part of the marshaled object, so credentials supplied with WithHeaders or the corresponding environment variables are not exposed. The documented OTLP WithEndpoint input is a collector address rather than a credential-bearing URL. The higher-risk case is therefore the Zipkin collector URL, which is retained and logged in full, or an application passing sensitive data in an OTLP endpoint outside the documented format.

Proof of concept

The following program demonstrates the behavior with OpenTelemetry Go 1.44.0. It deliberately places credentials and a token in the Zipkin collector URL and enables internal Info logging:

package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/go-logr/logr/funcr"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/zipkin"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	var logs bytes.Buffer
	otel.SetLogger(funcr.New(func(_, args string) {
		_, _ = logs.WriteString(args)
	}, funcr.Options{Verbosity: 4}))

	exporter, err := zipkin.New(
		"http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret",
	)
	if err != nil {
		panic(err)
	}

	tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
	_ = tp.Shutdown(context.Background())

	fmt.Println(logs.String())
}

The TracerProvider created event contains:

http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret

For versions before 1.15.0, set funcr.Options{Verbosity: 1} instead.

Impact

This is a conditional disclosure through application logs. Affected applications must enable verbose OpenTelemetry internal diagnostics and configure a trace exporter containing information they do not intend to expose to readers of those logs. In that configuration, a person or system with log access can learn the trace collector address and internal network topology. If credentials or tokens are embedded directly in a Zipkin collector URL, those values can also be recovered from the logs.

There is no exposure with the default OpenTelemetry logger, and the vulnerable log is generated from local application configuration rather than remotely supplied span data. OTLP authentication headers, certificate or private-key contents, and telemetry payloads are not logged by this path.

Remediation

Upgrade the affected OpenTelemetry Go modules to version 1.45.0 or later. The fix in 3a1412d stops recursively marshaling exporter and client configuration and records their types instead.

If an immediate upgrade is not possible:

  • keep OpenTelemetry internal logging below the Info verbosity described above;
  • do not embed credentials or tokens in exporter endpoint URLs; use authentication headers or another supported credential mechanism; and
  • restrict access to existing logs and rotate any credentials that may already have been recorded.
critical: 0 high: 0 medium: 0 low: 1 go.opentelemetry.io/otel/exporters/otlp/otlptrace 1.43.0 (golang)

pkg:golang/go.opentelemetry.io/otel/exporters/otlp/otlptrace@1.43.0

low 2.0: CVE--2026--81870 Exposure of Sensitive Information to an Unauthorized Actor

Affected range>=1.5.0
<=1.44.0
Fixed version1.45.0
CVSS Score2
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
EPSS Score0.187%
EPSS Percentile9th percentile
Description

Summary

OpenTelemetry Go versions 1.5.0 through 1.44.0 can include trace exporter endpoint configuration in an internal diagnostic log emitted when an SDK TracerProvider is created. The default OpenTelemetry logger does not emit this event. Exposure requires an application to install a logger that enables OpenTelemetry's internal Info-level diagnostics and for someone other than the intended audience to have access to those logs.

The logged configuration can disclose the address of the trace collector and whether the OTLP/HTTP connection is configured as insecure. The Zipkin exporter logs its complete collector URL, so credentials in URL userinfo or tokens in the query string are also disclosed if an application embeds them there. OTLP authentication headers, TLS key material, and exported span data are not included in this log.

Exporter MarshalLog implementations that caused this configuration to be included in internal logs were introduced by a1fff3c.

Details

When sdk/trace.NewTracerProvider constructs a provider, it records a TracerProvider created internal Info event containing the provider configuration. In affected versions, the configuration's MarshalLog methods recursively include:

  1. the provider's span processors;
  2. each processor's span exporter; and
  3. for the OTLP trace exporter, its client configuration.

This causes the following values to be present in the event:

  • OTLP trace gRPC: the configured endpoint;
  • OTLP trace HTTP: the configured endpoint and the Insecure flag; and
  • Zipkin: the complete collector URL.

OpenTelemetry Go does not emit this event with its default logger, which only emits errors. An application must explicitly configure a sufficiently verbose logger with otel.SetLogger. The required logr verbosity is version-dependent:

  • versions 1.5.0 through 1.14.x use V(1) for this Info event; and
  • versions 1.15.0 through 1.44.0 use V(4).

OTLP header configuration is not part of the marshaled object, so credentials supplied with WithHeaders or the corresponding environment variables are not exposed. The documented OTLP WithEndpoint input is a collector address rather than a credential-bearing URL. The higher-risk case is therefore the Zipkin collector URL, which is retained and logged in full, or an application passing sensitive data in an OTLP endpoint outside the documented format.

Proof of concept

The following program demonstrates the behavior with OpenTelemetry Go 1.44.0. It deliberately places credentials and a token in the Zipkin collector URL and enables internal Info logging:

package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/go-logr/logr/funcr"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/zipkin"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	var logs bytes.Buffer
	otel.SetLogger(funcr.New(func(_, args string) {
		_, _ = logs.WriteString(args)
	}, funcr.Options{Verbosity: 4}))

	exporter, err := zipkin.New(
		"http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret",
	)
	if err != nil {
		panic(err)
	}

	tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
	_ = tp.Shutdown(context.Background())

	fmt.Println(logs.String())
}

The TracerProvider created event contains:

http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret

For versions before 1.15.0, set funcr.Options{Verbosity: 1} instead.

Impact

This is a conditional disclosure through application logs. Affected applications must enable verbose OpenTelemetry internal diagnostics and configure a trace exporter containing information they do not intend to expose to readers of those logs. In that configuration, a person or system with log access can learn the trace collector address and internal network topology. If credentials or tokens are embedded directly in a Zipkin collector URL, those values can also be recovered from the logs.

There is no exposure with the default OpenTelemetry logger, and the vulnerable log is generated from local application configuration rather than remotely supplied span data. OTLP authentication headers, certificate or private-key contents, and telemetry payloads are not logged by this path.

Remediation

Upgrade the affected OpenTelemetry Go modules to version 1.45.0 or later. The fix in 3a1412d stops recursively marshaling exporter and client configuration and records their types instead.

If an immediate upgrade is not possible:

  • keep OpenTelemetry internal logging below the Info verbosity described above;
  • do not embed credentials or tokens in exporter endpoint URLs; use authentication headers or another supported credential mechanism; and
  • restrict access to existing logs and rotate any credentials that may already have been recorded.
critical: 0 high: 0 medium: 0 low: 1 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp 1.43.0 (golang)

pkg:golang/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@1.43.0

low 2.0: CVE--2026--81870 Exposure of Sensitive Information to an Unauthorized Actor

Affected range>=1.5.0
<=1.44.0
Fixed version1.45.0
CVSS Score2
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
EPSS Score0.187%
EPSS Percentile9th percentile
Description

Summary

OpenTelemetry Go versions 1.5.0 through 1.44.0 can include trace exporter endpoint configuration in an internal diagnostic log emitted when an SDK TracerProvider is created. The default OpenTelemetry logger does not emit this event. Exposure requires an application to install a logger that enables OpenTelemetry's internal Info-level diagnostics and for someone other than the intended audience to have access to those logs.

The logged configuration can disclose the address of the trace collector and whether the OTLP/HTTP connection is configured as insecure. The Zipkin exporter logs its complete collector URL, so credentials in URL userinfo or tokens in the query string are also disclosed if an application embeds them there. OTLP authentication headers, TLS key material, and exported span data are not included in this log.

Exporter MarshalLog implementations that caused this configuration to be included in internal logs were introduced by a1fff3c.

Details

When sdk/trace.NewTracerProvider constructs a provider, it records a TracerProvider created internal Info event containing the provider configuration. In affected versions, the configuration's MarshalLog methods recursively include:

  1. the provider's span processors;
  2. each processor's span exporter; and
  3. for the OTLP trace exporter, its client configuration.

This causes the following values to be present in the event:

  • OTLP trace gRPC: the configured endpoint;
  • OTLP trace HTTP: the configured endpoint and the Insecure flag; and
  • Zipkin: the complete collector URL.

OpenTelemetry Go does not emit this event with its default logger, which only emits errors. An application must explicitly configure a sufficiently verbose logger with otel.SetLogger. The required logr verbosity is version-dependent:

  • versions 1.5.0 through 1.14.x use V(1) for this Info event; and
  • versions 1.15.0 through 1.44.0 use V(4).

OTLP header configuration is not part of the marshaled object, so credentials supplied with WithHeaders or the corresponding environment variables are not exposed. The documented OTLP WithEndpoint input is a collector address rather than a credential-bearing URL. The higher-risk case is therefore the Zipkin collector URL, which is retained and logged in full, or an application passing sensitive data in an OTLP endpoint outside the documented format.

Proof of concept

The following program demonstrates the behavior with OpenTelemetry Go 1.44.0. It deliberately places credentials and a token in the Zipkin collector URL and enables internal Info logging:

package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/go-logr/logr/funcr"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/zipkin"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	var logs bytes.Buffer
	otel.SetLogger(funcr.New(func(_, args string) {
		_, _ = logs.WriteString(args)
	}, funcr.Options{Verbosity: 4}))

	exporter, err := zipkin.New(
		"http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret",
	)
	if err != nil {
		panic(err)
	}

	tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
	_ = tp.Shutdown(context.Background())

	fmt.Println(logs.String())
}

The TracerProvider created event contains:

http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret

For versions before 1.15.0, set funcr.Options{Verbosity: 1} instead.

Impact

This is a conditional disclosure through application logs. Affected applications must enable verbose OpenTelemetry internal diagnostics and configure a trace exporter containing information they do not intend to expose to readers of those logs. In that configuration, a person or system with log access can learn the trace collector address and internal network topology. If credentials or tokens are embedded directly in a Zipkin collector URL, those values can also be recovered from the logs.

There is no exposure with the default OpenTelemetry logger, and the vulnerable log is generated from local application configuration rather than remotely supplied span data. OTLP authentication headers, certificate or private-key contents, and telemetry payloads are not logged by this path.

Remediation

Upgrade the affected OpenTelemetry Go modules to version 1.45.0 or later. The fix in 3a1412d stops recursively marshaling exporter and client configuration and records their types instead.

If an immediate upgrade is not possible:

  • keep OpenTelemetry internal logging below the Info verbosity described above;
  • do not embed credentials or tokens in exporter endpoint URLs; use authentication headers or another supported credential mechanism; and
  • restrict access to existing logs and rotate any credentials that may already have been recorded.
critical: 0 high: 0 medium: 0 low: 1 go.opentelemetry.io/otel/sdk 1.44.0 (golang)

pkg:golang/go.opentelemetry.io/otel/sdk@1.44.0

low 2.0: CVE--2026--81870 Exposure of Sensitive Information to an Unauthorized Actor

Affected range>=1.5.0
<=1.44.0
Fixed version1.45.0
CVSS Score2
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
EPSS Score0.187%
EPSS Percentile9th percentile
Description

Summary

OpenTelemetry Go versions 1.5.0 through 1.44.0 can include trace exporter endpoint configuration in an internal diagnostic log emitted when an SDK TracerProvider is created. The default OpenTelemetry logger does not emit this event. Exposure requires an application to install a logger that enables OpenTelemetry's internal Info-level diagnostics and for someone other than the intended audience to have access to those logs.

The logged configuration can disclose the address of the trace collector and whether the OTLP/HTTP connection is configured as insecure. The Zipkin exporter logs its complete collector URL, so credentials in URL userinfo or tokens in the query string are also disclosed if an application embeds them there. OTLP authentication headers, TLS key material, and exported span data are not included in this log.

Exporter MarshalLog implementations that caused this configuration to be included in internal logs were introduced by a1fff3c.

Details

When sdk/trace.NewTracerProvider constructs a provider, it records a TracerProvider created internal Info event containing the provider configuration. In affected versions, the configuration's MarshalLog methods recursively include:

  1. the provider's span processors;
  2. each processor's span exporter; and
  3. for the OTLP trace exporter, its client configuration.

This causes the following values to be present in the event:

  • OTLP trace gRPC: the configured endpoint;
  • OTLP trace HTTP: the configured endpoint and the Insecure flag; and
  • Zipkin: the complete collector URL.

OpenTelemetry Go does not emit this event with its default logger, which only emits errors. An application must explicitly configure a sufficiently verbose logger with otel.SetLogger. The required logr verbosity is version-dependent:

  • versions 1.5.0 through 1.14.x use V(1) for this Info event; and
  • versions 1.15.0 through 1.44.0 use V(4).

OTLP header configuration is not part of the marshaled object, so credentials supplied with WithHeaders or the corresponding environment variables are not exposed. The documented OTLP WithEndpoint input is a collector address rather than a credential-bearing URL. The higher-risk case is therefore the Zipkin collector URL, which is retained and logged in full, or an application passing sensitive data in an OTLP endpoint outside the documented format.

Proof of concept

The following program demonstrates the behavior with OpenTelemetry Go 1.44.0. It deliberately places credentials and a token in the Zipkin collector URL and enables internal Info logging:

package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/go-logr/logr/funcr"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/zipkin"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	var logs bytes.Buffer
	otel.SetLogger(funcr.New(func(_, args string) {
		_, _ = logs.WriteString(args)
	}, funcr.Options{Verbosity: 4}))

	exporter, err := zipkin.New(
		"http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret",
	)
	if err != nil {
		panic(err)
	}

	tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
	_ = tp.Shutdown(context.Background())

	fmt.Println(logs.String())
}

The TracerProvider created event contains:

http://user:pass@<!-- -->zipkin.internal:9411/api/v2/spans?token=secret

For versions before 1.15.0, set funcr.Options{Verbosity: 1} instead.

Impact

This is a conditional disclosure through application logs. Affected applications must enable verbose OpenTelemetry internal diagnostics and configure a trace exporter containing information they do not intend to expose to readers of those logs. In that configuration, a person or system with log access can learn the trace collector address and internal network topology. If credentials or tokens are embedded directly in a Zipkin collector URL, those values can also be recovered from the logs.

There is no exposure with the default OpenTelemetry logger, and the vulnerable log is generated from local application configuration rather than remotely supplied span data. OTLP authentication headers, certificate or private-key contents, and telemetry payloads are not logged by this path.

Remediation

Upgrade the affected OpenTelemetry Go modules to version 1.45.0 or later. The fix in 3a1412d stops recursively marshaling exporter and client configuration and records their types instead.

If an immediate upgrade is not possible:

  • keep OpenTelemetry internal logging below the Info verbosity described above;
  • do not embed credentials or tokens in exporter endpoint URLs; use authentication headers or another supported credential mechanism; and
  • restrict access to existing logs and rotate any credentials that may already have been recorded.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

🔍 Vulnerabilities of permitio/pdp-v2:next

📦 Image Reference permitio/pdp-v2:next
digestsha256:91da910734a9dc9dfb7e1c72eeeba85e97390a8b1d1e2c2f4ee43cec26f1f022
vulnerabilitiescritical: 0 high: 0 medium: 0 low: 0
platformlinux/amd64
size136 MB
packages247
📦 Base Image python:026d26881d2e1ebad06e4f309b0fc5f03c0471c5230d325d7b571be802586ee6
also known as
  • 3.13-alpine3.23
  • 3.13.15-alpine3.23
digestsha256:f282f385cfce21b0a644094330930704424a48d59afe8f08052e0f8e0b7a35c6
vulnerabilitiescritical: 0 high: 3 medium: 2 low: 0

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — no CRITICAL or HIGH issues found. 7 MEDIUM, 5 LOW.

Non-blocking:

  • MEDIUM Dockerfile:70 — Comment omits the go.mod directive/GODEBUG dependency it actually relies on
  • MEDIUM Dockerfile:74 — Major toolchain bump validated only by "it builds"; comment claims only glibc
  • MEDIUM Dockerfile:75 — Merge falsifies two CVE waivers whose removal gate names the closed PDP#334
  • MEDIUM Dockerfile:76 — opa_build base stays tag-only; the 1.26.6 floor and toolchain are unenforced
  • LOW Dockerfile:57 — Stated merge order omits permit-opa#51, which gates #52
  • LOW Dockerfile:78 — GOTOOLCHAIN=local is load-bearing but inherited, never asserted

Findings that could not be anchored to a line inside this PR's diff:

  • MEDIUM .github/workflows/tests.yml:52 — permit-opa is compiled into released images from an unpinned ref: main (file not present in this PR's diff)
    This PR's comment (Dockerfile:62-64) makes it load-bearing that "tests.yml and release.yml check out permitio/permit-opa at ref: main, unpinned, so a permit-opa merge reaches the very next PDP build". That property is the defect it is being used to justify: tests.yml:52 and release.yml:43 pin nothing, so every published permitio/pdp-v2 image compiles whatever was on another repo's default branch at release time, with no record of which commit produced /app/bin/opa and no review gate between a permit-opa merge and a customer artifact. It is also what turns this routine bump into a cross-repo merge-order hazard managed by hand in PR bodies. (release.yml:40 is additionally still actions/checkout@v3.)
    Suggestion: Pin the permit-opa checkout to a commit SHA or release tag in both workflows and bump it deliberately, so the shipped OPA source is reproducible and the ordering constraint becomes a version bump rather than a race.
  • MEDIUM .github/workflows/tests.yml:77 — arm64 leg of the new builder is first exercised at release, and never scanned (file not present in this PR's diff)
    build-pdp-image builds platforms: linux/amd64 only (tests.yml:77), while release.yml:62 and :114 build linux/amd64,linux/arm64. Neither build passes target:, so both resolve application -> main-permit -> main, which COPYs from opa_build (Dockerfile:233). So golang:1.26-bookworm/arm64 compiles the shipped OPA for the first time during a published release, under QEMU, and the Docker Scout gate (tests.yml:233-239) scans local://permitio/pdp-v2:next, the amd64 artifact - the arm64 binary's new go1.26 stdlib is never scanned before it reaches Docker Hub. The only arm64 evidence is a local --target opa_build run recorded in the PR body, and this stage has shipped an arm64-only defect before (Dockerfile:82, issue #289).
    Suggestion: Add linux/arm64 to the PR build, or add a cheap --target opa_build --platform linux/arm64 job, so a builder change fails on the PR rather than mid-release.
  • MEDIUM Dockerfile:95 — OPA_BUILD arg is disconnected from the custom-vs-vanilla branch (line 95 is not inside a diff hunk (nearest diff line: 79))
    Two independent switches decide the same thing. if [ -f /custom/custom_opa.tar.gz ] (line 95) picks the custom vs vanilla OPA binary, while ARG OPA_BUILD=permit (line 1) separately selects PDP_OPA_PLUGINS='{"permit_graph":{}}' (line 327) via FROM main-${OPA_BUILD} AS application (line 329). Nothing ties them, and no workflow or Makefile passes --build-arg OPA_BUILD (grep over .github/workflows and Makefile: zero hits), so it is always permit. A clean vanilla build therefore ships a vanilla OPA in an image declaring a plugin the Dockerfile itself says at line 319-320 "we MUST not add". build_opal_bundle.sh:19-29 compounds it: PDP_VANILLA=true skips the tarball but never clears custom/, so a stale tarball silently yields a custom build.
    Suggestion: Declare ARG OPA_BUILD inside opa_build, take the download branch only when OPA_BUILD=vanilla, and hard-fail when OPA_BUILD=permit and /custom/custom_opa.tar.gz is absent. Make build_opal_bundle.sh clear custom/ on both branches.
  • LOW Dockerfile:102 — -a negates the go-build cache mount and opa_build is not cross-compiled (line 102 is not inside a diff hunk (nearest diff line: 79))
    Lines 92-94 mount /go/pkg/mod and /root/.cache/go-build "for MUCH faster incremental builds", but line 102 passes -a, which forces a rebuild of every package including the stdlib, so the go-build cache mount never serves a compile (the module cache mount still helps). With CGO_ENABLED=0 the -a buys nothing - the build cache already keys on CGO_ENABLED and build tags. Compounding it, opa_build has no --platform=$BUILDPLATFORM (unlike rust_chef at line 12), so release.yml's linux/amd64,linux/arm64 runs that full stdlib+OPA rebuild under QEMU for the arm64 leg; release.yml:93-94 already notes "opa_build and main are each built twice with the second leg under QEMU".
    Suggestion: Drop -a, and build once on the build platform using native Go cross-compilation, which removes QEMU from this stage.
  • LOW Dockerfile:106 — Vanilla OPA fallback downloads 'latest' with no checksum and no curl --fail (line 106 is not inside a diff hunk (nearest diff line: 79))
    In the stage this PR re-bases, the non-custom branch runs curl -L -o /opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static (and the aarch64 twin on line 107). Three gaps: latest means the build has no record of which OPA version shipped; there is no checksum or signature check on a binary that lands at /app/bin/opa (line 233) and evaluates every authorization decision; and without --fail, curl writes an HTTP error body to /opa and exits 0, so a CDN 404/5xx yields an image whose OPA binary is an HTML page while the build stays green. Reachability is narrow - CI always supplies custom_opa.tar.gz - but this is the shipped Dockerfile.
    Suggestion: Pin the OPA version, add --fail --show-error, and verify the published SHA256 before accepting the binary.
  • LOW test_offline_mode/Dockerfile:1 — test_offline_mode base has no version at all, let alone a digest (file not present in this PR's diff)
    Auditing the repo's other FROM lines while this one moves: test_offline_mode/Dockerfile:1 is FROM python:alpine - no major.minor, no Alpine suite, no digest. Every rebuild can land on a different CPython major and a different Alpine, so the offline-mode check can start failing (or passing) for reasons unrelated to the PDP, with no record of what it ran on. Not shipped to customers, hence LOW, but it is the last wholly unversioned base in the tree; Dockerfile:12 (rust:1.94-alpine) and Dockerfile:169 (python:3.13-alpine3.23) are at least tag-versioned.
    Suggestion: Give it an explicit, digest-pinned tag matching the main image's Python, e.g. python:3.13-alpine3.23@sha256:<digest>.

Details are in the inline comments on each line.

Comment thread Dockerfile Outdated
Comment thread Dockerfile Outdated
Comment thread Dockerfile Outdated
Comment thread Dockerfile
Comment thread Dockerfile
Comment thread Dockerfile
…/crypto waiver gates (PER-16045)

Review follow-up (Zeev):
- opa_build sets ENV GOTOOLCHAIN=local instead of relying on the base image,
  and fails the build if the builder is below go1.26.6 (GO-2026-6090) - the
  same shape as the CPython floor check in `main`. It also catches a merge
  that restores golang:1.25 (checked: a 1.25 builder fails on it).
- The comment names what the 1.26 toolchain changes in /app/bin/opa (Green
  Tea GC) versus what follows permit-opa's go.mod (GODEBUG defaults), and
  the merge order now includes permit-opa#51. Drops the closed PDP#334.
- pdp-v2.vex.json (version 11) and the tests.yml scout comment no longer
  claim the builder is golang:1.25 or gate removal on the closed PDP#334;
  the remaining gate is permit-opa#52.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 17, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EliMoshkovich

Copy link
Copy Markdown
Collaborator Author

@zeevmoney, here is what happened to the review findings that had no inline thread. None of them change in this PR; all are about code this diff doesn't touch.

  • MEDIUM tests.yml:52, permit-opa built from unpinned ref: main. Valid, but out of scope. Pinning the checkout to a SHA or tag changes how every PDP build and release picks up permit-opa, so it needs its own PR and an agreed bump process. Until then, the ordering constraint is written down in permit-opa's go.mod (permit-opa#52) and in this Dockerfile comment.
  • MEDIUM tests.yml:77, arm64 leg first built at release and never scanned. Valid, but out of scope and not new: the PR build has always been amd64-only. I built the arm64 opa_build stage locally with this change and permit-opa#52's source (go1.26.8, x/crypto v0.57.0, CGO_ENABLED=0). Follow-up: an arm64 --target opa_build PR job.
  • MEDIUM Dockerfile:95, OPA_BUILD disconnected from the custom-vs-vanilla branch. Not changing: it has nothing to do with the builder bump, and CI always supplies the tarball. Follow-up.
  • LOW Dockerfile:102, -a defeats the build cache; no $BUILDPLATFORM cross-compile. Not changing: build-speed work, unrelated to this PR.
  • LOW Dockerfile:106, vanilla fallback downloads latest without --fail or a checksum. Not changing here; CI never takes that branch. Follow-up.
  • LOW test_offline_mode/Dockerfile:1, unversioned base. Not changing: test-only and outside this diff.

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of the fixes. 3 MEDIUM, 9 LOW.

Fix verification — 12 findings from the 2026-09-17 review: 4 fixed, 2 partly fixed, 6 not fixed

Non-blocking:

  • LOW .docker/scout/pdp-v2.vex.json:23 — Waiver removal gate disagrees with permit-opa#52's own step 4
  • LOW .github/workflows/tests.yml:251 — PR description still says the waivers name the closed PDP#334 as the gate

Findings that could not be anchored to a line inside this PR's diff:

  • MEDIUM .github/workflows/tests.yml:52 — permit-opa is still compiled into released images from an unpinned ref: main (line 52 is not inside a diff hunk (nearest diff line: 248))
    Unchanged since the first review, and this PR now depends on it in three places. tests.yml:52 and release.yml:43 pin nothing, so every published permitio/pdp-v2 image compiles whatever was on another repo's default branch at release time, with no record of which commit produced /app/bin/opa and no review gate between a permit-opa merge and a customer artifact. Dockerfile:63-65 states that as the ordering mechanism, and both rewritten vex impact_statements make it the CVE removal gate. The concrete consequence is written into the same comment at Dockerfile:76-78: deleting one godebug default=go1.25 line in permit-opa (its go.mod:161 on #52) changes the shipped OPA binary on the next PDP build, with no PDP-side diff. release.yml:40 is additionally still actions/checkout@v3.
    Suggestion: Pin the permit-opa checkout to a commit SHA or release tag in both workflows and bump it deliberately, so the shipped OPA source is reproducible and the ordering constraint becomes a version bump rather than a race. Bump release.yml:40 to a SHA-pinned actions/checkout at the same time.
  • MEDIUM .github/workflows/tests.yml:77 — arm64 leg of the new Go 1.26 builder is still first compiled at release (line 77 is not inside a diff hunk (nearest diff line: 248))
    The previous round did not close this - no code changed and the PR body demotes it to a follow-up. tests.yml:77 builds platforms: linux/amd64; release.yml:62 and :114 build linux/amd64,linux/arm64. Neither passes target:, so both resolve application (Dockerfile:341) -> main-permit -> main, which COPYs from opa_build at Dockerfile:245. So golang:1.26-bookworm/arm64, and the new floor RUN at Dockerfile:86-88, first execute under QEMU while publishing to Docker Hub. The scout gate is pull_request-only (tests.yml:199) and scans local://permitio/pdp-v2:next (tests.yml:228, :237), the amd64 artifact, so the arm64 binary's new go1.26 stdlib is never scanned. This stage has shipped an arm64-only defect before (Dockerfile:94-97, issue #289).
    Suggestion: Add a cheap --target opa_build --platform linux/arm64 job to tests.yml (it stops before the Rust and Python stages), or add linux/arm64 to build-pdp-image, so a builder change fails on the PR rather than mid-release.
  • MEDIUM Dockerfile:107 — OPA_BUILD arg still decides the plugin, not which OPA binary ships (line 107 is not inside a diff hunk (nearest diff line: 91))
    Unchanged and unanswered. Two independent switches decide the same thing: if [ -f /custom/custom_opa.tar.gz ] (line 107) picks the custom vs vanilla OPA binary, while ARG OPA_BUILD=permit (line 1) separately selects PDP_OPA_PLUGINS='{"permit_graph":{}}' (line 339) through FROM main-${OPA_BUILD} AS application (line 341). A repo-wide grep for OPA_BUILD matches only lines 1 and 341 - no workflow, Makefile or compose file passes --build-arg OPA_BUILD - so it is always permit and main-vanilla (line 333) is unbuildable. A vanilla build therefore ships a vanilla OPA declaring a plugin the file itself says at lines 331-332 we MUST not add. build_opal_bundle.sh:19-22 compounds it: PDP_VANILLA=true skips the tarball but never clears custom/, so a stale tarball silently yields a custom build.
    Suggestion: Declare ARG OPA_BUILD inside opa_build, take the download branch only when OPA_BUILD=vanilla, and hard-fail when OPA_BUILD=permit and /custom/custom_opa.tar.gz is absent. Move rm -rf custom; mkdir custom out of the conditional in build_opal_bundle.sh.
  • LOW .github/workflows/release.yml:77 — The stated reason for excluding opa_build from no-cache-filters is now weaker (file not present in this PR's diff)
    Lines 77-79 justify leaving opa_build out of no-cache-filters with "COPY custom* /custom pulls in custom_opa.tar.gz, which the Pre-build step regenerates every run, so that stage already re-executes unconditionally." This PR inserts ENV GOTOOLCHAIN=local and the go1.26.6 floor RUN (Dockerfile:85-88) ahead of that COPY at Dockerfile:90. Those two layers are keyed only on the resolved base image digest, so they can be served from cache-from: type=gha whenever golang:1.26-bookworm has not moved - the stage no longer re-executes end to end. The behaviour stays correct (a moved digest changes the key and re-runs the check), but the prose overstates it, and Dockerfile:82-83 reads as though the floor check runs on every build.
    Suggestion: Add a clause to release.yml:77-81 saying the stage's pre-COPY preamble (GOTOOLCHAIN plus the floor check) caches on the base image digest, which is why it still does not need the filter.
  • LOW Dockerfile:114 — -a still defeats the go-build cache mount; opa_build still not cross-compiled (line 114 is not inside a diff hunk (nearest diff line: 91))
    Unchanged and unanswered, and the bump makes it more expensive. Lines 105-106 mount /go/pkg/mod and /root/.cache/go-build "for MUCH faster incremental builds", but line 114 passes -a, forcing a rebuild of every package including the whole Go 1.26 stdlib, so the build-cache mount never serves a compile. With CGO_ENABLED=0 the -a buys nothing - the cache already keys on CGO_ENABLED and build tags - and its self-justification at line 99 is cargo-culted. Line 84 also has no --platform=$BUILDPLATFORM (unlike line 12), so release.yml's arm64 leg runs that full rebuild under QEMU; release.yml:93-94 already notes it. The sibling permit-opa#52 went the other way and cross-compiles from $BUILDPLATFORM.
    Suggestion: Drop -a and build once on the build platform with --platform=$BUILDPLATFORM plus GOARCH from $TARGETARCH. The fix is only complete if the vanilla branch's case $(uname -m) (lines 117-121) moves to $TARGETARCH at the same time - on a BUILDPLATFORM-pinned stage uname -m reports the builder's arch and would download the wrong binary.
  • LOW Dockerfile:118 — New floor guards the stage, not the binary the vanilla branch actually ships (line 118 is not inside a diff hunk (nearest diff line: 91))
    Unchanged since the first review, and the new floor check does not reach it. When /custom/custom_opa.tar.gz is absent (line 107) the stage compiles nothing: lines 118-119 run curl -L -o /opa https://openpolicyagent.org/downloads/latest/opa_linux_{amd64,arm64}_static and that prebuilt binary is what line 245 copies to /app/bin/opa. latest records no version; there is no checksum or signature on a binary that evaluates every authorization decision; and without --fail, curl writes an HTTP error body to /opa and exits 0, so a CDN 404/5xx yields a green build whose OPA binary is an HTML page. The go1.26.6 / GO-2026-6090 guarantee stated at lines 81-83 does not describe this branch's binary at all.
    Suggestion: Pin the OPA version, add --fail --show-error, verify the published SHA256 before accepting the binary, and say at lines 81-83 that the floor binds only the custom-tarball branch.
  • LOW Dockerfile:276 — ensurepip path hardcodes python3.13 while the floor check branches for 3.14 (line 276 is not inside a diff hunk (nearest diff line: 91))
    Pre-existing and outside this PR's subject, raised from a full read of the file. The CPython floor check at line 241 branches per minor ({13: (3, 13, 15), 14: (3, 14, 7)}) and the comment at lines 142-145 explains that the base may resolve to the 3.14 line. Line 276 then runs rm -r /usr/local/lib/python3.13/ensurepip, a hardcoded minor, and rm -r without -f exits non-zero on a missing path. Exactly one of the two is right: with FROM python:3.13-alpine3.23 (line 181) the 3.14 branch of the floor check is unreachable, and the day anyone moves that tag, line 276 fails the build. A reader has to resolve the contradiction to know which.
    Suggestion: Derive the path instead of hardcoding it (e.g. from sysconfig's stdlib path), or drop the 3.14 branch from the floor check so both places agree the base is 3.13-only.
  • LOW Dockerfile:284 — Duplicate USER permit instruction is dead (line 284 is not inside a diff hunk (nearest diff line: 91))
    Pre-existing, noticed while reading the whole file for this PR's multi-stage audit. Line 279 is USER permit and line 284 repeats it verbatim, with only COPY ./horizon /app/horizon (line 282) between them - a COPY does not change the user, so line 284 is a no-op. It is a metadata-only history entry rather than a filesystem layer, so the cost is confusion rather than image size: a reader looking for where the image drops privileges finds two answers.
    Suggestion: Delete line 284.
  • LOW build_opal_bundle.sh:3 — build_opal_bundle.sh uses set -e instead of set -euo pipefail (file not present in this PR's diff)
    Pre-existing, and it is the script that produces the exact input this PR's builder compiles. Line 3 is set -e only. Line 25 is a pipeline - find * \( -name '*go*' -o -name 'LICENSE.md' \) -print0 | xargs -0 tar -czf .../custom_opa.tar.gz --exclude '.*' - whose exit status without pipefail is tar's alone, so a failing find produces a green run and a tarball that may be short. The Dockerfile then decides custom-vs-vanilla purely on [ -f /custom/custom_opa.tar.gz ] (Dockerfile:107), so whatever this script leaves behind fully determines which OPA binary ships.
    Suggestion: Change line 3 to set -euo pipefail and re-run shellcheck. Pair it with ${PDP_VANILLA:-} on lines 6 and 19: both read the variable bare today, so adding -u on its own would abort the script with "unbound variable" whenever PDP_VANILLA is not exported, which is the normal case.
  • LOW test_offline_mode/Dockerfile:1 — test_offline_mode base still has no version at all, let alone a digest (file not present in this PR's diff)
    Unchanged and unanswered. FROM python:alpine has no major.minor, no Alpine suite and no digest, while this PR's own subject is which base a builder resolves to. Every rebuild can land on a different CPython major and a different Alpine, so the offline-mode check can start failing (or passing) for reasons unrelated to the PDP, with no record of what it ran on; test_offline_mode/docker-compose.yaml builds it for both the online and offline testers. Not shipped to customers, hence LOW, but it is the last wholly unversioned base in the tree: Dockerfile:12 (rust:1.94-alpine) and Dockerfile:181 (python:3.13-alpine3.23) are at least tag-versioned, and PDP#338 digest-pins all three of those.
    Suggestion: Give it an explicit, digest-pinned tag matching the main image's Python, e.g. python:3.13-alpine3.23@sha256:<digest>.

Already raised in an existing thread (not re-posted):

  • MEDIUM Dockerfile:84 — Base is still a floating tag; nothing records which 1.26.x built the binary
  • LOW Dockerfile:86 — The go1.26.6 floor added here is absent from permit-opa's own builder

Details are in the inline comments on each line.

Comment thread .docker/scout/pdp-v2.vex.json Outdated
Comment thread .github/workflows/tests.yml

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 further finding(s) from the re-review of this PR.

1 of them restate a finding from the 2026-09-17 review whose fix did not close it. They were left out of the re-review comment in error: they matched the original thread, which is now resolved, and my de-duplication treated that as already-raised. A resolved thread is a closed conversation, so they belong on fresh threads. The verdict already submitted on this PR is unchanged.

Comment thread Dockerfile
Comment thread Dockerfile
…s (PER-16045)

Review follow-up (Zeev, round 2):

- Dockerfile: the patch version floats on purpose (Go security releases
  arrive without a bump), and the exact toolchain is recorded in the
  binary's build info, which is what scanners read. The floor check caches
  on the base digest, and it binds only the branch that compiles
  permit-opa, not the prebuilt-OPA fallback.
- release.yml: opa_build re-executes from `COPY custom*` on; the two layers
  before it cache on the base image digest.
- Dockerfile: drop the second `USER permit` (a no-op after a COPY).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 18, 2026 19:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EliMoshkovich

Copy link
Copy Markdown
Collaborator Author

Findings with no inline thread (5d84477). I re-checked the six you marked not fixed. All are real, and all predate this PR, which changes only the builder version. None was answered last round. Here is each one:

  • MEDIUM tests.yml:52 / release.yml:43 unpinned ref: main. Not changing here. Pinning is right for reproducibility, but it changes how the PDP consumes permit-opa: every permit-opa fix would then need a PDP bump PR, and nothing here would renew that pin. That is a decision for Eli/Omer, not a side effect of a builder bump. The ordering hazard it caused now has a guard on the permit-opa side: permit-opa#52's pdp-builder check reads this repo's Dockerfile on main and fails until this PR is merged. release.yml:40's actions/checkout@v3 is untouched by this PR; follow-up.
  • MEDIUM tests.yml:77 arm64 leg only at release. Not adding a job here. For this change, arm64 is the leg I did build: every local run in the PR body, including this round's with permit-opa#52 at 31d5dda, was --platform linux/arm64 --target opa_build. The result is go1.26.8, x/crypto v0.57.0, CGO_ENABLED=0, with no interpreter or NEEDED entry. A standing arm64 PR job is worth having, but it is a QEMU-cost decision of its own; follow-up.
  • MEDIUM Dockerfile:107 OPA_BUILD vs the tarball check. Not changing. It is real, but no workflow builds vanilla, and reworking which switch picks the OPA binary changes what a tarball-less build ships. That belongs in its own PR, together with the build_opal_bundle.sh cleanup.
  • LOW release.yml:77 prose. Fixed in 5d84477. It now says the stage re-executes from COPY custom* on, that ENV GOTOOLCHAIN and the floor check cache on the base image digest, and that a moved tag re-runs the check. The Dockerfile comment says the same.
  • LOW Dockerfile:114 -a. Not changing. It costs build time, not correctness. As you note, the complete fix (BUILDPLATFORM plus TARGETARCH, and moving the vanilla branch off uname -m) reworks the stage; follow-up.
  • LOW Dockerfile:118 vanilla branch. Partly addressed in 5d84477. The comment now says the go1.26.6 floor binds only the branch that compiles permit-opa, and that the fallback without the tarball downloads a prebuilt OPA. The download itself (latest, no --fail, no checksum) is unchanged. It is pre-existing, and it goes with the OPA_BUILD fix above.
  • LOW Dockerfile:276 hardcoded python3.13 ensurepip. Not changing here. It concerns the Python base, which PDP#338 is reworking; follow-up.
  • LOW Dockerfile:284 duplicate USER permit. Fixed in 5d84477 (deleted). docker buildx build --check reports the same single pre-existing warning before and after.
  • LOW build_opal_bundle.sh:3 set -e. Not changing here. The script is only used locally (Makefile). CI runs its own inline copies of the same find | xargs tar pipeline in tests.yml and release.yml, whose default bash -e shell also has no pipefail. Fixing the script alone would leave the two copies that matter, so all three belong in one change; follow-up.
  • LOW test_offline_mode/Dockerfile:1 python:alpine. Not changing here. It is test-only and pre-existing, and it belongs with PDP#338's digest-pinning sweep.

All the not-changed items are listed as follow-ups in the PR body.

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third-round review of the fixes. 3 MEDIUM, 10 LOW.

Fix verification: 14 findings from the 2026-09-18 re-review. 5 fixed, 1 partly fixed, 7 not fixed, 1 accepted wontfix

Non-blocking:

  • LOW Dockerfile:81 — Float rationale omits PDP#338's digest pin and permit-opa#51's exact pin
  • LOW Dockerfile:83 — "which is what image scanners read" overstates the audit trail the float gets
  • LOW Dockerfile:89 — This FROM line is parsed by permit-opa CI, with nothing here saying so
  • LOW Dockerfile:91 — The floor check reads the toolchain version and then discards it
  • LOW Dockerfile:286 — ensurepip path hardcodes python3.13 while the floor check branches for 3.14

1 of the above restate a finding whose fix did not close it.

Findings that could not be anchored to a line inside this PR's diff:

  • MEDIUM .github/workflows/tests.yml:52 — permit-opa is still compiled into released images from an unpinned ref: main (line 52 is not inside a diff hunk (nearest diff line: 248))
    Raised in rounds 1 and 2 and unchanged; this PR now leans on it in three places. tests.yml:52 and release.yml:43 are ref: main, so every published permitio/pdp-v2 image compiles whatever was on another repo's default branch at release time, with no record of which commit produced /app/bin/opa and no review gate between a permit-opa merge and a customer artifact. Dockerfile:63-64 states the unpinned ref as the ordering mechanism and .docker/scout/pdp-v2.vex.json:23 and :40 make it the CVE removal gate. The consequence is written at Dockerfile:76-78: deleting one godebug default=go1.25 line in permit-opa (live at its go.mod:162) changes the shipped OPA binary on the next PDP build with no PDP-side diff. release.yml:40 is also still actions/checkout@v3, on the step that carries CLONE_REPO_TOKEN (:45).
    Suggestion: Pin the permit-opa checkout to a commit SHA or release tag in both workflows and bump it deliberately, so the shipped OPA source is reproducible and the ordering constraint becomes a version bump rather than a race. Bump release.yml:40 to a SHA-pinned actions/checkout at the same time.
  • MEDIUM .github/workflows/tests.yml:77 — No CI gate builds opa_build for arm64; only the release does (line 77 is not inside a diff hunk (nearest diff line: 248))
    Raised in rounds 1 and 2; no workflow changed, and the PR body moves it to a follow-up with no ticket. This PR's own arm64 validation was run by hand, so the gap is the absent CI gate for the next change, not this one. tests.yml:77 builds platforms: linux/amd64; release.yml:62 and :117 build linux/amd64,linux/arm64. Neither passes target:, so both resolve application (Dockerfile:344) and COPY from opa_build (Dockerfile:250). A later edit to the stage, or a moved golang:1.26-bookworm digest, first executes on arm64 under QEMU while publishing to Docker Hub. The scout gate is pull_request-only (tests.yml:199) and scans the amd64 artifact (tests.yml:228, :237). This stage shipped an arm64-only defect before (Dockerfile:99-102, issue #289).
    Suggestion: Add a --target opa_build --platform linux/arm64 job to tests.yml, which stops before the Rust and Python stages and so is cheap, or add linux/arm64 to build-pdp-image, so a builder or base-digest change fails on the PR rather than mid-release.
  • MEDIUM Dockerfile:112 — OPA_BUILD selects the plugin, not which OPA binary ships (line 112 is not inside a diff hunk (nearest diff line: 96))
    Raised in rounds 1 and 2 and unchanged; the PR body now lists it as a follow-up. Two independent switches decide the same thing: if [ -f /custom/custom_opa.tar.gz ] (line 112) picks the custom versus vanilla OPA binary, while ARG OPA_BUILD=permit (line 1) separately selects PDP_OPA_PLUGINS='{"permit_graph":{}}' (line 342) via FROM main-${OPA_BUILD} AS application (line 344). Grep returns OPA_BUILD only at lines 1 and 344, and nothing in the repo (workflows, Makefile:22-32, compose) ever passes --build-arg OPA_BUILD or PDP_VANILLA, so the two never move together. A build that passes OPA_BUILD=vanilla while a stale custom/ tarball exists ships a custom OPA with the plugin disabled; the reverse ships a vanilla OPA declaring a plugin lines 334-335 say we MUST not add. build_opal_bundle.sh:19-22 compounds it: PDP_VANILLA=true skips the tarball but never clears custom/.
    Suggestion: Declare ARG OPA_BUILD inside opa_build, take the download branch only when OPA_BUILD=vanilla, and hard-fail when OPA_BUILD=permit and /custom/custom_opa.tar.gz is absent. Move rm -rf custom; mkdir custom out of the conditional in build_opal_bundle.sh so a vanilla run cannot inherit a stale tarball.
  • LOW Dockerfile:119 — -a defeats the go-build cache mount; opa_build still builds under QEMU (line 119 is not inside a diff hunk (nearest diff line: 96))
    Raised in rounds 1 and 2 and unchanged; the bump makes it more expensive. Lines 110-111 mount /go/pkg/mod and /root/.cache/go-build "for MUCH faster incremental builds", but line 119 passes -a, forcing a rebuild of every package including the whole Go 1.26 stdlib, so the go-build cache mount never serves a compile (the module mount still serves downloads). With CGO_ENABLED=0 the -a buys nothing, and -installsuffix netgo plus -extldflags=-static on the same line are likewise inert. Line 89 also has no --platform=$BUILDPLATFORM (contrast line 12 for rust_chef), so release.yml's arm64 leg runs that full rebuild emulated. permit-opa#52 went the other way: its Dockerfile:19 pins $BUILDPLATFORM and :38-39 cross-compile via GOOS/GOARCH.
    Suggestion: Drop -a and build once on the build platform with --platform=$BUILDPLATFORM plus GOARCH from $TARGETARCH, matching permit-opa#52. The fix is only complete if the vanilla branch's case $(uname -m) (lines 122-126) moves to $TARGETARCH at the same time, because on a BUILDPLATFORM-pinned stage uname -m reports the builder's arch and would download the wrong binary.
  • LOW Dockerfile:123 — Vanilla fallback downloads latest OPA with no --fail and no checksum (line 123 is not inside a diff hunk (nearest diff line: 96))
    Partly fixed since round 2: the doc half landed at lines 87-88, which scope the floor to the custom-tarball branch, but the branch itself is untouched. When /custom/custom_opa.tar.gz is absent (line 112), lines 123-124 run curl -L -o /opa https://openpolicyagent.org/downloads/latest/opa_linux_{amd64,arm64}_static, and line 250 copies that binary to /app/bin/opa. latest records no version, there is no checksum or signature, and without --fail curl writes an HTTP error body to /opa and exits 0, so a CDN 404 or 5xx yields a green build whose OPA binary is an HTML page. Scope: tests.yml:62-69 and release.yml:47-54 regenerate the tarball unconditionally, so published images take the compile branch; this bites local and manual builds. The new sentence at line 87 also does not say this branch is unpinned.
    Suggestion: Pin the OPA version in the URL, add --fail --show-error, and verify the published SHA256 before accepting the binary. In the same pass, say in the line 87-88 sentence that the fallback is unpinned and unverified, so the deferral survives the merge of the PR body that currently holds it.
  • LOW Dockerfile:266 — Second dead USER permit left behind by this round's fix (line 266 is not inside a diff hunk (nearest diff line: 286))
    This round deleted one no-op USER permit (the one after COPY ./horizon) but left the identical pattern at line 266. Between line 266 and USER root at line 271 there is only COPY kong_routes.json /config/kong_routes.json (line 269), and a COPY without --chown writes UID/GID 0 regardless of the current USER; /config is already permit-owned from line 260. No RUN executes under line 266, so it changes nothing. The round-2 finding named a defect that exists in two places and the fix removed one, leaving a reader the same two answers to "where does this image drop privileges". Line 284 is the one that matters: it sets the final runtime user inherited by main-vanilla, main-permit and application.
    Suggestion: Delete line 266, or give line 269 --chown=permit:permit if permit ownership of /config/kong_routes.json was the intent.
  • LOW build_opal_bundle.sh:3 — build_opal_bundle.sh uses set -e; a failing find in the tar pipeline is masked (file not present in this PR's diff)
    Raised in round 2 and unchanged; the PR body lists it under follow-ups. It is the script that produces the exact input this PR's builder compiles. Line 3 is set -e only. Line 25 is a pipeline, find * \( -name '*go*' -o -name 'LICENSE.md' \) -print0 | xargs -0 tar -czf .../custom_opa.tar.gz --exclude '.*'; without pipefail a failing find is masked, so a partial or empty file list produces a green run and a tarball that may be short. Dockerfile:112 then decides custom versus vanilla purely on [ -f /custom/custom_opa.tar.gz ], so whatever this script leaves behind fully determines which OPA binary ships. The same pipeline is inlined at tests.yml:69 and release.yml:54, in run: blocks that set no shell options.
    Suggestion: Change line 3 to set -euo pipefail and re-run shellcheck, pairing it with ${PDP_VANILLA:-} on lines 6 and 19, which read the variable bare today, so -u alone would abort with "unbound variable" whenever PDP_VANILLA is not exported. Add set -euo pipefail to the two workflow run: blocks as well.
  • LOW test_offline_mode/Dockerfile:1 — test_offline_mode base has no version at all, let alone a digest (file not present in this PR's diff)
    Raised in rounds 1 and 2 and unchanged; the PR body lists it under follow-ups. FROM python:alpine has no major.minor, no Alpine suite and no digest, while this PR's own subject is which base a builder resolves to. Every rebuild can land on a different CPython major and a different Alpine, so the offline-mode check can start failing or passing for reasons unrelated to the PDP, with no record of what it ran on; test_offline_mode/docker-compose.yaml:41-59 builds it for both the online and offline testers. Nothing under .github/workflows/ or the Makefile references it, so it is a manual harness, hence LOW, but it is the last wholly unversioned base in the tree: Dockerfile:12 is rust:1.94-alpine, :89 golang:1.26-bookworm, :186 python:3.13-alpine3.23.
    Suggestion: Give it an explicit tag matching the main image's Python, for example python:3.13-alpine3.23, and a digest alongside it if PDP#338's digest convention lands.

Details are in the inline comments on each line.

Comment thread Dockerfile
Comment thread Dockerfile Outdated
Comment thread Dockerfile
Comment thread Dockerfile
Comment thread Dockerfile
…h (PER-16045)

Review round 3 on #342.

* The float rationale said the build info "is what image scanners read".
  Nothing here scans a published tag: the docker-scout job is
  `pull_request`-only and points at a local tag, which tests.yml already
  calls a gap (PER-15358). The paragraph now says what actually holds - the
  toolchain is in the build info, survives `-s -w`, and is readable with
  `go version` on an extracted copy, because the runtime base ships no Go -
  and calls it an after-the-fact audit rather than a gate.
* The same paragraph now names PDP#338, which digest-pins this line and adds
  a daily Dependabot digest bump, and says what to reword once it lands; and
  it names permit-opa's release assets as the build that pins the toolchain
  exactly, so both trees describe the same three-way policy.
* The floor RUN read `go env GOVERSION` and printed it only when it failed.
  It now echoes the accepted version too, on the one run where the base image
  digest moved - so the layer log answers which 1.26.x compiled /app/bin/opa.
  Verified in /bin/sh: go1.26.8 and go1.26.6 exit 0 and print, go1.26.5 and
  go1.25.14 still exit 1 with the floor message.
* The comment block above the FROM now says that permit-opa's `pdp-builder`
  check parses that line for a literal `golang:<major>.<minor>`, so an ARG, a
  line split or a stage rename breaks another repo's CI. Verified by running
  permit-opa#52's check-pdp-builder.sh against this file: still parses.
* The ensurepip removal hardcoded python3.13 while the CPython floor check 34
  lines above branches for 3.13 and 3.14. Exactly one could be right, and the
  day the base tag moved to 3.14 the `rm -r` (no -f) would have failed the
  build. The path now comes from sysconfig's stdlib, so both places agree and
  neither has to be edited when the base moves. On a stock CPython under
  /usr/local it resolves to the same path as the literal; verified the exact
  quoting through /bin/sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 21, 2026 01:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EliMoshkovich

Copy link
Copy Markdown
Collaborator Author

Round 3 on #342 (head f867aad). All five inline findings fixed: three comment corrections, the success-path echo, and the ensurepip path derived from sysconfig so it stops contradicting the CPython floor check. Verified without a build - permit-opa#52's check-pdp-builder.sh still parses the FROM line (that parse is now documented above it), the floor RUN chain behaves through /bin/sh on 1.26.8 / 1.26.6 / 1.26.5 / 1.25.14, and the ensurepip substitution evaluates correctly with the exact quoting used in the file. No Docker daemon was available on this machine this round, so the image was not rebuilt; the earlier rounds' full arm64 builds still stand for everything except these three comment blocks and two one-liners.

On the four findings that could not be anchored, all pre-existing and none touched by this PR. This PR is approved and is a builder-version bump plus comments; I am not growing it into any of these, but here is where each stands:

  • tests.yml:52 - permit-opa compiled from an unpinned ref: main (MEDIUM). Agreed, and it is the same thing permit-opa#52's HIGH lands on: it is the mechanism that makes the merge ordering a race instead of a version bump, and it is also why a godebug line deleted in another repo changes /app/bin/opa with no diff here. Pinning it to a tag or SHA is the change that removes the whole hazard class - in both repos at once. It is a change to how released PDP images source their OPA, so it wants its own PR and Omer's/@eli's call on the bump cadence, not a rider on a builder bump. I have written it up as the permanent fix in permit-opa#52's description.
  • tests.yml:77 - no arm64 CI gate for opa_build (MEDIUM). Agreed on the substance; your --target opa_build --platform linux/arm64 job is the right shape and is genuinely cheap. It is a tests.yml change, and this PR touches no workflow, so it goes on the same follow-up list rather than in here.
  • Dockerfile:112 - OPA_BUILD selects the plugin, not the binary (MEDIUM). Pre-existing and real; the fix you describe changes which OPA ships under some --build-arg combinations, which is not something to land inside a toolchain bump.
  • Dockerfile:119 - -a defeats the build cache, no --platform=$BUILDPLATFORM (LOW). Also real, and your note about uname -m on a BUILDPLATFORM-pinned stage is the reason it is not a one-liner: dropping -a and cross-compiling means moving the vanilla branch to $TARGETARCH in the same change, i.e. touching the code path that decides which OPA binary ships. That is a separate PR with its own arm64 validation.

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth-round review of the fixes. 4 MEDIUM, 10 LOW.

Fix verification: 13 findings from the 2026-09-20 review. 5 fixed, 8 not fixed

Non-blocking:

  • MEDIUM Dockerfile:71 — "Until this lands" is wrong: releases cut from older commits stay broken
  • LOW .docker/scout/pdp-v2.vex.json:23 — Rewritten x/crypto waivers still treat the merged permit-opa#49 as pending
  • LOW Dockerfile:85 — Three-builds sentence states permit-opa#51 and #52 outcomes as current
  • LOW Dockerfile:89 — PR body still says build info is "what scanners read", which f867aad retracted
  • LOW Dockerfile:93 — PDP#338 falsifies the auditability paragraph too; the reword note misses it
  • LOW Dockerfile:100 — Floor echo is usually CACHED in a release log; it is not the release's answer

Findings that could not be anchored to a line inside this PR's diff:

  • MEDIUM .github/workflows/tests.yml:52 — permit-opa is still compiled into released images from an unpinned ref: main (line 52 is not inside a diff hunk (nearest diff line: 248))
    Raised in rounds 1 to 3 and unchanged; the only answer is a PR-body follow-up with no ticket. tests.yml:52 and release.yml:43 check out permitio/permit-opa at ref: main, so every published pdp-v2 image compiles whatever sat on another repo's default branch, with no record of which commit produced /app/bin/opa. This PR builds on that: Dockerfile:63-64 names the unpinned ref as the ordering mechanism, and the rewritten VEX removal gates (.docker/scout/pdp-v2.vex.json:23, :40) say 'the next PDP build resolves x/crypto v0.57.0'. Per Dockerfile:76-78, deleting one godebug line in permit-opa changes the shipped binary with no PDP diff. release.yml:40 is still actions/checkout@v3 on the step holding CLONE_REPO_TOKEN.
    Suggestion: Pin the permit-opa checkout to a commit SHA or release tag in both workflows and bump it deliberately, so the shipped OPA source is reproducible and the ordering constraint becomes a version bump. Move release.yml:40 to the same checkout version as line 26 in the same change.
  • MEDIUM .github/workflows/tests.yml:77 — No CI gate builds opa_build for arm64; only the release does (line 77 is not inside a diff hunk (nearest diff line: 248))
    Raised in rounds 1 to 3; f867aad changed only the Dockerfile and the PR body defers this with no ticket. tests.yml:77 builds platforms: linux/amd64; release.yml:62 and :117 build amd64 and arm64 with no target:, resolving application (Dockerfile:367), which copies /app/bin/opa from opa_build (Dockerfile:273). The stage this PR rewrote (new base, GOTOOLCHAIN, floor RUN) and its floating tag therefore first execute on arm64, under QEMU, inside the job that pushes to Docker Hub. This stage has shipped an arm64-only defect before (Dockerfile:122-124, #289), and this PR's own arm64 validation was manual.
    Suggestion: Add a cheap --target opa_build --platform linux/arm64 build to tests.yml (it stops before the Rust and Python stages), or add linux/arm64 to build-pdp-image, so a builder change or a moved base digest fails on the PR rather than mid-release.
  • MEDIUM Dockerfile:135 — OPA_BUILD selects the plugin, not which OPA binary ships (line 135 is not inside a diff hunk (nearest diff line: 119))
    Raised in rounds 1 to 3 and unchanged; deferred in the PR body only. Two independent switches decide one thing: if [ -f /custom/custom_opa.tar.gz ] (line 135) picks compiled versus downloaded OPA, while ARG OPA_BUILD=permit (line 1) separately picks PDP_OPA_PLUGINS='{"permit_graph":{}}' (line 365) via FROM main-${OPA_BUILD} (line 367). Nothing in the repo passes --build-arg OPA_BUILD, so they never move together. OPA_BUILD=vanilla with a stale custom/ tarball ships permit-opa with the plugin disabled; the reverse ships vanilla OPA declaring a plugin that lines 357-358 say MUST not be added. build_opal_bundle.sh:19-22 compounds it: PDP_VANILLA=true skips the tarball but never clears custom/.
    Suggestion: Declare ARG OPA_BUILD inside opa_build, take the download branch only when OPA_BUILD=vanilla, and fail the build when OPA_BUILD=permit and /custom/custom_opa.tar.gz is absent. Move rm -rf custom; mkdir custom out of the conditional in build_opal_bundle.sh.
  • LOW Dockerfile:142 — -a defeats the go-build cache mount; opa_build still builds under QEMU (line 142 is not inside a diff hunk (nearest diff line: 119))
    Raised in rounds 1 to 3 and unchanged. Line 142 passes -a, which rebuilds every package including the Go 1.26 stdlib, so the go-build cache mount at line 134 (sold at line 132 as 'for MUCH faster incremental builds') never serves a compile. With CGO_ENABLED=0 the -a buys nothing, although line 127 documents it as needed for a static build. Line 111 has no --platform=$BUILDPLATFORM, so release.yml's arm64 leg runs that full rebuild emulated. permit-opa#52's Dockerfile:19 pins $BUILDPLATFORM and :39 cross-compiles with GOOS/GOARCH.
    Suggestion: Drop -a, pin the stage to --platform=$BUILDPLATFORM and cross-compile with GOARCH=$TARGETARCH. Move the vanilla branch's case $(uname -m) (line 145) to $TARGETARCH in the same change, or a BUILDPLATFORM-pinned stage downloads the builder's arch.
  • LOW Dockerfile:146 — Vanilla fallback downloads latest OPA with no --fail and no checksum (line 146 is not inside a diff hunk (nearest diff line: 119))
    Raised in rounds 1 to 3; neither the code nor the prose half changed. Lines 146-147 run curl -L -o /opa https://openpolicyagent.org/downloads/latest/opa_linux_{amd64,arm64}_static when no tarball exists, and line 273 ships that file as /app/bin/opa. latest records no version, nothing verifies a checksum, and without --fail curl writes an HTTP error body to /opa and exits 0, so a CDN 404 or 5xx yields a green build whose OPA is an HTML page. Lines 102-103 still say only that the fallback 'downloads a prebuilt OPA'. Published images take the compile branch (tests.yml:62-69, release.yml:47-54); any build without the tarball takes this one.
    Suggestion: Pin the OPA version in the URL, add --fail --show-error, and verify the published SHA256. At minimum, say at lines 102-103 that the fallback is unpinned and unverified, so the deferral survives the PR body.
  • LOW Dockerfile:289 — Second dead USER permit left behind by the earlier fix (line 289 is not inside a diff hunk (nearest diff line: 301))
    Raised in round 3 with no reply and no change, and absent from the PR-body follow-up list, which still describes the earlier fix as 'dropped the second USER permit, a no-op after a COPY'. Line 289 is the same shape: USER permit followed only by COPY kong_routes.json /config/kong_routes.json (line 292) and USER root (line 294). A COPY without --chown writes UID/GID 0 whatever USER is set, and no RUN executes under line 289, so it changes nothing. Line 307 is the one that sets the runtime user, so a reader gets two answers to where the image drops privileges.
    Suggestion: Delete line 289, or give line 292 --chown=permit:permit if permit ownership of /config/kong_routes.json was the intent.
  • LOW build_opal_bundle.sh:3 — build_opal_bundle.sh uses set -e; a failing find in the tar pipeline is masked (file not present in this PR's diff)
    Raised in rounds 2 and 3 and unchanged; deferred in the PR body only. Line 3 is set -e only, and line 25 is find * ... -print0 | xargs -0 tar -czf .../custom_opa.tar.gz, so a failing find is masked and a short file list still yields a green run and a tarball. Dockerfile:135 chooses compiled versus downloaded OPA purely on [ -f /custom/custom_opa.tar.gz ], so what this script leaves behind decides which binary ships. The same pipeline is inlined at tests.yml:69 and release.yml:54 in run: blocks with no shell: override, which GitHub runs as bash -e without pipefail.
    Suggestion: Use set -euo pipefail, paired with ${PDP_VANILLA:-} at lines 6 and 19 (read bare today, so -u would abort), and add set -o pipefail to the two workflow run: blocks.
  • LOW test_offline_mode/Dockerfile:1 — test_offline_mode base has no version at all, let alone a digest (file not present in this PR's diff)
    Raised in rounds 1 to 3 and unchanged; deferred in the PR body only. FROM python:alpine has no major.minor, no Alpine suite and no digest, so every rebuild of the offline-mode harness (test_offline_mode/docker-compose.yaml builds it for both testers) can land on a different CPython and Alpine with no record of which it ran on. PDP#338 adds a Dependabot docker entry for /test_offline_mode, but a tag with no version gives it nothing to bump. It is a manual harness, and the last unversioned base in the tree (Dockerfile:12, :111, :209 all carry versions).
    Suggestion: Give it an explicit tag matching the main image, python:3.13-alpine3.23, and a digest once PDP#338's digest convention lands.

Details are in the inline comments on each line.

Comment thread Dockerfile Outdated
Comment thread .docker/scout/pdp-v2.vex.json Outdated
Comment thread Dockerfile Outdated
Comment thread Dockerfile
Comment thread Dockerfile Outdated
Comment thread Dockerfile Outdated
… cannot cache (PER-16045)

Zeev's 2026-09-22 review: six threads, all accuracy claims about other
repos, other PRs, and what a build log proves.

- "until this lands" scoped the permit-opa#52 breakage to main. release.yml
  checks this repo out with no `ref:`, so a release builds the Dockerfile of
  the commit it was cut from, while permit-opa still comes from `ref: main`;
  tests.yml also builds on `v*` pushes. v0.9.15 is d3da8b9, still on the 1.25
  builder. State the lasting rule instead: cut every release and `v*` build
  from a commit containing this FROM line.
- The x/crypto waivers still spoke of permit-opa#49 as pending. It MERGED
  2026-09-16 and permit-opa main carries x/crypto v0.55.0 + grpc v1.83.2.
  Both statements are past tense now, and say why the superseded 0.53.0
  subcomponent PURL stays in the list. VEX doc re-issued as version 12.
- The three-builds sentence stated permit-opa#51/#52 outcomes as current;
  both are open and being edited. Replaced with wording that stays true
  whatever they land as: permit-opa's toolchain policy lives in permit-opa's
  own files, not in this comment.
- The PDP#338 reword note covered only the float paragraph. #338 also drops
  docker-scout's `pull_request` gate and adds image-scan-published.yml
  (daily), so it falsifies the auditability paragraph too - and closes that
  gap rather than recording it. Its Dependabot entry carries
  `cooldown: default-days: 7`.
- The floor check's echo is a gate, not a record: it caches on the base image
  digest and reads CACHED in a typical release log. The compile RUN cannot
  cache (`COPY custom* /custom` sees a tarball the workflow regenerates every
  run), so echo the toolchain there instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 22, 2026 23:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…k is unpinned (PER-16045)

`USER permit` before `COPY kong_routes.json` and the `USER root` after it
changed nothing: no RUN executes between them, and a COPY without --chown
writes root:root whatever USER is set. Same shape as the one this PR already
removed. The file stays root-owned, as it was.

The floor comment now says the no-tarball fallback downloads OPA `latest`
with no --fail and no checksum, so that deferral lives in the file and not
only in the PR body.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 23, 2026 13:48
@EliMoshkovich

Copy link
Copy Markdown
Collaborator Author

@zeevmoney on the eight unanchored findings from your last review. This PR's scope is the OPA builder's move to golang:1.26-bookworm. The deferrals are pre-existing behaviour that this PR doesn't change, and each one is listed under the PR body's follow-ups.

  • MEDIUM tests.yml:52, permit-opa built from ref: main. Not changing here. Pinning the checkout changes how the two repos release together (every permit-opa change would need a PDP bump). That's a process decision, not a builder change. release.yml's checkout@v3 goes with it.
  • MEDIUM tests.yml:77, no arm64 opa_build gate. Not changing here. An arm64 build of this stage runs a full -a compile under QEMU on every PR. This change was validated on linux/arm64 by hand (see "How validated"). Better done together with the -a/$BUILDPLATFORM fix below, which makes it cheap.
  • MEDIUM Dockerfile:135, OPA_BUILD vs the tarball check. Not changing here. It's pre-existing, and nothing in the repo passes --build-arg OPA_BUILD, so every real build takes the permit branch with the tarball present.
  • LOW Dockerfile:142, -a plus no --platform=$BUILDPLATFORM. Not changing here. It's a build-speed issue from before this PR. Fixing it means cross-compiling and moving the vanilla branch to $TARGETARCH, a separate change.
  • LOW Dockerfile:146, vanilla fallback downloads latest unverified. Prose fixed in de482d2: the floor comment now says the fallback is unpinned (latest) and unverified (no --fail, no checksum). The code is unchanged: published images never take that branch.
  • LOW Dockerfile:289, second dead USER permit. Fixed in de482d2. Removed it along with the USER root after it, since neither has a RUN in its range. kong_routes.json stays root-owned, as it was. PR body updated.
  • LOW build_opal_bundle.sh, set -e without pipefail. Not changing here. The file isn't in this PR's diff, and the fix touches two workflow run: blocks too.
  • LOW test_offline_mode/Dockerfile, unversioned python:alpine. Not changing here. It's a manual harness that this PR doesn't touch, and PDP#338's digest convention is the natural place for it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EliMoshkovich
EliMoshkovich merged commit c8edea7 into main Sep 23, 2026
13 of 15 checks passed
@EliMoshkovich
EliMoshkovich deleted the eli/per-16045-opa-builder-go126 branch September 23, 2026 18:05

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fifth-round check of this PR after it merged. 3 MEDIUM, 6 LOW. These were not fixed before the merge, or were introduced by commits added after the round-4 approval, so they need a follow-up change. No verdict is submitted on a merged PR.

Fix verification: 14 findings from the 2026-09-22 review. 5 fixed, 3 partly fixed, 6 not fixed

Non-blocking:

  • MEDIUM Dockerfile:166: OPA_BUILD selects the plugin, not which OPA binary ships
  • LOW Dockerfile:97: PDP#338 reword note describes a superseded #338 head
  • LOW Dockerfile:109: "Both are open" was false on main 16 minutes after merge
  • LOW Dockerfile:175: -a defeats the go-build cache mount; opa_build still builds under QEMU
  • LOW Dockerfile:175: Vanilla fallback downloads latest OPA with no --fail and no checksum

5 of the above restate a finding whose fix did not close it.

Findings that could not be anchored to a line inside this PR's diff:

  • MEDIUM .github/workflows/tests.yml:52: permit-opa is still compiled into released images from an unpinned ref: main (line 52 is not inside a diff hunk (nearest diff line: 248))
    Not fixed: the workflows are unchanged, and the only response is Dockerfile prose (:79-85) naming this checkout as the cause of the release hazard. tests.yml:52 and release.yml:43 still check out permitio/permit-opa at ref: main, so every published pdp-v2 image compiles whatever sits on another repo's default branch, with no record of which commit produced /app/bin/opa. This PR's ordering (Dockerfile:63-64) and the VEX removal gate (.docker/scout/pdp-v2.vex.json:23, :40: 'the next PDP build resolves x/crypto v0.57.0') both rest on that float, so permit-opa#52 merging will change the shipped binary with no PDP diff. release.yml:40 is still actions/checkout@v3 on the step holding CLONE_REPO_TOKEN.
    Suggestion: Pin the permit-opa checkout in both workflows to a commit SHA or release tag and bump it deliberately; that also turns the backport rule at Dockerfile:79-82 into an ordinary version bump. Move release.yml:40 to actions/checkout@v4 to match line 26.
  • MEDIUM .github/workflows/tests.yml:77: No CI gate builds opa_build for arm64; only the release does (line 77 is not inside a diff hunk (nearest diff line: 248))
    Not fixed: no workflow changed, and this PR's arm64 evidence is a manual docker buildx build --platform linux/arm64 --target opa_build (PR body). tests.yml:77 builds platforms: linux/amd64 only. release.yml:62 and :117 build amd64 and arm64 and resolve application, which copies /app/bin/opa from opa_build (Dockerfile:307). So the stage this PR rewrote (new base tag, GOTOOLCHAIN, floor RUN, new echo) and its floating tag first execute on arm64 under QEMU, inside the job that pushes to Docker Hub. Dockerfile:152-154 records an earlier arm64-only defect in this same stage (#289).
    Suggestion: Add a cheap --target opa_build --platform linux/arm64 build to tests.yml (it stops before the Rust and Python stages), or add linux/arm64 to build-pdp-image, so a builder change or a moved base digest fails on the PR rather than mid-release.
  • LOW build_opal_bundle.sh:3: build_opal_bundle.sh uses set -e; a failing find in the tar pipeline is masked (file not present in this PR's diff)
    Not fixed: the script and both workflows are unchanged; deferred in the PR body with no ticket. build_opal_bundle.sh:3 is set -e only and :25 is find * ... -print0 | xargs -0 tar -czf .../custom_opa.tar.gz, so a failing find is masked and a short file list still yields a tarball and a green run. Dockerfile:165 picks compiled versus downloaded OPA purely on that tarball's presence, so this pipeline decides which binary ships. The same pipeline at tests.yml:69 and release.yml:54 runs in run: blocks with no shell: override, which GitHub runs as bash -e without pipefail.
    Suggestion: Use set -euo pipefail, reading ${PDP_VANILLA:-} at lines 6 and 19 so -u does not abort, and add shell: bash (which runs bash -eo pipefail) or an explicit set -o pipefail to the two workflow run: blocks.
  • LOW test_offline_mode/Dockerfile:1: test_offline_mode base has no version at all, let alone a digest (file not present in this PR's diff)
    Not fixed: the file is unchanged; the PR body defers it with no ticket. FROM python:alpine has no major.minor, no Alpine suite and no digest, so each rebuild of the offline-mode harness (test_offline_mode/docker-compose.yaml builds it) can land on a different CPython and Alpine with no record of which one ran. PDP#338 (open) adds a weekly Dependabot docker entry for /test_offline_mode, but a tag with no version gives it nothing to bump. It is the last unversioned base in the tree; Dockerfile:12, :141 and :243 all carry versions.
    Suggestion: Use an explicit tag matching the main image, python:3.13-alpine3.23, and add a digest once PDP#338's digest convention lands.

Details are in the inline comments on each line.

Comment thread Dockerfile
Comment thread Dockerfile
Comment thread Dockerfile
Comment thread Dockerfile
Comment thread Dockerfile
@EliMoshkovich

Copy link
Copy Markdown
Collaborator Author

@zeevmoney the 4 findings from your review body that had no line to anchor to are all fixed in #345 (374c0cc):

  • MEDIUM, tests.yml:52: permit-opa built from unpinned ref: main. tests.yml and release.yml now check out permit-opa at ab54a3766111b3f6c4796c5ab0a7d7e75f7f590d (permit-opa main today). A comment above the tests.yml checkout explains how to bump it, and the Pre build step fails if the two files pin different SHAs. release.yml:40 moves from actions/checkout@v3 to @v4. The VEX removal gates now say x/crypto v0.57.0 arrives when the pin moves past permit-opa#52.
  • MEDIUM, tests.yml:77: no arm64 CI gate for opa_build. build-pdp-image now builds --target opa_build --platform linux/arm64 on every run. That stage now cross-compiles on $BUILDPLATFORM, so the build is cheap.
  • LOW, build_opal_bundle.sh: failing find masked. The script now uses set -euo pipefail and ${PDP_VANILLA:-}, and always empties custom/. Both workflow Pre build steps run under shell: bash, which enables pipefail.
  • LOW, test_offline_mode/Dockerfile:1: unversioned base. Changed to python:3.13-alpine3.23. The digest waits for ci: gate releases on CVEs and unify Dependabot, Trivy and Docker Scout (PER-15358) #338.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants