Merge pull request #3611 from tonistiigi/policy-update-v0.31-rc2

Updates and fixes to policy support
This commit is contained in:
Tõnis Tiigi
2026-01-16 12:52:06 -08:00
committed by GitHub
10 changed files with 391 additions and 36 deletions
+102 -16
View File
@@ -48,6 +48,7 @@ import (
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/gitutil"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tonistiigi/fsutil"
@@ -66,29 +67,28 @@ type policyProgressLogger struct {
ch chan *client.SolveStatus
done chan struct{}
dgst digest.Digest
started time.Time
name string
started time.Time
mu sync.Mutex
timer *time.Timer
window int
open bool
closed bool
}
const policyProgressWindow = 500 * time.Millisecond
func newPolicyProgressLogger(pw progress.Writer, name string) *policyProgressLogger {
if pw == nil {
return nil
}
ch, done := progress.NewChannel(pw)
dgst := digest.FromBytes([]byte(identity.NewID()))
tm := time.Now()
vtx := client.Vertex{
Digest: dgst,
Name: name,
Started: &tm,
}
ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
return &policyProgressLogger{
ch: ch,
done: done,
dgst: dgst,
started: tm,
name: name,
ch: ch,
done: done,
dgst: dgst,
name: name,
}
}
@@ -96,6 +96,34 @@ func (l *policyProgressLogger) Log(msg string) {
if l == nil || msg == "" {
return
}
needStart := false
var started time.Time
var window int
l.mu.Lock()
if l.closed {
l.mu.Unlock()
return
}
if !l.open {
needStart = true
l.open = true
l.window++
window = l.window
started = time.Now()
l.started = started
} else {
window = l.window
}
if l.timer != nil {
l.timer.Stop()
}
l.timer = time.AfterFunc(policyProgressWindow, func() {
l.completeWindow(window, nil)
})
if needStart {
l.sendVertexStart(started)
}
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
@@ -107,6 +135,7 @@ func (l *policyProgressLogger) Log(msg string) {
Timestamp: time.Now(),
}},
}
l.mu.Unlock()
}
func (l *policyProgressLogger) Write(p []byte) (int, error) {
@@ -120,19 +149,66 @@ func (l *policyProgressLogger) Close(err error) {
if l == nil {
return
}
shouldComplete := false
var started time.Time
l.mu.Lock()
if l.closed {
l.mu.Unlock()
return
}
l.closed = true
if l.open {
shouldComplete = true
started = l.started
l.open = false
}
l.window++
if l.timer != nil {
l.timer.Stop()
l.timer = nil
}
if shouldComplete {
l.sendVertexComplete(started, err)
}
l.mu.Unlock()
close(l.ch)
<-l.done
}
func (l *policyProgressLogger) completeWindow(window int, err error) {
l.mu.Lock()
if l.closed || !l.open || window != l.window {
l.mu.Unlock()
return
}
started := l.started
l.open = false
l.sendVertexComplete(started, err)
l.mu.Unlock()
}
func (l *policyProgressLogger) sendVertexStart(started time.Time) {
vtx := client.Vertex{
Digest: l.dgst,
Name: l.name,
Started: &started,
}
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
}
func (l *policyProgressLogger) sendVertexComplete(started time.Time, err error) {
tm := time.Now()
vtx := client.Vertex{
Digest: l.dgst,
Name: l.name,
Started: &l.started,
Started: &started,
Completed: &tm,
}
if err != nil {
vtx.Error = err.Error()
}
l.ch <- &client.SolveStatus{Vertexes: []*client.Vertex{&vtx}}
close(l.ch)
<-l.done
}
func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, cfg *confutil.Config, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) {
@@ -457,6 +533,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O
Log: logf,
FS: opt.Inputs.policy.FS,
VerifierProvider: policy.SignatureVerifier(cfg),
DefaultPlatform: defaultPlatform(bopts),
})
cbs = append(cbs, p.CheckPolicy)
if popt.Strict {
@@ -1233,3 +1310,12 @@ func parseOCILayoutPath(s string) (localPath, dgst, tag string) {
}
return
}
func defaultPlatform(bopts gateway.BuildOpts) *ocispecs.Platform {
pl := bopts.Workers[0].Platforms
if len(pl) == 0 {
return nil
}
p := platforms.Normalize(pl[0])
return &p
}
+6
View File
@@ -16,6 +16,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug"
solvererrdefs "github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/sourcepolicy/policysession"
"github.com/moby/buildkit/util/grpcerrors"
"github.com/moby/buildkit/util/stack"
"github.com/pkg/errors"
@@ -108,6 +109,11 @@ func main() {
if errors.As(err, &exitCodeErr) {
os.Exit(int(exitCodeErr))
}
for _, msg := range policysession.DenyMessages(err) {
if msg.GetMessage() != "" {
fmt.Fprintf(os.Stderr, "Policy: %s\n", msg.GetMessage())
}
}
for _, s := range solvererrdefs.Sources(err) {
s.Print(cmd.Err())
+2
View File
@@ -216,6 +216,7 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
Env: env,
FS: fsProvider,
VerifierProvider: verifier,
DefaultPlatform: &p,
})
srcReq := &gwpb.ResolveSourceMetaResponse{
@@ -387,6 +388,7 @@ func parseSource(input string) (*pb.SourceOp, error) {
if err != nil {
return nil, errors.Wrapf(err, "failed to parse image source reference")
}
ref = reference.TagNameOnly(ref)
return &pb.SourceOp{Identifier: "docker-image://" + ref.String()}, nil
}
if strings.HasPrefix(input, "git://") {
+18
View File
@@ -0,0 +1,18 @@
package policy
import (
_ "embed"
"github.com/open-policy-agent/opa/v1/ast"
)
const builtinPolicyModuleFilename = "builtin/buildx_defaults.rego"
//go:embed builtins.rego
var builtinPolicyModule string
func builtinPolicyModuleAST() (*ast.Module, error) {
return ast.ParseModuleWithOpts(builtinPolicyModuleFilename, builtinPolicyModule, ast.ParserOptions{
RegoVersion: ast.RegoV1,
})
}
+23
View File
@@ -0,0 +1,23 @@
package docker
docker_github_builder(image, repo) if {
image.hasProvenance
some sig in image.signatures
docker_github_builder_signature(sig, repo)
}
docker_github_builder_tag(image, repo, tag) if {
docker_github_builder(image, repo)
some sig in image.signatures
sig.signer.sourceRepositoryRef == sprintf("refs/tags/%s", [tag])
}
docker_github_builder_signature(sig, repo) if {
sig.kind == "docker-github-builder"
sig.type == "bundle-v0.3"
sig.signer.certificateIssuer == "CN=sigstore-intermediate,O=sigstore.dev"
sig.signer.issuer == "https://token.actions.githubusercontent.com"
sig.signer.sourceRepositoryURI == sprintf("https://github.com/%s", [repo])
sig.signer.runnerEnvironment == "github-hosted"
count(sig.timestamps) > 0
}
+6
View File
@@ -89,6 +89,10 @@ func parseSignatures(ctx context.Context, getVerifier PolicyVerifierProvider, ac
}
desc := toOCIDescriptor(rootBlob.Descriptor_)
if desc.MediaType != ocispecs.MediaTypeImageIndex {
return nil, nil
}
sc, err := policyimage.ResolveSignatureChain(ctx, acp, desc, platform)
if err != nil {
return nil, errors.Wrapf(err, "resolving signature chain for image %s", desc.Digest)
@@ -120,6 +124,8 @@ func parseSignatures(ctx context.Context, getVerifier PolicyVerifierProvider, ac
Timestamps: siRaw.Timestamps,
IsDHI: siRaw.IsDHI,
DockerReference: siRaw.DockerReference,
SignatureType: toSignatureType(siRaw.SignatureType),
SignatureKind: toSignatureKind(siRaw.Kind),
}
// TODO: signature type after upstream update
+8 -1
View File
@@ -195,6 +195,13 @@ func loadPolicyModules(root fs.StatFS, filename string) (map[string]*ast.Module,
modules := map[string]*ast.Module{
filepath.ToSlash(policyFile): mod,
}
builtinMod, err := builtinPolicyModuleAST()
if err != nil {
return nil, nil, errors.Wrapf(err, "parse builtin policy module %s", builtinPolicyModuleFilename)
}
if _, ok := modules[builtinPolicyModuleFilename]; !ok {
modules[builtinPolicyModuleFilename] = builtinMod
}
files := []File{
{
Filename: filepath.ToSlash(policyFile),
@@ -593,7 +600,7 @@ func missingInputRefs(mods []*ast.Module, input *Input) []string {
return nil
}
inputMap := normalizeInput(input)
refs := collectUnknowns(mods)
refs := collectUnknowns(mods, nil)
missing := make([]string, 0, len(refs))
for _, ref := range refs {
key := strings.TrimPrefix(ref, "input.")
+40 -3
View File
@@ -132,7 +132,8 @@ type Image struct {
}
type AttestationSignature struct {
SignatureType SignatureType `json:"signatureType,omitempty"`
SignatureKind SignatureKind `json:"kind,omitempty"`
SignatureType SignatureType `json:"type,omitempty"`
Timestamps []policytypes.TimestampVerificationResult `json:"timestamps,omitempty"`
DockerReference string `json:"dockerReference,omitempty"`
IsDHI bool `json:"isDHI,omitempty"`
@@ -171,10 +172,46 @@ type SignerInfo struct {
type SignatureType string
const (
SignatureTypeBundle SignatureType = "bundle-v0.3"
SignatureTypeHashedRecord SignatureType = "hashedreckord"
SignatureTypeBundleV03 SignatureType = "bundle-v0.3"
SignatureTypeSimpleSigningV1 SignatureType = "simplesigning-v1"
)
func toSignatureType(st policytypes.SignatureType) SignatureType {
switch st {
case policytypes.SignatureBundleV03:
return SignatureTypeBundleV03
case policytypes.SignatureSimpleSigningV1:
return SignatureTypeSimpleSigningV1
}
return ""
}
type SignatureKind string
const (
SignatureKindDockerGithubBuilder SignatureKind = "docker-github-builder"
SignatureKindDockerHardenedImage SignatureKind = "docker-hardened-image"
SignatureKindSelfSignedGithubRepo SignatureKind = "self-signed-github-repo"
SignatureKindSelfSigned SignatureKind = "self-signed"
SignatureKindUntrusted SignatureKind = "untrusted"
)
func toSignatureKind(k policytypes.Kind) SignatureKind {
switch k {
case policytypes.KindDockerGithubBuilder:
return SignatureKindDockerGithubBuilder
case policytypes.KindDockerHardenedImage:
return SignatureKindDockerHardenedImage
case policytypes.KindSelfSignedGithubRepo:
return SignatureKindSelfSignedGithubRepo
case policytypes.KindSelfSigned:
return SignatureKindSelfSigned
case policytypes.KindUntrusted:
return SignatureKindUntrusted
}
return ""
}
type Local struct {
Name string `json:"name,omitempty"`
}
+60 -16
View File
@@ -59,6 +59,7 @@ type Opt struct {
Log func(logrus.Level, string)
FS func() (fs.StatFS, func() error, error)
VerifierProvider PolicyVerifierProvider
DefaultPlatform *ocispecs.Platform
}
var _ policysession.PolicyCallback = (&Policy{}).CheckPolicy
@@ -90,16 +91,13 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
src := req.Source
var platform *ocispecs.Platform
if req.Platform != nil {
platformStr := req.Platform.OS + "/" + req.Platform.Architecture
if req.Platform.Variant != "" {
platformStr += "/" + req.Platform.Variant
}
pl, err := platforms.Parse(platformStr)
pl, err := platformFromReq(req)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse platform")
return nil, nil, err
}
pl = platforms.Normalize(pl)
platform = &pl
platform = pl
} else {
platform = p.opt.DefaultPlatform
}
inp, unknowns, err := SourceToInputWithLogger(ctx, p.opt.VerifierProvider, src, platform, p.opt.Log)
@@ -203,6 +201,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
opts = append(opts, f.impl(st))
}
opts = append(opts, rego.Module(builtinPolicyModuleFilename, builtinPolicyModule))
for _, file := range p.opt.Files {
opts = append(opts, rego.Module(file.Filename, string(file.Data)))
}
@@ -210,7 +209,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to marshal policy input")
}
p.log(logrus.InfoLevel, "checking policy for source %s", src.Source.Identifier)
p.log(logrus.InfoLevel, "checking policy for source %s", sourceName(req))
p.log(logrus.DebugLevel, "policy input: %s", dt)
if len(unknowns) > 0 {
@@ -224,10 +223,11 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if err != nil {
return nil, nil, err
}
unk := collectUnknowns(pq.Support)
unk := collectUnknowns(pq.Support, unknowns)
if _, ok := st.Unknowns[funcVerifyGitSignature]; ok {
unk = append(unk, "input.git.commit")
}
if len(unk) > 0 {
next := &gwpb.ResolveSourceMetaRequest{
Source: req.Source.Source,
@@ -237,7 +237,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
return nil, nil, err
}
if next.Image != nil || next.Git != nil || hasHTTPUnknowns(unk) {
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", src.Source.Identifier, summarizeUnknownsForLog(unk))
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", sourceName(req), summarizeUnknownsForLog(unk))
return nil, next, nil
}
}
@@ -289,14 +289,14 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if resp.Action == moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW {
if len(st.ImagePins) > 1 {
return nil, nil, errors.Errorf("multiple image pins set to %s: %v", src.Source.Identifier, st.ImagePins)
return nil, nil, errors.Errorf("multiple image pins set to %s: %v", sourceName(req), st.ImagePins)
}
if len(st.ImagePins) == 1 {
newSrc, err := addPinToImage(src.Source, slices.Collect(maps.Keys(st.ImagePins))[0])
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to add image pin to source")
}
p.log(logrus.InfoLevel, "policy decision for source %s: convert to %s", src.Source.Identifier, newSrc.Identifier)
p.log(logrus.InfoLevel, "policy decision for source %s: convert to %s", sourceName(req), newSrc.Identifier)
return &policysession.DecisionResponse{
Action: moby_buildkit_v1_sourcepolicy.PolicyAction_CONVERT,
@@ -305,11 +305,38 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
}
}
p.log(logrus.InfoLevel, "policy decision for source %s: %s %v", src.Source.Identifier, resp.Action, resp.DenyMessages)
p.log(logrus.InfoLevel, "policy decision for source %s: %s", sourceName(req), resp.Action)
for _, dm := range resp.DenyMessages {
p.log(logrus.InfoLevel, " - %s", dm.Message)
}
return resp, nil, nil
}
func platformFromReq(req *policysession.CheckPolicyRequest) (*ocispecs.Platform, error) {
if req.Platform != nil {
platformStr := req.Platform.OS + "/" + req.Platform.Architecture
if req.Platform.Variant != "" {
platformStr += "/" + req.Platform.Variant
}
pl, err := platforms.Parse(platformStr)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse platform")
}
pl = platforms.Normalize(pl)
return &pl, nil
}
return nil, nil
}
func sourceName(req *policysession.CheckPolicyRequest) string {
name := req.Source.Source.Identifier
if p, _ := platformFromReq(req); p != nil {
name += " (" + platforms.Format(*p) + ")"
}
return name
}
func (p *Policy) Print(ctx print.Context, msg string) error {
if p.opt.Log != nil {
p.opt.Log(logrus.InfoLevel, ctx.Location.Format("%s", msg))
@@ -646,7 +673,7 @@ func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSou
return nil
}
func collectUnknowns(mods []*ast.Module) []string {
func collectUnknowns(mods []*ast.Module, allowed []string) []string {
seen := map[string]struct{}{}
var out []string
@@ -654,6 +681,7 @@ func collectUnknowns(mods []*ast.Module) []string {
ast.WalkRefs(mod, func(ref ast.Ref) bool {
if ref.HasPrefix(ast.InputRootRef) {
s := ref.String() // e.g. "input.request.path"
s = "input." + trimKey(strings.TrimPrefix(s, "input."))
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
@@ -662,7 +690,23 @@ func collectUnknowns(mods []*ast.Module) []string {
return true
})
}
return out
if allowed == nil {
return out
}
valid := map[string]struct{}{}
for _, k := range allowed {
valid[k] = struct{}{}
}
filtered := make([]string, 0, len(out))
for _, k := range out {
if _, ok := valid[k]; ok {
filtered = append(filtered, k)
}
}
return filtered
}
func summarizeUnknownsForLog(unk []string) []string {
+126
View File
@@ -12,6 +12,7 @@ var policyTestTests = []func(t *testing.T, sb integration.Sandbox){
testPolicyTestRunFilter,
testPolicyTestFailMissingInput,
testPolicyTestNestedPath,
testPolicyTestDockerGitHubBuilder,
}
func testPolicyTestRunFilter(t *testing.T, sb integration.Sandbox) {
@@ -182,3 +183,128 @@ test_allowlisted_repo if {
require.NoError(t, err, string(out))
require.Contains(t, string(out), "test_allowlisted_repo: PASS")
}
func testPolicyTestDockerGitHubBuilder(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
dir := tmpdir(
t,
fstest.CreateFile("policy.rego", []byte(`
package docker
default allow = false
allow if docker_github_builder(input.image, "org/repo")
decision := {"allow": allow}
`), 0600),
fstest.CreateFile("policy_test.rego", []byte(`
package docker
test_docker_github_builder if {
result := data.docker.decision with input as {
"image": {
"hasProvenance": true,
"signatures": [{
"kind": "docker-github-builder",
"type": "bundle-v0.3",
"signer": {
"certificateIssuer": "CN=sigstore-intermediate,O=sigstore.dev",
"issuer": "https://token.actions.githubusercontent.com",
"sourceRepositoryURI": "https://github.com/org/repo",
"runnerEnvironment": "github-hosted"
},
"timestamps": [{
"type": "tlog",
"uri": "https://example.com/tlog",
"timestamp": "2024-01-01T00:00:00Z"
}]
}]
}
}
result.allow
}
test_docker_github_builder_denied if {
result := data.docker.decision with input as {
"image": {
"hasProvenance": true,
"signatures": [{
"kind": "docker-github-builder",
"type": "bundle-v0.3",
"signer": {
"certificateIssuer": "CN=sigstore-intermediate,O=sigstore.dev",
"issuer": "https://token.actions.githubusercontent.com",
"sourceRepositoryURI": "https://github.com/other/repo",
"runnerEnvironment": "github-hosted"
},
"timestamps": [{
"type": "tlog",
"uri": "https://example.com/tlog",
"timestamp": "2024-01-01T00:00:00Z"
}]
}]
}
}
not result.allow
}
test_docker_github_builder_tag if {
docker_github_builder_tag({
"hasProvenance": true,
"signatures": [{
"kind": "docker-github-builder",
"type": "bundle-v0.3",
"signer": {
"certificateIssuer": "CN=sigstore-intermediate,O=sigstore.dev",
"issuer": "https://token.actions.githubusercontent.com",
"sourceRepositoryURI": "https://github.com/org/repo",
"sourceRepositoryRef": "refs/tags/v1.2.3",
"runnerEnvironment": "github-hosted"
},
"timestamps": [{
"type": "tlog",
"uri": "https://example.com/tlog",
"timestamp": "2024-01-01T00:00:00Z"
}]
}]
}, "org/repo", "v1.2.3")
}
test_docker_github_builder_tag_denied if {
not docker_github_builder_tag({
"hasProvenance": true,
"signatures": [{
"kind": "docker-github-builder",
"type": "bundle-v0.3",
"signer": {
"certificateIssuer": "CN=sigstore-intermediate,O=sigstore.dev",
"issuer": "https://token.actions.githubusercontent.com",
"sourceRepositoryURI": "https://github.com/org/repo",
"sourceRepositoryRef": "refs/tags/other",
"runnerEnvironment": "github-hosted"
},
"timestamps": [{
"type": "tlog",
"uri": "https://example.com/tlog",
"timestamp": "2024-01-01T00:00:00Z"
}]
}]
}, "org/repo", "v1.2.3")
}
`), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs(
"policy",
"test",
"--filename",
"policy",
".",
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "test_docker_github_builder: PASS")
require.Contains(t, string(out), "test_docker_github_builder_denied: PASS")
require.Contains(t, string(out), "test_docker_github_builder_tag: PASS")
require.Contains(t, string(out), "test_docker_github_builder_tag_denied: PASS")
}