policy: verify BuildKit builder images
Extend the built-in policy to validate signed moby/buildkit release and floating tags before docker-container builders are created. Pull the image first, inspect it through Docker, and bind verification to the descriptor digest. Resolve signature attestations through the BuildKit API embedded in the Docker daemon. If pulling fails, use a local image while applying the same verification when the containerd image store exposes an immutable descriptor. Keep the classic image-store behavior unchanged because no descriptor is available. Allow unmanaged repositories and digest-only references unchanged. Add the allow-untrusted-image driver option as an explicit verification bypass. Document the behavior and add policy, digest-pinning, and local fallback coverage. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
+1
-13
@@ -65,18 +65,6 @@ var sendGitQueryAsInput = sync.OnceValue(func() bool {
|
|||||||
return false
|
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
|
// policyExplicitlyDisabled reports whether the user passed `--policy
|
||||||
// disabled=true`, which suppresses both user-defined and builtin default
|
// disabled=true`, which suppresses both user-defined and builtin default
|
||||||
// policies.
|
// policies.
|
||||||
@@ -664,7 +652,7 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
|
|||||||
// (docker/dockerfile, docker/dockerfile-upstream) that may be implicitly
|
// (docker/dockerfile, docker/dockerfile-upstream) that may be implicitly
|
||||||
// loaded during a build, and passes through any other source so user
|
// loaded during a build, and passes through any other source so user
|
||||||
// policies retain full control.
|
// policies retain full control.
|
||||||
if defaultPolicyEnabled() && !policyExplicitlyDisabled(opt.Policy) {
|
if policy.DefaultPolicyEnabled() && !policyExplicitlyDisabled(opt.Policy) {
|
||||||
builtin := policyOpt{
|
builtin := policyOpt{
|
||||||
Files: []policyFileSpec{{
|
Files: []policyFileSpec{{
|
||||||
Filename: policy.DefaultPolicyFilename,
|
Filename: policy.DefaultPolicyFilename,
|
||||||
|
|||||||
@@ -8,15 +8,19 @@ import (
|
|||||||
|
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/buildx/driver"
|
"github.com/docker/buildx/driver"
|
||||||
|
"github.com/docker/buildx/policy"
|
||||||
"github.com/docker/buildx/store"
|
"github.com/docker/buildx/store"
|
||||||
"github.com/docker/buildx/store/storeutil"
|
"github.com/docker/buildx/store/storeutil"
|
||||||
|
"github.com/docker/buildx/util/confutil"
|
||||||
"github.com/docker/buildx/util/dockerutil"
|
"github.com/docker/buildx/util/dockerutil"
|
||||||
"github.com/docker/buildx/util/imagetools"
|
"github.com/docker/buildx/util/imagetools"
|
||||||
"github.com/docker/buildx/util/platformutil"
|
"github.com/docker/buildx/util/platformutil"
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/moby/buildkit/util/grpcerrors"
|
"github.com/moby/buildkit/util/grpcerrors"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"google.golang.org/grpc/codes"
|
"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 {
|
for i, n := range b.NodeGroup.Nodes {
|
||||||
func(i int, n store.Node) {
|
func(i int, n store.Node) {
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
@@ -137,6 +154,7 @@ func (b *Builder) LoadNodes(ctx context.Context, opts ...LoadNodesOption) (_ []N
|
|||||||
Files: n.Files,
|
Files: n.Files,
|
||||||
DriverOpts: n.DriverOpts,
|
DriverOpts: n.DriverOpts,
|
||||||
Auth: imageopt.Auth,
|
Auth: imageopt.Auth,
|
||||||
|
ImageVerifier: imageVerifier,
|
||||||
Platforms: n.Platforms,
|
Platforms: n.Platforms,
|
||||||
ContextPathHash: b.opts.contextPathHash,
|
ContextPathHash: b.opts.contextPathHash,
|
||||||
DialMeta: lno.dialMeta,
|
DialMeta: lno.dialMeta,
|
||||||
|
|||||||
@@ -45,3 +45,11 @@ Extended build capabilities with BuildKit
|
|||||||
### <a name="builder"></a> Override the configured builder instance (--builder)
|
### <a name="builder"></a> Override the configured builder instance (--builder)
|
||||||
|
|
||||||
You can also use the `BUILDX_BUILDER` environment variable.
|
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/)
|
* [`kubernetes` driver](https://docs.docker.com/build/builders/drivers/kubernetes/)
|
||||||
* [`remote` driver](https://docs.docker.com/build/builders/drivers/remote/)
|
* [`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)
|
### <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
|
The `--leave` flag changes the action of the command to remove a node from a
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package bkimage
|
package bkimage
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultImage = "moby/buildkit:buildx-stable-1" // TODO: make this verified
|
DefaultImage = "moby/buildkit:buildx-stable-1"
|
||||||
QemuImage = "tonistiigi/binfmt:latest" // TODO: make this verified
|
QemuImage = "tonistiigi/binfmt:latest" // TODO: make this verified
|
||||||
DefaultRootlessImage = DefaultImage + "-rootless"
|
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"
|
"time"
|
||||||
|
|
||||||
cerrdefs "github.com/containerd/errdefs"
|
cerrdefs "github.com/containerd/errdefs"
|
||||||
|
"github.com/containerd/platforms"
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/buildx/driver"
|
"github.com/docker/buildx/driver"
|
||||||
"github.com/docker/buildx/driver/bkimage"
|
"github.com/docker/buildx/driver/bkimage"
|
||||||
"github.com/docker/buildx/util/confutil"
|
"github.com/docker/buildx/util/confutil"
|
||||||
"github.com/docker/buildx/util/ghutil"
|
"github.com/docker/buildx/util/ghutil"
|
||||||
"github.com/docker/buildx/util/imagetools"
|
"github.com/docker/buildx/util/imagetools"
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
|
"github.com/docker/buildx/util/sourcemeta"
|
||||||
"github.com/docker/cli/cli/context/docker"
|
"github.com/docker/cli/cli/context/docker"
|
||||||
contextstore "github.com/docker/cli/cli/context/store"
|
contextstore "github.com/docker/cli/cli/context/store"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
@@ -29,6 +32,7 @@ import (
|
|||||||
"github.com/moby/moby/api/types/mount"
|
"github.com/moby/moby/api/types/mount"
|
||||||
dockerclient "github.com/moby/moby/client"
|
dockerclient "github.com/moby/moby/client"
|
||||||
"github.com/moby/moby/client/pkg/security"
|
"github.com/moby/moby/client/pkg/security"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,21 +47,22 @@ type Driver struct {
|
|||||||
|
|
||||||
// if you add fields, remember to update docs:
|
// if you add fields, remember to update docs:
|
||||||
// https://github.com/docker/docs/blob/main/content/build/drivers/docker-container.md
|
// https://github.com/docker/docs/blob/main/content/build/drivers/docker-container.md
|
||||||
netMode string
|
netMode string
|
||||||
image string
|
image string
|
||||||
memory opts.MemBytes
|
allowUntrustedImage bool
|
||||||
memorySwap opts.MemSwapBytes
|
memory opts.MemBytes
|
||||||
cpuQuota int64
|
memorySwap opts.MemSwapBytes
|
||||||
cpuPeriod int64
|
cpuQuota int64
|
||||||
cpuShares int64
|
cpuPeriod int64
|
||||||
cpusetCpus string
|
cpuShares int64
|
||||||
cpusetMems string
|
cpusetCpus string
|
||||||
cgroupParent string
|
cpusetMems string
|
||||||
restartPolicy container.RestartPolicy
|
cgroupParent string
|
||||||
env []string
|
restartPolicy container.RestartPolicy
|
||||||
defaultLoad bool
|
env []string
|
||||||
gpus []container.DeviceRequest
|
defaultLoad bool
|
||||||
writeProvenanceGHA bool
|
gpus []container.DeviceRequest
|
||||||
|
writeProvenanceGHA bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) IsMobyDriver() bool {
|
func (d *Driver) IsMobyDriver() bool {
|
||||||
@@ -92,27 +97,59 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
|
|||||||
imageName = d.image
|
imageName = d.image
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := l.Wrap("pulling image "+imageName, func() error {
|
imageRef := imageName
|
||||||
ra, err := imagetools.RegistryAuthForRef(imageName, d.Auth)
|
pullErr := l.Wrap("pulling image "+imageRef, func() error {
|
||||||
|
ra, err := imagetools.RegistryAuthForRef(imageRef, d.Auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := d.DockerAPI.ImagePull(ctx, imageName, dockerclient.ImagePullOptions{
|
resp, err := d.DockerAPI.ImagePull(ctx, imageRef, dockerclient.ImagePullOptions{
|
||||||
RegistryAuth: ra,
|
RegistryAuth: ra,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return resp.Wait(ctx)
|
return resp.Wait(ctx)
|
||||||
}); err != nil {
|
})
|
||||||
// image pulling failed, check if it exists in local image store.
|
image, inspectErr := d.DockerAPI.ImageInspect(ctx, imageRef)
|
||||||
// if not, return pulling error. otherwise log it.
|
if inspectErr != nil {
|
||||||
_, errInspect := d.DockerAPI.ImageInspect(ctx, imageName)
|
if pullErr != nil {
|
||||||
found := errInspect == nil
|
return pullErr
|
||||||
if !found {
|
}
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
l.Wrap("pulling failed, using local image "+imageName, func() error { return nil })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &container.Config{
|
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 {
|
func (d *Driver) wait(ctx context.Context, l progress.SubLogger) error {
|
||||||
try := 1
|
try := 1
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ func (f *factory) New(ctx context.Context, cfg driver.InitConfig) (driver.Driver
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
case k == "allow-untrusted-image":
|
||||||
|
d.allowUntrustedImage, err = strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, errors.Errorf("invalid driver option %s for docker-container driver", k)
|
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"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/policy"
|
||||||
"github.com/docker/cli/cli/context/store"
|
"github.com/docker/cli/cli/context/store"
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/moby/buildkit/session/auth/authprovider"
|
"github.com/moby/buildkit/session/auth/authprovider"
|
||||||
"github.com/moby/buildkit/util/tracing/delegated"
|
"github.com/moby/buildkit/util/tracing/delegated"
|
||||||
dockerclient "github.com/moby/moby/client"
|
dockerclient "github.com/moby/moby/client"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -27,6 +29,15 @@ type BuildkitConfig struct {
|
|||||||
// Rootless bool
|
// 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 {
|
type InitConfig struct {
|
||||||
Name string
|
Name string
|
||||||
EndpointAddr string
|
EndpointAddr string
|
||||||
@@ -36,6 +47,7 @@ type InitConfig struct {
|
|||||||
Files map[string][]byte
|
Files map[string][]byte
|
||||||
DriverOpts map[string]string
|
DriverOpts map[string]string
|
||||||
Auth authprovider.AuthConfigProvider
|
Auth authprovider.AuthConfigProvider
|
||||||
|
ImageVerifier ImageVerifier
|
||||||
Platforms []ocispecs.Platform
|
Platforms []ocispecs.Platform
|
||||||
ContextPathHash string
|
ContextPathHash string
|
||||||
DialMeta map[string][]string
|
DialMeta map[string][]string
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package policy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultPolicyFilename is the synthetic filename used for the embedded
|
// DefaultPolicyFilename is the synthetic filename used for the embedded
|
||||||
@@ -15,3 +18,28 @@ var defaultPolicyModule []byte
|
|||||||
func DefaultPolicyData() []byte {
|
func DefaultPolicyData() []byte {
|
||||||
return defaultPolicyModule
|
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
|
package docker
|
||||||
|
|
||||||
# Default policy embedded in Buildx. It verifies trust for images shipped
|
# 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
|
||||||
# - docker/dockerfile-upstream
|
# - docker/dockerfile-upstream
|
||||||
# - docker/buildkit-syft-scanner
|
# - docker/buildkit-syft-scanner
|
||||||
|
# - moby/buildkit
|
||||||
#
|
#
|
||||||
# Any image outside this managed set is allowed and passes through to user
|
# Any image outside this managed set is allowed and passes through to user
|
||||||
# policies unchanged. Access by digest is always allowed. For tag-based
|
# policies unchanged. Access by digest is always allowed. For tag-based
|
||||||
# access the rules below enforce a signed release from the expected GitHub
|
# access the rules below enforce a signed release from the expected GitHub
|
||||||
# source repository using the existing docker_github_builder_signature
|
# source repository using the existing docker_github_builder_signature
|
||||||
# helper from builtins.rego.
|
# 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 {
|
is_dockerfile if {
|
||||||
input.image
|
input.image
|
||||||
input.image.fullRepo == "docker.io/docker/dockerfile"
|
input.image.fullRepo == "docker.io/docker/dockerfile"
|
||||||
@@ -28,6 +36,11 @@ is_syft_scanner if {
|
|||||||
input.image.fullRepo == "docker.io/docker/buildkit-syft-scanner"
|
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 == "latest"
|
||||||
dockerfile_floating_tag(tag) if tag == "labs"
|
dockerfile_floating_tag(tag) if tag == "labs"
|
||||||
dockerfile_floating_tag(tag) if tag == "master"
|
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 syft_scanner_floating_tag(tag)
|
||||||
syft_scanner_tag_requires_sig(tag) if version_tag_ge(tag, 1, 10)
|
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 {
|
default_policy_deny_msgs contains msg if {
|
||||||
is_dockerfile
|
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])
|
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_sig_ok(tag) if {
|
||||||
dockerfile_floating_tag(tag)
|
dockerfile_floating_tag(tag)
|
||||||
some sig in input.image.signatures
|
some sig in input.image.signatures
|
||||||
|
|||||||
+148
-5
@@ -33,11 +33,7 @@ func makeDefaultPolicy(t *testing.T, sigInfo *policytypes.SignatureInfo) *Policy
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NewPolicy(Opt{
|
return DefaultPolicy(Opt{
|
||||||
Files: []File{{
|
|
||||||
Filename: DefaultPolicyFilename,
|
|
||||||
Data: DefaultPolicyData(),
|
|
||||||
}},
|
|
||||||
Log: func(level logrus.Level, msg string) {
|
Log: func(level logrus.Level, msg string) {
|
||||||
t.Logf("[%s] %s", level, msg)
|
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"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/buildx/driver"
|
"github.com/docker/buildx/driver"
|
||||||
|
"github.com/docker/buildx/driver/bkimage"
|
||||||
"github.com/moby/buildkit/identity"
|
"github.com/moby/buildkit/identity"
|
||||||
"github.com/moby/buildkit/util/testutil/integration"
|
"github.com/moby/buildkit/util/testutil/integration"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -25,6 +26,7 @@ var createTests = []func(t *testing.T, sb integration.Sandbox){
|
|||||||
testCreateRestartAlways,
|
testCreateRestartAlways,
|
||||||
testCreateRemoteContainer,
|
testCreateRemoteContainer,
|
||||||
testCreateWithProvenanceGHA,
|
testCreateWithProvenanceGHA,
|
||||||
|
testCreateCustomImageWithDefaultPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateMemoryLimit(t *testing.T, sb integration.Sandbox) {
|
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")
|
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) {
|
func testCreateWithProvenanceGHA(t *testing.T, sb integration.Sandbox) {
|
||||||
if !isDockerContainerWorker(sb) {
|
if !isDockerContainerWorker(sb) {
|
||||||
t.Skip("only testing with docker-container worker")
|
t.Skip("only testing with docker-container worker")
|
||||||
|
|||||||
Reference in New Issue
Block a user