From 0491d7c7b9d2f5f85fa5435291d5198907899aea Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 20 Apr 2026 16:46:05 -0700 Subject: [PATCH] policy: add default policy for buildkit-syft-scanner images Extends the builtin default policy to verify docker-github-builder signatures on docker/buildkit-syft-scanner images. Tags `latest` and 1.10+ require a signature from the docker/buildkit-syft-scanner source repository; older tags pass through unchanged. Factors the shared ref matching logic into sig_ref_matches for dockerfile and introduces a syft-scanner variant that uses the plain refs/tags/ prefix. Signed-off-by: Tonis Tiigi --- policy/default.rego | 49 ++++++++++++++++++++-- policy/default_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/policy/default.rego b/policy/default.rego index d8be50395..2e89c1bab 100644 --- a/policy/default.rego +++ b/policy/default.rego @@ -5,6 +5,7 @@ package docker # # - docker/dockerfile # - docker/dockerfile-upstream +# - docker/buildkit-syft-scanner # # Any image outside this managed set is allowed and passes through to user # policies unchanged. Access by digest is always allowed. For tag-based @@ -22,6 +23,11 @@ is_dockerfile if { input.image.fullRepo == "docker.io/docker/dockerfile-upstream" } +is_syft_scanner if { + input.image + input.image.fullRepo == "docker.io/docker/buildkit-syft-scanner" +} + dockerfile_floating_tag(tag) if tag == "latest" dockerfile_floating_tag(tag) if tag == "labs" dockerfile_floating_tag(tag) if tag == "master" @@ -29,6 +35,11 @@ dockerfile_floating_tag(tag) if tag == "master" dockerfile_tag_requires_sig(tag) if dockerfile_floating_tag(tag) dockerfile_tag_requires_sig(tag) if version_tag_ge(tag, 1, 21) +syft_scanner_floating_tag(tag) if tag == "latest" + +syft_scanner_tag_requires_sig(tag) if syft_scanner_floating_tag(tag) +syft_scanner_tag_requires_sig(tag) if version_tag_ge(tag, 1, 10) + default_policy_deny_msgs contains msg if { is_dockerfile @@ -39,6 +50,15 @@ default_policy_deny_msgs contains msg if { msg := sprintf("image %s is not allowed by default policy: a verified docker-github-builder signature is required for %s tag", [input.image.ref, input.image.tag]) } +default_policy_deny_msgs contains msg if { + is_syft_scanner + tag := input.image.tag + tag != "" + syft_scanner_tag_requires_sig(tag) + not syft_scanner_sig_ok(tag) + msg := sprintf("image %s is not allowed by default policy: a verified docker-github-builder signature is required for %s tag", [input.image.ref, input.image.tag]) +} + dockerfile_sig_ok(tag) if { dockerfile_floating_tag(tag) some sig in input.image.signatures @@ -52,6 +72,19 @@ dockerfile_sig_ok(tag) if { dockerfile_sig_ref_matches(sig, tag) } +syft_scanner_sig_ok(tag) if { + syft_scanner_floating_tag(tag) + some sig in input.image.signatures + docker_github_builder_signature(sig, "docker/buildkit-syft-scanner") +} + +syft_scanner_sig_ok(tag) if { + not syft_scanner_floating_tag(tag) + some sig in input.image.signatures + docker_github_builder_signature(sig, "docker/buildkit-syft-scanner") + syft_scanner_sig_ref_matches(sig, tag) +} + decision := { "allow": count(default_policy_deny_msgs) == 0, @@ -89,14 +122,24 @@ version_tag_ge(tag, target_major, target_minor) if { } dockerfile_sig_ref_matches(sig, tag) if { - ref := trim_prefix(sig.signer.sourceRepositoryRef, "refs/tags/dockerfile/") + sig_ref_matches(sig.signer.sourceRepositoryRef, tag, "refs/tags/dockerfile/") +} + +syft_scanner_sig_ref_matches(sig, tag) if { + ref := trim_prefix(sig.signer.sourceRepositoryRef, "refs/tags/") ref != sig.signer.sourceRepositoryRef + version_tag_selector_matches(tag, ref) +} + +sig_ref_matches(ref, tag, prefix) if { + stripped_ref := trim_prefix(ref, prefix) + stripped_ref != ref tag_labs := endswith(tag, "-labs") - ref_labs := endswith(ref, "-labs") + ref_labs := endswith(stripped_ref, "-labs") tag_labs == ref_labs version_tag_selector_matches( trim_suffix(tag, "-labs"), - trim_suffix(ref, "-labs"), + trim_suffix(stripped_ref, "-labs"), ) } diff --git a/policy/default_test.go b/policy/default_test.go index 9cc684e5f..749accb04 100644 --- a/policy/default_test.go +++ b/policy/default_test.go @@ -306,3 +306,97 @@ func TestDefaultPolicyImages(t *testing.T) { }) } } + +func TestDefaultPolicySyftScannerImages(t *testing.T) { + testCases := []struct { + name string + sig *policytypes.SignatureInfo + ref string + allow bool + denyMsg string + }{ + { + name: "syft_scanner_old_version_allowed_unsigned", + ref: "docker/buildkit-syft-scanner:1.9.0", + allow: true, + }, + { + name: "syft_scanner_new_version_requires_signature", + ref: "docker/buildkit-syft-scanner:1.10.0", + denyMsg: "signature is required for 1.10.0 tag", + }, + { + name: "syft_scanner_new_minor_version_requires_signature", + ref: "docker/buildkit-syft-scanner:1.10", + denyMsg: "signature is required for 1.10 tag", + }, + { + name: "syft_scanner_new_major_version_requires_signature", + ref: "docker/buildkit-syft-scanner:1", + denyMsg: "signature is required for 1 tag", + }, + { + name: "syft_scanner_new_version_allowed_with_matching_signature", + sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"), + ref: "docker/buildkit-syft-scanner:1.10.0", + allow: true, + }, + { + name: "syft_scanner_new_minor_version_allowed_with_matching_patch_signature", + sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"), + ref: "docker/buildkit-syft-scanner:1.10", + allow: true, + }, + { + name: "syft_scanner_new_major_version_allowed_with_matching_minor_signature", + sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"), + ref: "docker/buildkit-syft-scanner:1", + allow: true, + }, + { + name: "syft_scanner_new_version_denied_with_wrong_signature_repo", + sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/1.10.0"), + ref: "docker/buildkit-syft-scanner:1.10.0", + denyMsg: "signature is required for 1.10.0 tag", + }, + { + name: "syft_scanner_new_version_denied_with_mismatched_ref", + sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.11.0"), + ref: "docker/buildkit-syft-scanner:1.10.0", + denyMsg: "signature is required for 1.10.0 tag", + }, + { + name: "syft_scanner_new_minor_version_denied_with_newer_patch_ref", + sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.11.0"), + ref: "docker/buildkit-syft-scanner:1.10", + denyMsg: "signature is required for 1.10 tag", + }, + { + name: "syft_scanner_latest_allowed_with_signature_any_ref", + sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"), + ref: "docker/buildkit-syft-scanner:latest", + allow: true, + }, + { + name: "syft_scanner_latest_denied_without_signature", + ref: "docker/buildkit-syft-scanner:latest", + denyMsg: "signature is required for latest tag", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := makeDefaultPolicy(t, tc.sig) + resp := runDefaultPolicyImage(t, p, tc.ref) + if tc.allow { + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, resp.Action) + require.Empty(t, resp.DenyMessages) + return + } + + require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, resp.Action) + require.Len(t, resp.DenyMessages, 1) + require.Contains(t, resp.DenyMessages[0].Message, tc.denyMsg) + }) + } +}