Harbor's Pull-Blocking Does Not Block an OCI Index
The same image, same 7 Critical findings, gets a 412 PROJECTPOLICYVIOLATION as a plain manifest and a 200 as an OCI index — the format buildx and most modern build tooling produce by default.
The project was configured with prevent_vul: true and severity: critical, against a fixture (tc01) carrying 7 Critical findings. The first pull attempt was supposed to prove the gate works. It didn't: docker pull of tc01:v1 succeeded, exit=0, digest resolved and downloaded like any clean image.
The wrong hypothesis, and the test that killed it
The obvious suspect was admin bypass — maybe prevent_vul doesn't apply to privileged pulls. So the test created a project-scoped robot account with pull-only permissions, robot$poc+pocpuller, and pulled again. Same result: tc01:v1 downloaded cleanly. Whatever was letting the vulnerable image through, it wasn't a permissions exemption — the robot account has no more privilege than any consumer, and it still got the image.
The root cause test
The real variable was manifest shape, not identity. The same content was pushed twice: once as a plain manifest tagged tc01:plain, once as the OCI index tagged tc01:v1. Pulling the plain manifest returned:
HTTP 412
{"errors":[{"code":"PROJECTPOLICYVIOLATION","message":"current image with
268 vulnerabilities cannot be pulled due to configured policy in 'Prevent
images with vulnerability severity of \"Critical\" or higher from running.'
To continue with pull, please contact your project administrator to exempt
matched vulnerabilities through configuring the CVE allowlist."}]}268 vulnerabilities, 7 of them Critical, blocked exactly as configured. The Docker client sees this as a resolve failure: unexpected status from HEAD request to .../manifests/plain: 412 Precondition Failed. Pulling the OCI index — same digest content underneath, same 7 Critical findings — returned HTTP 200. No error, no warning, image served.
Plain manifest: 412 PROJECTPOLICYVIOLATION. OCI index, same content: 200.
Why this matters more than a lab curiosity
An OCI index isn't an edge case. It's the default output of docker buildx build, and increasingly the default for any multi-platform or attestation-carrying build. A pipeline that switched to buildx for completely unrelated reasons — multi-arch support, SBOM attestations — picks up this bypass for free, with no change to prevent_vul, no change to severity thresholds, and no error anywhere in its own logs. The gate looks identical in the Harbor UI whether it's actually evaluating the image or silently skipping it, because the index manifest itself carries no scan result for prevent_vul to key off.
The failed hypotheses are worth keeping, too. Ruling out admin bypass with an actual pull-only robot account, rather than trusting a plausible story about permissions, is what turned up the real variable. A policy that appears to work for a plain manifest and silently doesn't for an index is a worse failure mode than an outright broken gate — the dashboard says "protected" right up until the pipeline that matters pushes the one manifest shape nobody tested.
Next: prevent_vul breaks docker push of buildx attestations — the mirror-image bug, where the same gate blocks a push for an image with zero findings.