Files
buildx/policy/default_test.go
Tonis Tiigi acaf251f0b policy: verify BuildKit builder images
Extend the built-in policy to validate signed moby/buildkit release and
floating tags before docker-container builders are created.

Pull the image first, inspect it through Docker, and bind verification to the
descriptor digest. Resolve signature attestations through the BuildKit API
embedded in the Docker daemon.

If pulling fails, use a local image while applying the same verification when
the containerd image store exposes an immutable descriptor. Keep the classic
image-store behavior unchanged because no descriptor is available.

Allow unmanaged repositories and digest-only references unchanged. Add the
allow-untrusted-image driver option as an explicit verification bypass.

Document the behavior and add policy, digest-pinning, and local fallback
coverage.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-07-20 18:22:41 -07:00

546 lines
19 KiB
Go

package policy
import (
"context"
"testing"
"time"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb"
moby_buildkit_v1_sourcepolicy "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/sourcepolicy/policysession"
policyimage "github.com/moby/policy-helpers/image"
policytypes "github.com/moby/policy-helpers/types"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
// makeDefaultPolicy returns a Policy instance backed by the embedded
// default policy module, optionally wired to a mock signature verifier
// returning the supplied SignatureInfo for image attestations.
func makeDefaultPolicy(t *testing.T, sigInfo *policytypes.SignatureInfo) *Policy {
t.Helper()
var verifierProvider PolicyVerifierProvider
if sigInfo != nil {
verifierProvider = func() (PolicyVerifier, error) {
return &mockPolicyVerifier{
verifyImage: func(_ context.Context, _ policyimage.ReferrersProvider, _ ocispecs.Descriptor, _ *ocispecs.Platform) (*policytypes.SignatureInfo, error) {
return sigInfo, nil
},
}, nil
}
}
return DefaultPolicy(Opt{
Log: func(level logrus.Level, msg string) {
t.Logf("[%s] %s", level, msg)
},
VerifierProvider: verifierProvider,
})
}
// dockerGithubBuilderSig returns a SignatureInfo that satisfies the
// docker_github_builder_signature helper for the given source repository
// and ref. Pass an empty ref to omit the SourceRepositoryRef field.
func dockerGithubBuilderSig(sourceRepo, sourceRef string) *policytypes.SignatureInfo {
return &policytypes.SignatureInfo{
Kind: policytypes.KindDockerGithubBuilder,
SignatureType: policytypes.SignatureBundleV03,
Timestamps: []policytypes.TimestampVerificationResult{
{Type: "rekor", URI: "https://rekor.sigstore.dev", Timestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)},
},
Signer: &certificate.Summary{
CertificateIssuer: "CN=sigstore-intermediate,O=sigstore.dev",
Extensions: certificate.Extensions{
Issuer: "https://token.actions.githubusercontent.com",
SourceRepositoryURI: "https://github.com/" + sourceRepo,
SourceRepositoryRef: sourceRef,
RunnerEnvironment: "github-hosted",
},
},
}
}
// runDefaultPolicyImage evaluates the default policy against the given image
// reference. An attestation chain and an empty image config are always
// supplied so that the policy can fully resolve metadata and produce a
// decision (rather than requesting more data via the next response).
func runDefaultPolicyImage(t *testing.T, p *Policy, ref string) *policysession.DecisionResponse {
t.Helper()
src := &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "docker-image://" + ref},
Image: &gwpb.ResolveSourceImageResponse{
Digest: "sha256:abababababababababababababababababababababababababababababababab",
Config: []byte(`{"created":"2024-01-01T00:00:00Z","config":{}}`),
AttestationChain: newTestAttestationChain(t),
},
}
resp, _, err := p.CheckPolicy(context.Background(), &policysession.CheckPolicyRequest{
Platform: &pb.Platform{OS: "linux", Architecture: "amd64"},
Source: src,
})
require.NoError(t, err)
require.NotNil(t, resp)
return resp
}
func TestDefaultPolicyAllowsNonImageSources(t *testing.T) {
p := makeDefaultPolicy(t, nil)
src := &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "https://example.com/foo.tar.gz"},
HTTP: &gwpb.ResolveSourceHTTPResponse{
Checksum: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
},
}
resp, _, err := p.CheckPolicy(context.Background(), &policysession.CheckPolicyRequest{
Source: src,
})
require.NoError(t, err)
require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, resp.Action)
}
func TestDefaultPolicyImages(t *testing.T) {
testCases := []struct {
name string
sig *policytypes.SignatureInfo
ref string
allow bool
denyMsg string
}{
{
name: "allows_unrelated_images",
ref: "alpine:latest",
allow: true,
},
{
name: "dockerfile_digest_only_always_allowed",
ref: "docker/dockerfile@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
allow: true,
},
{
name: "dockerfile_tagged_digest_denied",
ref: "docker/dockerfile:1.21.0@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_old_version_allowed_unsigned",
ref: "docker/dockerfile:1.20.0",
allow: true,
},
{
name: "dockerfile_new_version_requires_signature",
ref: "docker/dockerfile:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_new_minor_version_requires_signature",
ref: "docker/dockerfile:1.21",
denyMsg: "signature is required for 1.21 tag",
},
{
name: "dockerfile_new_major_version_requires_signature",
ref: "docker/dockerfile:1",
denyMsg: "signature is required for 1 tag",
},
{
name: "dockerfile_new_version_allowed_with_matching_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile:1.21.0",
allow: true,
},
{
name: "dockerfile_new_minor_version_allowed_with_matching_patch_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile:1.21",
allow: true,
},
{
name: "dockerfile_new_version_allowed_with_matching_signature_labs",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
ref: "docker/dockerfile:1.21.0-labs",
allow: true,
},
{
name: "dockerfile_new_version_allowed_with_matching_signature_labs2",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
ref: "docker/dockerfile:1.21-labs",
allow: true,
},
{
name: "dockerfile_new_version_allowed_with_matching_signature_labs3",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
ref: "docker/dockerfile:1-labs",
allow: true,
},
{
name: "dockerfile_new_version_allowed_with_matching_signature_labs4",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
ref: "docker/dockerfile:labs",
allow: true,
},
{
name: "dockerfile_new_version_denied_with_nonlabs_signature_for_labs_tag",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile:1-labs",
denyMsg: "signature is required for 1-labs tag",
},
{
name: "dockerfile_new_version_denied_with_mismatched_ref_labs",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0-labs"),
ref: "docker/dockerfile:1.21.0-labs",
denyMsg: "signature is required for 1.21.0-labs tag",
},
{
name: "dockerfile_new_version_denied_with_nonlabs_signature_for_exact_labs_tag",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile:1.21.0-labs",
denyMsg: "signature is required for 1.21.0-labs tag",
},
{
name: "dockerfile_new_version_denied_with_wrong_signature_repo",
sig: dockerGithubBuilderSig("docker/dockerfile", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_upstream_new_version_denied_with_wrong_signature_repo",
sig: dockerGithubBuilderSig("docker/dockerfile-upstream", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile-upstream:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_upstream_new_version_requires_signature",
ref: "docker/dockerfile-upstream:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_upstream_new_version_allowed_with_matching_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
ref: "docker/dockerfile-upstream:1.21.0",
allow: true,
},
{
name: "dockerfile_upstream_new_version_denied_with_mismatched_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0"),
ref: "docker/dockerfile-upstream:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_new_version_denied_with_mismatched_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0"),
ref: "docker/dockerfile:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_new_minor_version_denied_with_newer_patch_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.23.0"),
ref: "docker/dockerfile:1.22",
denyMsg: "signature is required for 1.22 tag",
},
{
name: "dockerfile_new_version_denied_with_mismatched_ref_labs",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0-labs"),
ref: "docker/dockerfile:1.21.0-labs",
denyMsg: "signature is required for 1.21.0-labs tag",
},
{
name: "dockerfile_new_version_denied_with_wrong_signature_tag",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/1.21.0"),
ref: "docker/dockerfile:1.21.0",
denyMsg: "signature is required for 1.21.0 tag",
},
{
name: "dockerfile_latest_allowed_with_signature_any_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.30.0"),
ref: "docker/dockerfile:latest",
allow: true,
},
{
name: "dockerfile_latest_denied_without_signature",
ref: "docker/dockerfile:latest",
denyMsg: "signature is required for latest tag",
},
{
name: "dockerfile_upstream_latest_allowed_with_signature_any_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.30.0"),
ref: "docker/dockerfile-upstream:latest",
allow: true,
},
{
name: "dockerfile_upstream_latest_denied_without_signature",
ref: "docker/dockerfile-upstream:latest",
denyMsg: "signature is required for latest tag",
},
{
name: "dockerfile_upstream_master_allowed_with_signature_any_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/heads/master"),
ref: "docker/dockerfile-upstream:master",
allow: true,
},
{
name: "dockerfile_upstream_master_denied_without_signature",
ref: "docker/dockerfile-upstream:master",
denyMsg: "signature is required for master 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)
})
}
}
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)
})
}
}
func TestDefaultPolicyBuildKitImages(t *testing.T) {
testCases := []struct {
name string
sig *policytypes.SignatureInfo
ref string
allow bool
denyMsg string
}{
{
name: "buildkit_digest_only_always_allowed",
ref: "moby/buildkit@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
allow: true,
},
{
name: "buildkit_tagged_digest_denied",
ref: "moby/buildkit:v0.31.2@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
denyMsg: "signature is required for v0.31.2 tag",
},
{
name: "buildkit_floating_denied_without_signature",
ref: "moby/buildkit:buildx-stable-1",
denyMsg: "signature is required for buildx-stable-1 tag",
},
{
name: "buildkit_floating_allowed_with_signature_any_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"),
ref: "moby/buildkit:buildx-stable-1",
allow: true,
},
{
name: "buildkit_floating_rootless_allowed_with_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"),
ref: "moby/buildkit:buildx-stable-1-rootless",
allow: true,
},
{
name: "buildkit_floating_gpu_allowed_with_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"),
ref: "moby/buildkit:buildx-stable-1-gpu",
allow: true,
},
{
name: "buildkit_latest_denied_without_signature",
ref: "moby/buildkit:latest",
denyMsg: "signature is required for latest tag",
},
{
name: "buildkit_floating_denied_with_wrong_signature_repo",
sig: dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/v0.31.2"),
ref: "moby/buildkit:buildx-stable-1",
denyMsg: "signature is required for buildx-stable-1 tag",
},
{
name: "buildkit_release_denied_without_signature",
ref: "moby/buildkit:v0.31.2",
denyMsg: "signature is required for v0.31.2 tag",
},
{
name: "buildkit_release_allowed_with_matching_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"),
ref: "moby/buildkit:v0.31.2",
allow: true,
},
{
name: "buildkit_release_rootless_allowed_with_matching_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"),
ref: "moby/buildkit:v0.31.2-rootless",
allow: true,
},
{
name: "buildkit_release_ubuntu_allowed_with_matching_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"),
ref: "moby/buildkit:v0.31.2-ubuntu",
allow: true,
},
{
name: "buildkit_release_rc_allowed_with_matching_signature",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.0-rc1"),
ref: "moby/buildkit:v0.31.0-rc1",
allow: true,
},
{
name: "buildkit_release_denied_with_mismatched_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.1"),
ref: "moby/buildkit:v0.31.2",
denyMsg: "signature is required for v0.31.2 tag",
},
{
name: "buildkit_release_denied_with_variant_in_signature_ref",
sig: dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2-rootless"),
ref: "moby/buildkit:v0.31.2-rootless",
denyMsg: "signature is required for v0.31.2-rootless tag",
},
{
name: "buildkit_first_signed_release_requires_signature",
ref: "moby/buildkit:v0.27.0",
denyMsg: "signature is required for v0.27.0 tag",
},
{
name: "buildkit_future_major_denied_without_signature",
ref: "moby/buildkit:v1.0.0",
denyMsg: "signature is required for v1.0.0 tag",
},
{
name: "buildkit_old_release_allowed_unsigned",
ref: "moby/buildkit:v0.26.2",
allow: true,
},
{
name: "buildkit_master_cache_allowed_as_unrecognized",
ref: "moby/buildkit:master-cache",
allow: true,
},
{
name: "buildkit_bare_version_selector_allowed_as_unrecognized",
ref: "moby/buildkit:v0.8",
allow: true,
},
{
name: "buildkit_beta_tag_allowed_as_unrecognized",
ref: "moby/buildkit:v0.13.0-beta1",
allow: true,
},
{
name: "buildkit_docker_tag_allowed_as_unrecognized",
ref: "moby/buildkit:docker-20.10.3",
allow: true,
},
}
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)
})
}
}