diff --git a/build/opt.go b/build/opt.go
index c32eb0cee..4b5246c75 100644
--- a/build/opt.go
+++ b/build/opt.go
@@ -65,18 +65,6 @@ var sendGitQueryAsInput = sync.OnceValue(func() bool {
return false
})
-// defaultPolicyEnabled reports whether the builtin default source policy is
-// enabled via the BUILDX_DEFAULT_POLICY environment variable. It is opt-in
-// for now; a future release may flip the default to on.
-var defaultPolicyEnabled = sync.OnceValue(func() bool {
- if v, ok := os.LookupEnv("BUILDX_DEFAULT_POLICY"); ok {
- if vv, err := strconv.ParseBool(v); err == nil {
- return vv
- }
- }
- return false
-})
-
// policyExplicitlyDisabled reports whether the user passed `--policy
// disabled=true`, which suppresses both user-defined and builtin default
// policies.
@@ -664,7 +652,7 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
// (docker/dockerfile, docker/dockerfile-upstream) that may be implicitly
// loaded during a build, and passes through any other source so user
// policies retain full control.
- if defaultPolicyEnabled() && !policyExplicitlyDisabled(opt.Policy) {
+ if policy.DefaultPolicyEnabled() && !policyExplicitlyDisabled(opt.Policy) {
builtin := policyOpt{
Files: []policyFileSpec{{
Filename: policy.DefaultPolicyFilename,
diff --git a/builder/node.go b/builder/node.go
index abffe0ce1..95e56e3b0 100644
--- a/builder/node.go
+++ b/builder/node.go
@@ -8,15 +8,19 @@ import (
"github.com/containerd/platforms"
"github.com/docker/buildx/driver"
+ "github.com/docker/buildx/policy"
"github.com/docker/buildx/store"
"github.com/docker/buildx/store/storeutil"
+ "github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/buildx/util/platformutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/util/grpcerrors"
+ digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
)
@@ -109,6 +113,19 @@ func (b *Builder) LoadNodes(ctx context.Context, opts ...LoadNodesOption) (_ []N
}
}
+ var imageVerifier driver.ImageVerifier
+ if policy.DefaultPolicyEnabled() {
+ pol := policy.DefaultPolicy(policy.Opt{
+ Log: func(_ logrus.Level, msg string) {
+ logrus.Debug(msg)
+ },
+ VerifierProvider: policy.SignatureVerifier(confutil.NewConfig(b.opts.dockerCli)),
+ })
+ imageVerifier = func(ctx context.Context, ref string, platform *ocispecs.Platform, resolver policy.SourceMetadataResolver) (digest.Digest, error) {
+ return pol.CheckSource(ctx, ref, platform, resolver)
+ }
+ }
+
for i, n := range b.NodeGroup.Nodes {
func(i int, n store.Node) {
eg.Go(func() error {
@@ -137,6 +154,7 @@ func (b *Builder) LoadNodes(ctx context.Context, opts ...LoadNodesOption) (_ []N
Files: n.Files,
DriverOpts: n.DriverOpts,
Auth: imageopt.Auth,
+ ImageVerifier: imageVerifier,
Platforms: n.Platforms,
ContextPathHash: b.opts.contextPathHash,
DialMeta: lno.dialMeta,
diff --git a/docs/reference/buildx.md b/docs/reference/buildx.md
index 0541572d0..3aa0cb70f 100644
--- a/docs/reference/buildx.md
+++ b/docs/reference/buildx.md
@@ -45,3 +45,11 @@ Extended build capabilities with BuildKit
### Override the configured builder instance (--builder)
You can also use the `BUILDX_BUILDER` environment variable.
+
+### Enable the default policy
+
+Set `BUILDX_DEFAULT_POLICY=1` to enable Buildx's built-in source policy. The
+policy verifies signed tags for images managed by Docker, including BuildKit
+builder images and Dockerfile frontends. Untagged digest references and images
+outside the managed repositories are allowed unchanged. Tagged references
+that also contain a digest still have their release identity verified.
diff --git a/docs/reference/buildx_create.md b/docs/reference/buildx_create.md
index 0c8d9f0ea..666ecd95c 100644
--- a/docs/reference/buildx_create.md
+++ b/docs/reference/buildx_create.md
@@ -173,6 +173,18 @@ documentation for the specific driver:
* [`kubernetes` driver](https://docs.docker.com/build/builders/drivers/kubernetes/)
* [`remote` driver](https://docs.docker.com/build/builders/drivers/remote/)
+With `BUILDX_DEFAULT_POLICY=1`, the `docker-container` driver verifies signed
+`moby/buildkit` builder image tags before creating the builder. To explicitly
+bypass this verification, set `allow-untrusted-image=true`. For example:
+
+```console
+$ BUILDX_DEFAULT_POLICY=1 docker buildx create --driver docker-container \
+ --driver-opt allow-untrusted-image=true
+```
+
+Only use this option for an image that you trust. It disables builder image
+verification for the new builder node.
+
### Remove a node from a builder (--leave)
The `--leave` flag changes the action of the command to remove a node from a
diff --git a/driver/bkimage/bkimage.go b/driver/bkimage/bkimage.go
index f2443e7c1..464adfb22 100644
--- a/driver/bkimage/bkimage.go
+++ b/driver/bkimage/bkimage.go
@@ -1,7 +1,14 @@
package bkimage
const (
- DefaultImage = "moby/buildkit:buildx-stable-1" // TODO: make this verified
- QemuImage = "tonistiigi/binfmt:latest" // TODO: make this verified
+ DefaultImage = "moby/buildkit:buildx-stable-1"
+ QemuImage = "tonistiigi/binfmt:latest" // TODO: make this verified
DefaultRootlessImage = DefaultImage + "-rootless"
+
+ // TrustedRepo is the fully-qualified repository whose tags are verified
+ // against the builtin default policy before a builder is created from
+ // them. Images from other repositories pass through unverified; the
+ // allow-untrusted-image driver-opt is only needed when a TrustedRepo tag
+ // that the policy covers does not verify correctly.
+ TrustedRepo = "docker.io/moby/buildkit"
)
diff --git a/driver/docker-container/driver.go b/driver/docker-container/driver.go
index f8b803a48..2e287721b 100644
--- a/driver/docker-container/driver.go
+++ b/driver/docker-container/driver.go
@@ -13,12 +13,15 @@ import (
"time"
cerrdefs "github.com/containerd/errdefs"
+ "github.com/containerd/platforms"
+ "github.com/distribution/reference"
"github.com/docker/buildx/driver"
"github.com/docker/buildx/driver/bkimage"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/ghutil"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/buildx/util/progress"
+ "github.com/docker/buildx/util/sourcemeta"
"github.com/docker/cli/cli/context/docker"
contextstore "github.com/docker/cli/cli/context/store"
"github.com/docker/cli/opts"
@@ -29,6 +32,7 @@ import (
"github.com/moby/moby/api/types/mount"
dockerclient "github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/security"
+ ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
@@ -43,21 +47,22 @@ type Driver struct {
// if you add fields, remember to update docs:
// https://github.com/docker/docs/blob/main/content/build/drivers/docker-container.md
- netMode string
- image string
- memory opts.MemBytes
- memorySwap opts.MemSwapBytes
- cpuQuota int64
- cpuPeriod int64
- cpuShares int64
- cpusetCpus string
- cpusetMems string
- cgroupParent string
- restartPolicy container.RestartPolicy
- env []string
- defaultLoad bool
- gpus []container.DeviceRequest
- writeProvenanceGHA bool
+ netMode string
+ image string
+ allowUntrustedImage bool
+ memory opts.MemBytes
+ memorySwap opts.MemSwapBytes
+ cpuQuota int64
+ cpuPeriod int64
+ cpuShares int64
+ cpusetCpus string
+ cpusetMems string
+ cgroupParent string
+ restartPolicy container.RestartPolicy
+ env []string
+ defaultLoad bool
+ gpus []container.DeviceRequest
+ writeProvenanceGHA bool
}
func (d *Driver) IsMobyDriver() bool {
@@ -92,27 +97,59 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
imageName = d.image
}
- if err := l.Wrap("pulling image "+imageName, func() error {
- ra, err := imagetools.RegistryAuthForRef(imageName, d.Auth)
+ imageRef := imageName
+ pullErr := l.Wrap("pulling image "+imageRef, func() error {
+ ra, err := imagetools.RegistryAuthForRef(imageRef, d.Auth)
if err != nil {
return err
}
- resp, err := d.DockerAPI.ImagePull(ctx, imageName, dockerclient.ImagePullOptions{
+ resp, err := d.DockerAPI.ImagePull(ctx, imageRef, dockerclient.ImagePullOptions{
RegistryAuth: ra,
})
if err != nil {
return err
}
return resp.Wait(ctx)
- }); err != nil {
- // image pulling failed, check if it exists in local image store.
- // if not, return pulling error. otherwise log it.
- _, errInspect := d.DockerAPI.ImageInspect(ctx, imageName)
- found := errInspect == nil
- if !found {
+ })
+ image, inspectErr := d.DockerAPI.ImageInspect(ctx, imageRef)
+ if inspectErr != nil {
+ if pullErr != nil {
+ return pullErr
+ }
+ return errors.Wrapf(inspectErr, "failed to inspect pulled image %s", imageRef)
+ }
+
+ imageName = imageRef
+ if image.Descriptor != nil && d.ImageVerifier != nil && !d.allowUntrustedImage {
+ named, err := reference.ParseNormalizedNamed(imageRef)
+ if err != nil {
+ return errors.Wrapf(err, "failed to parse image reference %s", imageRef)
+ }
+ if named.Name() == bkimage.TrustedRepo {
+ if _, canonical := named.(reference.Canonical); !canonical {
+ named = reference.TagNameOnly(named)
+ }
+ pinned, err := reference.WithDigest(named, image.Descriptor.Digest)
+ if err != nil {
+ return errors.Wrapf(err, "failed to construct image reference for %s", imageRef)
+ }
+ imageName = pinned.String()
+ }
+ }
+
+ // Policy verification requires the immutable descriptor exposed by the
+ // containerd image store. Classic-store images are allowed without it.
+ if image.Descriptor != nil {
+ var err error
+ imageName, err = d.verifiedImageRef(ctx, l, imageName)
+ if err != nil {
+ return err
+ }
+ }
+ if pullErr != nil {
+ if err := l.Wrap("using local image "+imageName, func() error { return nil }); err != nil {
return err
}
- l.Wrap("pulling failed, using local image "+imageName, func() error { return nil })
}
cfg := &container.Config{
@@ -229,6 +266,65 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
})
}
+// verifiedImageRef evaluates ref against the builtin default policy and
+// returns the canonical reference carrying the digest that verification
+// resolved. Source metadata is resolved through the BuildKit embedded in the
+// Docker daemon that hosts the builder, so registry access follows the
+// daemon configuration. The reference is returned unchanged when the policy
+// does not apply to it: policy disabled, allow-untrusted-image set, the
+// image pinned by digest without a tag, or an image outside the managed
+// moby/buildkit repository (which the default policy passes through).
+func (d *Driver) verifiedImageRef(ctx context.Context, l progress.SubLogger, ref string) (string, error) {
+ if d.ImageVerifier == nil || d.allowUntrustedImage {
+ return ref, nil
+ }
+
+ c, err := d.buildkitClient(ctx)
+ if err != nil {
+ return "", errors.Wrap(err, "failed to connect to BuildKit for image verification")
+ }
+ defer c.Close()
+ mr := sourcemeta.NewResolver(c)
+ defer mr.Close()
+
+ pinned, applied, err := driver.VerifyImageRef(ctx, l, ref, d.daemonPlatform(ctx), mr, d.ImageVerifier)
+ if err != nil {
+ if !applied {
+ return "", err
+ }
+ return "", errors.Wrapf(err, "failed to verify image %s", ref)
+ }
+ if !applied {
+ return ref, nil
+ }
+ return pinned, nil
+}
+
+// buildkitClient returns a client to the BuildKit embedded in the Docker
+// daemon that hosts the builder container.
+func (d *Driver) buildkitClient(ctx context.Context) (*client.Client, error) {
+ return client.New(ctx, "",
+ client.WithContextDialer(func(context.Context, string) (net.Conn, error) {
+ return d.DockerAPI.DialHijack(ctx, "/grpc", "h2c", d.DialMeta)
+ }),
+ client.WithSessionDialer(func(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) {
+ return d.DockerAPI.DialHijack(ctx, "/session", proto, meta)
+ }),
+ )
+}
+
+// daemonPlatform returns the platform of the images the daemon pulls,
+// falling back to the client platform when daemon info is unavailable.
+func (d *Driver) daemonPlatform(ctx context.Context) *ocispecs.Platform {
+ if resp, err := d.DockerAPI.Info(ctx, dockerclient.InfoOptions{}); err == nil {
+ if p, err := platforms.Parse(resp.Info.OSType + "/" + resp.Info.Architecture); err == nil {
+ return &p
+ }
+ }
+ p := platforms.Normalize(platforms.DefaultSpec())
+ return &p
+}
+
func (d *Driver) wait(ctx context.Context, l progress.SubLogger) error {
try := 1
for {
diff --git a/driver/docker-container/factory.go b/driver/docker-container/factory.go
index ee95ae673..ed3782ddf 100644
--- a/driver/docker-container/factory.go
+++ b/driver/docker-container/factory.go
@@ -117,6 +117,11 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
if err != nil {
return nil, err
}
+ case k == "allow-untrusted-image":
+ d.allowUntrustedImage, err = strconv.ParseBool(v)
+ if err != nil {
+ return nil, err
+ }
default:
return nil, errors.Errorf("invalid driver option %s for docker-container driver", k)
}
diff --git a/driver/image.go b/driver/image.go
new file mode 100644
index 000000000..d44f12188
--- /dev/null
+++ b/driver/image.go
@@ -0,0 +1,54 @@
+package driver
+
+import (
+ "context"
+
+ "github.com/distribution/reference"
+ "github.com/docker/buildx/driver/bkimage"
+ "github.com/docker/buildx/policy"
+ "github.com/docker/buildx/util/progress"
+ digest "github.com/opencontainers/go-digest"
+ ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+)
+
+// VerifyImageRef evaluates ref against the builtin default policy through
+// verify and returns the reference pinned to the digest that verification
+// resolved. The boolean result reports whether the policy applied: it is
+// false, with ref returned unchanged, when there is no verifier, when ref is
+// pinned by digest without a tag, or when ref is outside the managed
+// moby/buildkit repository (unmanaged images pass through the default policy
+// unchanged). A tagged canonical reference is still verified because its tag
+// carries the release identity checked by the policy.
+func VerifyImageRef(ctx context.Context, l progress.SubLogger, ref string, platform *ocispecs.Platform, resolver policy.SourceMetadataResolver, verify ImageVerifier) (string, bool, error) {
+ if verify == nil {
+ return ref, false, nil
+ }
+ named, err := reference.ParseNormalizedNamed(ref)
+ if err != nil {
+ return "", false, errors.Wrapf(err, "failed to parse image reference %s", ref)
+ }
+ _, isCanonical := named.(reference.Canonical)
+ _, isTagged := named.(reference.Tagged)
+ if isCanonical && !isTagged {
+ return ref, false, nil
+ }
+ if named.Name() != bkimage.TrustedRepo {
+ return ref, false, nil
+ }
+ named = reference.TagNameOnly(named)
+
+ var dgst digest.Digest
+ if err := l.Wrap("verifying image "+named.String(), func() error {
+ var err error
+ dgst, err = verify(ctx, named.String(), platform, resolver)
+ return err
+ }); err != nil {
+ return "", true, err
+ }
+ canonical, err := reference.WithDigest(named, dgst)
+ if err != nil {
+ return "", true, errors.Wrapf(err, "failed to construct canonical reference for %s", ref)
+ }
+ return canonical.String(), true, nil
+}
diff --git a/driver/image_test.go b/driver/image_test.go
new file mode 100644
index 000000000..a40b2261a
--- /dev/null
+++ b/driver/image_test.go
@@ -0,0 +1,47 @@
+package driver
+
+import (
+ "context"
+ "testing"
+
+ "github.com/docker/buildx/policy"
+ "github.com/moby/buildkit/client"
+ digest "github.com/opencontainers/go-digest"
+ ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/stretchr/testify/require"
+)
+
+type nopSubLogger struct{}
+
+func (nopSubLogger) Wrap(_ string, fn func() error) error { return fn() }
+func (nopSubLogger) Log(int, []byte) {}
+func (nopSubLogger) SetStatus(*client.VertexStatus) {}
+
+func TestVerifyImageRefTaggedCanonical(t *testing.T) {
+ dgst := digest.FromString("buildkit")
+ ref := "moby/buildkit:v0.31.2@" + dgst.String()
+ var verifiedRef string
+
+ pinned, applied, err := VerifyImageRef(context.Background(), nopSubLogger{}, ref, nil, nil, func(_ context.Context, ref string, _ *ocispecs.Platform, _ policy.SourceMetadataResolver) (digest.Digest, error) {
+ verifiedRef = ref
+ return dgst, nil
+ })
+ require.NoError(t, err)
+ require.True(t, applied)
+ require.Equal(t, "docker.io/"+ref, verifiedRef)
+ require.Equal(t, "docker.io/"+ref, pinned)
+}
+
+func TestVerifyImageRefDigestOnly(t *testing.T) {
+ dgst := digest.FromString("buildkit")
+ called := false
+
+ pinned, applied, err := VerifyImageRef(context.Background(), nopSubLogger{}, "moby/buildkit@"+dgst.String(), nil, nil, func(_ context.Context, _ string, _ *ocispecs.Platform, _ policy.SourceMetadataResolver) (digest.Digest, error) {
+ called = true
+ return dgst, nil
+ })
+ require.NoError(t, err)
+ require.False(t, applied)
+ require.False(t, called)
+ require.Equal(t, "moby/buildkit@"+dgst.String(), pinned)
+}
diff --git a/driver/manager.go b/driver/manager.go
index 9a3e3b5d4..b9ef2c275 100644
--- a/driver/manager.go
+++ b/driver/manager.go
@@ -5,11 +5,13 @@ import (
"sort"
"sync"
+ "github.com/docker/buildx/policy"
"github.com/docker/cli/cli/context/store"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/util/tracing/delegated"
dockerclient "github.com/moby/moby/client"
+ digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
@@ -27,6 +29,15 @@ type BuildkitConfig struct {
// Rootless bool
}
+// ImageVerifier validates the builder image ref against the builtin default
+// policy and returns the digest that verification resolved the reference to.
+// Drivers that materialize the builder from an image call it before creating
+// the builder so the verified digest can be used instead of the mutable tag.
+// Source metadata is resolved through the given resolver, which drivers back
+// with a BuildKit instance they have access to so that registry access
+// happens where the image is pulled from.
+type ImageVerifier func(ctx context.Context, ref string, platform *ocispecs.Platform, resolver policy.SourceMetadataResolver) (digest.Digest, error)
+
type InitConfig struct {
Name string
EndpointAddr string
@@ -36,6 +47,7 @@ type InitConfig struct {
Files map[string][]byte
DriverOpts map[string]string
Auth authprovider.AuthConfigProvider
+ ImageVerifier ImageVerifier
Platforms []ocispecs.Platform
ContextPathHash string
DialMeta map[string][]string
diff --git a/policy/default.go b/policy/default.go
index 188969cb6..9161f0835 100644
--- a/policy/default.go
+++ b/policy/default.go
@@ -2,6 +2,9 @@ package policy
import (
_ "embed"
+ "os"
+ "strconv"
+ "sync"
)
// DefaultPolicyFilename is the synthetic filename used for the embedded
@@ -15,3 +18,28 @@ var defaultPolicyModule []byte
func DefaultPolicyData() []byte {
return defaultPolicyModule
}
+
+// DefaultPolicy returns a Policy instance backed by the embedded default
+// policy module. Any files in opt are replaced: the default policy is always
+// evaluated standalone.
+func DefaultPolicy(opt Opt) *Policy {
+ opt.Files = []File{{
+ Filename: DefaultPolicyFilename,
+ Data: DefaultPolicyData(),
+ }}
+ return NewPolicy(opt)
+}
+
+// DefaultPolicyEnabled reports whether the builtin default policies are
+// enabled via the BUILDX_DEFAULT_POLICY environment variable. It is opt-in
+// for now; a future release may flip the default to on. The gate covers both
+// the default source policy applied to builds and the builder-image policy
+// applied when creating container builders.
+var DefaultPolicyEnabled = sync.OnceValue(func() bool {
+ if v, ok := os.LookupEnv("BUILDX_DEFAULT_POLICY"); ok {
+ if vv, err := strconv.ParseBool(v); err == nil {
+ return vv
+ }
+ }
+ return false
+})
diff --git a/policy/default.rego b/policy/default.rego
index 2e89c1bab..d3c9df12c 100644
--- a/policy/default.rego
+++ b/policy/default.rego
@@ -1,18 +1,26 @@
package docker
# Default policy embedded in Buildx. It verifies trust for images shipped
-# by Docker that may be implicitly loaded during a build:
+# by Docker that may be implicitly loaded during a build or used to run a
+# build:
#
# - docker/dockerfile
# - docker/dockerfile-upstream
# - docker/buildkit-syft-scanner
+# - moby/buildkit
#
# Any image outside this managed set is allowed and passes through to user
# policies unchanged. Access by digest is always allowed. For tag-based
# access the rules below enforce a signed release from the expected GitHub
# source repository using the existing docker_github_builder_signature
# helper from builtins.rego.
-
+#
+# The moby/buildkit rules also apply when Buildx pulls the image to create a
+# container builder; the docker-container driver evaluates this same policy
+# before creating the builder and pins the image to the digest the evaluation
+# resolved. Only known tags and their variants require a matching signature;
+# releases that predate signing (before v0.27.0) and unrecognized tags pass
+# through like any unmanaged image.
is_dockerfile if {
input.image
input.image.fullRepo == "docker.io/docker/dockerfile"
@@ -28,6 +36,11 @@ is_syft_scanner if {
input.image.fullRepo == "docker.io/docker/buildkit-syft-scanner"
}
+is_buildkit_image if {
+ input.image
+ input.image.fullRepo == "docker.io/moby/buildkit"
+}
+
dockerfile_floating_tag(tag) if tag == "latest"
dockerfile_floating_tag(tag) if tag == "labs"
dockerfile_floating_tag(tag) if tag == "master"
@@ -40,6 +53,40 @@ 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)
+# moby/buildkit floating tags are a closed enumeration, not prefix wildcards,
+# so tags like master-cache (a cache manifest, not a runnable image) are not
+# subjected to a signature check they could never pass.
+buildkit_floating_tag(tag) if tag in {
+ "latest", "latest-ubuntu", "rootless",
+ "master", "master-rootless", "master-ubuntu",
+ "nightly", "nightly-rootless", "nightly-ubuntu",
+}
+
+buildkit_floating_tag(tag) if regex.match(`^buildx-stable-\d+(?:-rootless|-gpu)?$`, tag)
+
+# buildkit_release_version returns the vX.Y.Z[-rcN] release a version tag
+# refers to, with the image variant suffix stripped. Release tags carry a
+# signature whose source repository ref names exactly this version.
+buildkit_release_version(tag) := v if {
+ m := regex.find_all_string_submatch_n(`^(v\d+\.\d+\.\d+(?:-rc\d+)?)(?:-rootless|-ubuntu)?$`, tag, 1)
+ count(m) == 1
+ v := m[0][1]
+}
+
+# v0.27.0 is the first signed moby/buildkit release.
+buildkit_version_signed(version) if {
+ m := regex.find_all_string_submatch_n(`^v(\d+)\.(\d+)\.`, version, 1)
+ count(m) == 1
+ to_number(m[0][1]) > 0
+}
+
+buildkit_version_signed(version) if {
+ m := regex.find_all_string_submatch_n(`^v(\d+)\.(\d+)\.`, version, 1)
+ count(m) == 1
+ to_number(m[0][1]) == 0
+ to_number(m[0][2]) >= 27
+}
+
default_policy_deny_msgs contains msg if {
is_dockerfile
@@ -59,6 +106,37 @@ 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_buildkit_image
+ tag := input.image.tag
+ tag != ""
+ buildkit_floating_tag(tag)
+ not buildkit_floating_sig_ok
+ msg := sprintf("image %s is not allowed by default policy: a verified docker-github-builder signature is required for %s tag", [input.image.ref, tag])
+}
+
+default_policy_deny_msgs contains msg if {
+ is_buildkit_image
+ tag := input.image.tag
+ tag != ""
+ not buildkit_floating_tag(tag)
+ version := buildkit_release_version(tag)
+ buildkit_version_signed(version)
+ not buildkit_release_sig_ok(version)
+ msg := sprintf("image %s is not allowed by default policy: a verified docker-github-builder signature is required for %s tag", [input.image.ref, tag])
+}
+
+buildkit_floating_sig_ok if {
+ some sig in input.image.signatures
+ docker_github_builder_signature(sig, "moby/buildkit")
+}
+
+buildkit_release_sig_ok(version) if {
+ some sig in input.image.signatures
+ docker_github_builder_signature(sig, "moby/buildkit")
+ sig.signer.sourceRepositoryRef == sprintf("refs/tags/%s", [version])
+}
+
dockerfile_sig_ok(tag) if {
dockerfile_floating_tag(tag)
some sig in input.image.signatures
diff --git a/policy/default_test.go b/policy/default_test.go
index 749accb04..625e66b03 100644
--- a/policy/default_test.go
+++ b/policy/default_test.go
@@ -33,11 +33,7 @@ func makeDefaultPolicy(t *testing.T, sigInfo *policytypes.SignatureInfo) *Policy
}, nil
}
}
- return NewPolicy(Opt{
- Files: []File{{
- Filename: DefaultPolicyFilename,
- Data: DefaultPolicyData(),
- }},
+ return DefaultPolicy(Opt{
Log: func(level logrus.Level, msg string) {
t.Logf("[%s] %s", level, msg)
},
@@ -400,3 +396,150 @@ func TestDefaultPolicySyftScannerImages(t *testing.T) {
})
}
}
+
+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)
+ })
+ }
+}
diff --git a/policy/image_policy.go b/policy/image_policy.go
new file mode 100644
index 000000000..a9c0878d9
--- /dev/null
+++ b/policy/image_policy.go
@@ -0,0 +1,154 @@
+package policy
+
+import (
+ "context"
+ "strings"
+
+ "github.com/containerd/platforms"
+ "github.com/distribution/reference"
+ "github.com/docker/buildx/util/sourcemeta"
+ gwpb "github.com/moby/buildkit/frontend/gateway/pb"
+ "github.com/moby/buildkit/solver/pb"
+ spb "github.com/moby/buildkit/sourcepolicy/pb"
+ "github.com/moby/buildkit/sourcepolicy/policysession"
+ digest "github.com/opencontainers/go-digest"
+ ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+)
+
+// ImageVerificationError is returned by CheckSource when the policy denied
+// the image. Any other error from CheckSource means the policy could not be
+// evaluated at all (e.g. source metadata could not be resolved).
+type ImageVerificationError struct {
+ Ref string
+ Messages []string
+}
+
+func (e *ImageVerificationError) Error() string {
+ if len(e.Messages) == 0 {
+ return "image " + e.Ref + " is not allowed by policy"
+ }
+ return strings.Join(e.Messages, "; ")
+}
+
+// CheckSource evaluates the policy against a source reference and returns
+// the digest it resolved to. Source metadata, including the signature
+// attestation chain, is resolved through resolver, usually a BuildKit
+// gateway (see sourcemeta.NewResolver). Only docker-image sources are
+// supported: source is either a plain image reference or a docker-image://
+// identifier.
+func (p *Policy) CheckSource(ctx context.Context, source string, platform *ocispecs.Platform, resolver SourceMetadataResolver) (digest.Digest, error) {
+ refstr, ok := strings.CutPrefix(source, "docker-image://")
+ if !ok {
+ if strings.Contains(source, "://") {
+ return "", errors.Errorf("unsupported source %s for policy evaluation", source)
+ }
+ refstr = source
+ }
+ named, err := reference.ParseNormalizedNamed(refstr)
+ if err != nil {
+ return "", errors.Wrapf(err, "failed to parse image reference %s", refstr)
+ }
+ named = reference.TagNameOnly(named)
+
+ if platform == nil {
+ pl := platforms.Normalize(platforms.DefaultSpec())
+ platform = &pl
+ }
+
+ srcOp := &pb.SourceOp{Identifier: "docker-image://" + named.String()}
+ src := &gwpb.ResolveSourceMetaResponse{Source: srcOp}
+
+ for range maxResolveIterations {
+ decision, next, err := p.CheckPolicy(ctx, &policysession.CheckPolicyRequest{
+ Platform: toPBPlatform(platform),
+ Source: src,
+ })
+ if err != nil {
+ return "", err
+ }
+ if next != nil {
+ src, err = resolveSourceMeta(ctx, resolver, next, srcOp, platform)
+ if err != nil {
+ return "", err
+ }
+ if next.Image != nil && next.Image.AttestationChain && (src.Image == nil || src.Image.AttestationChain == nil) {
+ return "", errors.Errorf("no signature metadata available for %s: image is not signed or the daemon does not support resolving it", named.String())
+ }
+ continue
+ }
+ if decision == nil {
+ return "", errors.New("policy returned no decision")
+ }
+ switch decision.Action {
+ case spb.PolicyAction_ALLOW, spb.PolicyAction_CONVERT:
+ return sourceDigest(ctx, resolver, src, srcOp, named, platform)
+ case spb.PolicyAction_DENY:
+ msgs := make([]string, 0, len(decision.DenyMessages))
+ for _, m := range decision.DenyMessages {
+ if m != nil && m.Message != "" {
+ msgs = append(msgs, m.Message)
+ }
+ }
+ return "", errors.WithStack(&ImageVerificationError{Ref: named.String(), Messages: msgs})
+ default:
+ return "", errors.Errorf("unknown policy action %s", decision.Action)
+ }
+ }
+ return "", errors.New("maximum attempts reached for resolving image metadata")
+}
+
+func resolveSourceMeta(ctx context.Context, resolver SourceMetadataResolver, req *gwpb.ResolveSourceMetaRequest, srcOp *pb.SourceOp, platform *ocispecs.Platform) (*gwpb.ResolveSourceMetaResponse, error) {
+ if resolver == nil {
+ return nil, errors.New("source metadata resolver is required for policy evaluation")
+ }
+ target := srcOp
+ if req.Source != nil {
+ target = req.Source
+ }
+ resp, err := resolver.ResolveSourceMetadata(ctx, target, sourcemeta.ToResolverOpt(req, platform))
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to resolve source metadata for %s", target.Identifier)
+ }
+ out := sourcemeta.ToGatewayMetaResponse(resp)
+ if out.Source == nil {
+ out.Source = target
+ }
+ return out, nil
+}
+
+// sourceDigest returns the digest the allowed source resolved to so callers
+// can pin it. When the policy decided without loading image metadata the
+// digest comes from the reference itself or from one extra resolution.
+func sourceDigest(ctx context.Context, resolver SourceMetadataResolver, src *gwpb.ResolveSourceMetaResponse, srcOp *pb.SourceOp, named reference.Named, platform *ocispecs.Platform) (digest.Digest, error) {
+ if src.Image != nil && src.Image.Digest != "" {
+ return digest.Digest(src.Image.Digest), nil
+ }
+ if canonical, ok := named.(reference.Canonical); ok {
+ return canonical.Digest(), nil
+ }
+ resp, err := resolveSourceMeta(ctx, resolver, &gwpb.ResolveSourceMetaRequest{
+ Source: srcOp,
+ Image: &gwpb.ResolveSourceImageRequest{NoConfig: true},
+ }, srcOp, platform)
+ if err != nil {
+ return "", err
+ }
+ if resp.Image == nil || resp.Image.Digest == "" {
+ return "", errors.Errorf("failed to resolve digest for %s", named.String())
+ }
+ return digest.Digest(resp.Image.Digest), nil
+}
+
+func toPBPlatform(p *ocispecs.Platform) *pb.Platform {
+ if p == nil {
+ return nil
+ }
+ return &pb.Platform{
+ OS: p.OS,
+ Architecture: p.Architecture,
+ Variant: p.Variant,
+ OSVersion: p.OSVersion,
+ OSFeatures: p.OSFeatures,
+ }
+}
diff --git a/policy/image_policy_test.go b/policy/image_policy_test.go
new file mode 100644
index 000000000..209922d0f
--- /dev/null
+++ b/policy/image_policy_test.go
@@ -0,0 +1,184 @@
+package policy
+
+import (
+ "context"
+ "testing"
+
+ "github.com/moby/buildkit/client/llb/sourceresolver"
+ gwpb "github.com/moby/buildkit/frontend/gateway/pb"
+ "github.com/moby/buildkit/solver/pb"
+ policyimage "github.com/moby/policy-helpers/image"
+ policytypes "github.com/moby/policy-helpers/types"
+ digest "github.com/opencontainers/go-digest"
+ ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/require"
+)
+
+type fakeSourceResolver func(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error)
+
+func (f fakeSourceResolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
+ return f(ctx, op, opt)
+}
+
+// toSourceResolverChain converts a gateway attestation chain, as built by the
+// shared test helpers, into the resolver-typed chain a BuildKit gateway
+// returns.
+func toSourceResolverChain(t *testing.T, ac *gwpb.AttestationChain) *sourceresolver.AttestationChain {
+ t.Helper()
+ out := &sourceresolver.AttestationChain{
+ Root: digest.Digest(ac.Root),
+ ImageManifest: digest.Digest(ac.ImageManifest),
+ AttestationManifest: digest.Digest(ac.AttestationManifest),
+ Blobs: map[digest.Digest]sourceresolver.Blob{},
+ }
+ for _, sm := range ac.SignatureManifests {
+ out.SignatureManifests = append(out.SignatureManifests, digest.Digest(sm))
+ }
+ for dgst, blob := range ac.Blobs {
+ out.Blobs[digest.Digest(dgst)] = sourceresolver.Blob{
+ Descriptor: ocispecs.Descriptor{
+ MediaType: blob.Descriptor_.MediaType,
+ Digest: digest.Digest(blob.Descriptor_.Digest),
+ Size: blob.Descriptor_.Size,
+ },
+ Data: blob.Data,
+ }
+ }
+ return out
+}
+
+func signedSigVerifier(sig *policytypes.SignatureInfo) PolicyVerifierProvider {
+ return func() (PolicyVerifier, error) {
+ return &mockPolicyVerifier{
+ verifyImage: func(_ context.Context, _ policyimage.ReferrersProvider, _ ocispecs.Descriptor, _ *ocispecs.Platform) (*policytypes.SignatureInfo, error) {
+ return sig, nil
+ },
+ }, nil
+ }
+}
+
+func TestCheckSourceSigned(t *testing.T) {
+ imgDigest := digest.FromString("resolved-image")
+ var sawChainRequest bool
+
+ resolver := fakeSourceResolver(func(_ context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
+ require.Equal(t, "docker-image://docker.io/moby/buildkit:v0.31.2", op.Identifier)
+ require.NotNil(t, opt.ImageOpt)
+ if opt.ImageOpt.AttestationChain {
+ sawChainRequest = true
+ }
+ return &sourceresolver.MetaResponse{
+ Op: op,
+ Image: &sourceresolver.ResolveImageResponse{
+ Digest: imgDigest,
+ AttestationChain: toSourceResolverChain(t, newTestAttestationChain(t)),
+ },
+ }, nil
+ })
+
+ p := DefaultPolicy(Opt{VerifierProvider: signedSigVerifier(dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"))})
+ dgst, err := p.CheckSource(context.Background(), "moby/buildkit:v0.31.2", &ocispecs.Platform{OS: "linux", Architecture: "amd64"}, resolver)
+ require.NoError(t, err)
+ require.Equal(t, imgDigest, dgst)
+ require.True(t, sawChainRequest)
+}
+
+func TestCheckSourceDenied(t *testing.T) {
+ newResolver := func(withSignatures bool) fakeSourceResolver {
+ return func(_ context.Context, op *pb.SourceOp, _ sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
+ ac := newTestAttestationChain(t)
+ if !withSignatures {
+ ac.SignatureManifests = nil
+ }
+ return &sourceresolver.MetaResponse{
+ Op: op,
+ Image: &sourceresolver.ResolveImageResponse{
+ Digest: digest.FromString("resolved-image"),
+ AttestationChain: toSourceResolverChain(t, ac),
+ },
+ }, nil
+ }
+ }
+
+ t.Run("unsigned-image", func(t *testing.T) {
+ p := DefaultPolicy(Opt{VerifierProvider: signedSigVerifier(dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"))})
+ _, err := p.CheckSource(context.Background(), "moby/buildkit:v0.31.2", &ocispecs.Platform{OS: "linux", Architecture: "amd64"}, newResolver(false))
+ var verr *ImageVerificationError
+ require.ErrorAs(t, err, &verr)
+ require.ErrorContains(t, err, "signature is required for v0.31.2 tag")
+ })
+
+ t.Run("signature-ref-mismatch", func(t *testing.T) {
+ p := DefaultPolicy(Opt{VerifierProvider: signedSigVerifier(dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.30.0"))})
+ _, err := p.CheckSource(context.Background(), "moby/buildkit:v0.31.2", &ocispecs.Platform{OS: "linux", Architecture: "amd64"}, newResolver(true))
+ var verr *ImageVerificationError
+ require.ErrorAs(t, err, &verr)
+ })
+
+ t.Run("floating-tag-without-signature", func(t *testing.T) {
+ p := DefaultPolicy(Opt{VerifierProvider: signedSigVerifier(dockerGithubBuilderSig("moby/buildkit", "refs/tags/v0.31.2"))})
+ _, err := p.CheckSource(context.Background(), "moby/buildkit:buildx-stable-1", &ocispecs.Platform{OS: "linux", Architecture: "amd64"}, newResolver(false))
+ var verr *ImageVerificationError
+ require.ErrorAs(t, err, &verr)
+ require.ErrorContains(t, err, "signature is required for buildx-stable-1 tag")
+ })
+}
+
+func TestCheckSourceOldTagPinnedWithoutMetadata(t *testing.T) {
+ // Releases before signing do not require metadata for the decision, but
+ // the digest is still resolved so the caller can pin the image.
+ imgDigest := digest.FromString("old-release")
+ var calls int
+
+ resolver := fakeSourceResolver(func(_ context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
+ calls++
+ require.NotNil(t, opt.ImageOpt)
+ require.False(t, opt.ImageOpt.AttestationChain)
+ require.True(t, opt.ImageOpt.NoConfig)
+ return &sourceresolver.MetaResponse{
+ Op: op,
+ Image: &sourceresolver.ResolveImageResponse{Digest: imgDigest},
+ }, nil
+ })
+
+ p := DefaultPolicy(Opt{})
+ dgst, err := p.CheckSource(context.Background(), "moby/buildkit:v0.26.2", nil, resolver)
+ require.NoError(t, err)
+ require.Equal(t, imgDigest, dgst)
+ require.Equal(t, 1, calls)
+}
+
+func TestCheckSourceCanonicalDigestWithoutResolution(t *testing.T) {
+ // An untagged canonical reference passes the policy and pins to its own
+ // digest without any metadata resolution.
+ dgst := digest.FromString("pinned")
+ resolver := fakeSourceResolver(func(context.Context, *pb.SourceOp, sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
+ return nil, errors.New("unexpected resolution")
+ })
+
+ p := DefaultPolicy(Opt{})
+ out, err := p.CheckSource(context.Background(), "moby/buildkit@"+dgst.String(), nil, resolver)
+ require.NoError(t, err)
+ require.Equal(t, dgst, out)
+}
+
+func TestCheckSourceMissingChainSupport(t *testing.T) {
+ // A daemon that cannot resolve the attestation chain (or an image
+ // without one) must produce a clear failure instead of looping.
+ resolver := fakeSourceResolver(func(_ context.Context, op *pb.SourceOp, _ sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
+ return &sourceresolver.MetaResponse{
+ Op: op,
+ Image: &sourceresolver.ResolveImageResponse{Digest: digest.FromString("resolved-image")},
+ }, nil
+ })
+
+ p := DefaultPolicy(Opt{})
+ _, err := p.CheckSource(context.Background(), "moby/buildkit:buildx-stable-1", nil, resolver)
+ require.ErrorContains(t, err, "no signature metadata available")
+}
+
+func TestCheckSourceUnsupportedScheme(t *testing.T) {
+ _, err := DefaultPolicy(Opt{}).CheckSource(context.Background(), "git://github.com/moby/buildkit.git", nil, nil)
+ require.ErrorContains(t, err, "unsupported source")
+}
diff --git a/tests/create.go b/tests/create.go
index 093351974..55606756b 100644
--- a/tests/create.go
+++ b/tests/create.go
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/docker/buildx/driver"
+ "github.com/docker/buildx/driver/bkimage"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/stretchr/testify/require"
@@ -25,6 +26,7 @@ var createTests = []func(t *testing.T, sb integration.Sandbox){
testCreateRestartAlways,
testCreateRemoteContainer,
testCreateWithProvenanceGHA,
+ testCreateCustomImageWithDefaultPolicy,
}
func testCreateMemoryLimit(t *testing.T, sb integration.Sandbox) {
@@ -111,6 +113,43 @@ func testCreateRemoteContainer(t *testing.T, sb integration.Sandbox) {
require.Fail(t, "remote builder is not running")
}
+func testCreateCustomImageWithDefaultPolicy(t *testing.T, sb integration.Sandbox) {
+ if !isDockerContainerWorker(sb) {
+ t.Skip("only testing with docker-container worker")
+ }
+
+ customImage := "localhost:1/buildx-test/custom-buildkit:" + identity.NewID()
+ var builderName string
+
+ cmd := dockerCmd(sb, withArgs("image", "tag", bkimage.DefaultImage, customImage))
+ dt, err := cmd.CombinedOutput()
+ require.NoError(t, err, string(dt))
+
+ t.Cleanup(func() {
+ if builderName != "" {
+ out, err := rmCmd(sb, withArgs(builderName))
+ require.NoError(t, err, out)
+ }
+ cmd := dockerCmd(sb, withArgs("image", "rm", customImage))
+ out, err := cmd.CombinedOutput()
+ require.NoError(t, err, string(out))
+ })
+
+ // images outside the managed moby/buildkit repository pass through the
+ // default policy, so bootstrapping a builder from a custom local image
+ // needs no opt-out or registry access.
+ out, err := createCmd(sb,
+ withArgs("--driver", "docker-container", "--driver-opt", "image="+customImage),
+ withEnv("BUILDX_DEFAULT_POLICY=1"),
+ )
+ require.NoError(t, err, out)
+ builderName = strings.TrimSpace(out)
+
+ out, err = inspectCmd(sb, withArgs(builderName, "--bootstrap"), withEnv("BUILDX_DEFAULT_POLICY=1"))
+ require.NoError(t, err, out)
+ require.Contains(t, out, "using local image "+customImage)
+}
+
func testCreateWithProvenanceGHA(t *testing.T, sb integration.Sandbox) {
if !isDockerContainerWorker(sb) {
t.Skip("only testing with docker-container worker")