Merge pull request #3961 from tonistiigi/buildkit-default-policy
policy: verify BuildKit builder images
This commit is contained in:
+1
-13
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -45,3 +45,11 @@ Extended build capabilities with BuildKit
|
||||
### <a name="builder"></a> 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
### <a name="leave"></a> Remove a node from a builder (--leave)
|
||||
|
||||
The `--leave` flag changes the action of the command to remove a node from a
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
+80
-2
@@ -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
|
||||
|
||||
+148
-5
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user