prevent_vul Breaks docker push of Buildx Attestations
Harbor's prevent_vul is documented as a pull-time gate. Turn it on, push a buildx image with attestations, and the push fails with a 412 on the attestation child manifest — for an image with zero findings.
prevent_vul is described everywhere as a pull-time control: Harbor refuses to serve a manifest whose scan result exceeds the configured severity. Nothing in that description suggests it can fail a docker push. It can, and the trigger is ordinary: a buildx build that carries attestations.
The project under test was configured like this:
{"auto_sbom_generation":"true","auto_scan":"true","prevent_vul":"true",
"public":"false","reuse_sys_cve_allowlist":"false","severity":"critical"}With that in place, pushing tc03:audittest failed — 412 on a child manifest, sha256:8bb449e7…, which is the attestation manifest. tc03 is the fixture with zero findings. Nothing about its vulnerability state should have tripped a severity gate set to Critical.
The error names the real reason
Fetching that child manifest on its own reproduces the failure exactly:
{"errors":[{"code":"PROJECTPOLICYVIOLATION","message":"current image
without vulnerability scanning 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
for help."}]}Read the message carefully. It is not "current image with N vulnerabilities" — the wording used when a real scan result exceeds the threshold. It is "current image without vulnerability scanning". The attestation manifest has no scan result, because it is not an image and never gets scanned. The gate's default for "no scan result" is deny, and the client's manifest request for that child is answered by the same gate that answers pulls.
The image passes with zero findings; the unscannable attestation child is denied for having no scan result at all.
The opposite failure, same policy
This is a distinct bug from the OCI-index bypass, and the two are mirror images of each other. In the same test run, tc01 — 7 Critical findings — was pushed twice: once as a plain manifest, once as an OCI index. The plain manifest was blocked:
{"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.'…"}The OCI index, with the same content and the same 7 Critical findings, returned HTTP 200. From the Docker client, the blocked plain tag looks like this:
Error response from daemon: unknown: failed to resolve reference
"…/poc/tc01:plain": unexpected status from HEAD request to
/v2/poc/tc01/manifests/plain: 412 Precondition FailedSo one policy produces both errors of judgement. Wrap a vulnerable image in an index and the gate lets it through. Attach an attestation to a clean image and the gate stops the push. The common cause is that the gate evaluates per manifest, with no model of which manifests in a graph are images and which are not.
What to do about it
Nothing here argues against prevent_vul. It argues against turning it on in a project whose pipelines push buildx attestations without testing that push first. The two observable facts are cheap to check in any project before enabling the flag:
- does a push of your normal build succeed with
prevent_vul: true? - does a pull of a known-bad image actually get a 412, as a plain manifest and as an index?
A gate that fails your clean builds and passes your dirty ones is worse than no gate, and both halves of that are visible in one afternoon of pushing fixtures.
Next: the allowlist that changes nothing you can see — Harbor accepts your CVE exception, drops half the fields, and leaves every number on the screen exactly where it was.