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>
155 lines
5.3 KiB
Go
155 lines
5.3 KiB
Go
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,
|
|
}
|
|
}
|