From eaee6675fb8ecb5719ec4549d91176ef35ea7cbe Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 17:49:06 -0800 Subject: [PATCH 1/9] policy: fix exposing signature kind and type Signed-off-by: Tonis Tiigi --- policy/signatures.go | 2 ++ policy/types.go | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/policy/signatures.go b/policy/signatures.go index 982e545da..5c87b2c2c 100644 --- a/policy/signatures.go +++ b/policy/signatures.go @@ -120,6 +120,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 diff --git a/policy/types.go b/policy/types.go index 5f0c95502..2c28d416f 100644 --- a/policy/types.go +++ b/policy/types.go @@ -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"` } From 20405112c25c8c13dd3985d8e8faed374cce7848 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 18:42:29 -0800 Subject: [PATCH 2/9] policy: allow image source without set platform Signed-off-by: Tonis Tiigi --- build/opt.go | 11 +++++++++++ commands/policy/eval.go | 1 + policy/validate.go | 3 +++ 3 files changed, 15 insertions(+) diff --git a/build/opt.go b/build/opt.go index a1c48a837..4d4a6b293 100644 --- a/build/opt.go +++ b/build/opt.go @@ -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" @@ -457,6 +458,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 +1235,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 +} diff --git a/commands/policy/eval.go b/commands/policy/eval.go index bdad0d08e..77a84d07e 100644 --- a/commands/policy/eval.go +++ b/commands/policy/eval.go @@ -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{ diff --git a/policy/validate.go b/policy/validate.go index 0c83da3cd..9bcf2e4f8 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -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 @@ -100,6 +101,8 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy } pl = platforms.Normalize(pl) platform = &pl + } else { + platform = p.opt.DefaultPlatform } inp, unknowns, err := SourceToInputWithLogger(ctx, p.opt.VerifierProvider, src, platform, p.opt.Log) From 73f5b1f9c797b7ac8700763c5a4fcdf4d8d7d38d Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 20:27:19 -0800 Subject: [PATCH 3/9] policy: update policy progress logic Fix the policy logger being open for the whole build. In new logic logger is opened on-demand if there are logs, remains open until timeout and is restarted if new logs come after. Signed-off-by: Tonis Tiigi --- build/opt.go | 107 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 16 deletions(-) diff --git a/build/opt.go b/build/opt.go index 4d4a6b293..1642304b2 100644 --- a/build/opt.go +++ b/build/opt.go @@ -67,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, } } @@ -97,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" } @@ -108,6 +135,7 @@ func (l *policyProgressLogger) Log(msg string) { Timestamp: time.Now(), }}, } + l.mu.Unlock() } func (l *policyProgressLogger) Write(p []byte) (int, error) { @@ -121,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) { From 30260ec935b91dfbdf8fad2cb8ae61fdbbdbaab3 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 20:49:43 -0800 Subject: [PATCH 4/9] policy: improve policy logging Signed-off-by: Tonis Tiigi --- policy/validate.go | 48 +++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/policy/validate.go b/policy/validate.go index 9bcf2e4f8..5383939d3 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -91,16 +91,11 @@ 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 } @@ -213,7 +208,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 { @@ -240,7 +235,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 } } @@ -292,14 +287,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, @@ -308,11 +303,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)) From 1d19f3e2dd9ea9b54067ac2f0050b13faec96b5e Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 21:24:27 -0800 Subject: [PATCH 5/9] policy: add docker_github_builder builtin helper Signed-off-by: Tonis Tiigi --- policy/builtin_module.go | 30 +++++++++++++++ policy/tester.go | 7 ++++ policy/validate.go | 1 + tests/policy_test.go | 80 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 policy/builtin_module.go diff --git a/policy/builtin_module.go b/policy/builtin_module.go new file mode 100644 index 000000000..c04364c53 --- /dev/null +++ b/policy/builtin_module.go @@ -0,0 +1,30 @@ +package policy + +import "github.com/open-policy-agent/opa/v1/ast" + +const builtinPolicyModuleFilename = "builtin/buildx_defaults.rego" + +const builtinPolicyModule = `package docker + +docker_github_builder(image, repo) if { + image.hasProvenance + some sig in image.signatures + valid_docker_github_builder_signature(sig, repo) +} + +valid_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 +} +` + +func builtinPolicyModuleAST() (*ast.Module, error) { + return ast.ParseModuleWithOpts(builtinPolicyModuleFilename, builtinPolicyModule, ast.ParserOptions{ + RegoVersion: ast.RegoV1, + }) +} diff --git a/policy/tester.go b/policy/tester.go index 90ed59b1d..17477f339 100644 --- a/policy/tester.go +++ b/policy/tester.go @@ -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), diff --git a/policy/validate.go b/policy/validate.go index 5383939d3..7f3b1b73a 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -201,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))) } diff --git a/tests/policy_test.go b/tests/policy_test.go index b477aefdb..4374d57ec 100644 --- a/tests/policy_test.go +++ b/tests/policy_test.go @@ -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,82 @@ 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 +} +`), 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") +} From 86885cd8a2ac1bf2d0a3a57127d0fe800fceb706 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 21:29:48 -0800 Subject: [PATCH 6/9] policy: add docker_github_builder_tag builtin helper Signed-off-by: Tonis Tiigi --- policy/builtin_module.go | 26 ++++++----------------- policy/builtins.rego | 23 ++++++++++++++++++++ tests/policy_test.go | 46 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 policy/builtins.rego diff --git a/policy/builtin_module.go b/policy/builtin_module.go index c04364c53..03513d053 100644 --- a/policy/builtin_module.go +++ b/policy/builtin_module.go @@ -1,27 +1,15 @@ package policy -import "github.com/open-policy-agent/opa/v1/ast" +import ( + _ "embed" + + "github.com/open-policy-agent/opa/v1/ast" +) const builtinPolicyModuleFilename = "builtin/buildx_defaults.rego" -const builtinPolicyModule = `package docker - -docker_github_builder(image, repo) if { - image.hasProvenance - some sig in image.signatures - valid_docker_github_builder_signature(sig, repo) -} - -valid_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 -} -` +//go:embed builtins.rego +var builtinPolicyModule string func builtinPolicyModuleAST() (*ast.Module, error) { return ast.ParseModuleWithOpts(builtinPolicyModuleFilename, builtinPolicyModule, ast.ParserOptions{ diff --git a/policy/builtins.rego b/policy/builtins.rego new file mode 100644 index 000000000..62462d061 --- /dev/null +++ b/policy/builtins.rego @@ -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 +} diff --git a/tests/policy_test.go b/tests/policy_test.go index 4374d57ec..7f51696c9 100644 --- a/tests/policy_test.go +++ b/tests/policy_test.go @@ -247,6 +247,50 @@ test_docker_github_builder_denied if { } 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), ) @@ -261,4 +305,6 @@ test_docker_github_builder_denied if { 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") } From db9c0c2532ed480710d2394909e376d973fc8244 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 22:01:31 -0800 Subject: [PATCH 7/9] policy: update unknown collection Signed-off-by: Tonis Tiigi --- policy/tester.go | 2 +- policy/validate.go | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/policy/tester.go b/policy/tester.go index 17477f339..b5f3bfaaa 100644 --- a/policy/tester.go +++ b/policy/tester.go @@ -600,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.") diff --git a/policy/validate.go b/policy/validate.go index 7f3b1b73a..a66b3a6ca 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -223,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, @@ -672,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 @@ -680,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) @@ -688,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 { From 535da79d626baa6c3f53e6b4ca9aafef8fa71365 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 15 Jan 2026 22:45:08 -0800 Subject: [PATCH 8/9] policy: build policy deny messages with error Supported from Buildkit v0.27.0-rc1 Signed-off-by: Tonis Tiigi --- cmd/buildx/main.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/buildx/main.go b/cmd/buildx/main.go index 7ebf42d82..772566a84 100644 --- a/cmd/buildx/main.go +++ b/cmd/buildx/main.go @@ -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()) From 89fdef17e668d0b94651cf97746a6f4042a91c0f Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 16 Jan 2026 12:12:41 -0800 Subject: [PATCH 9/9] policy: fixes for image source handling - Make sure tag is added to image reference as containerd reference parser refuses to parse otherwise. - When attestation is asked from non-index, return nil instead of error. This is for consistency as likely to fail in BuildKit before that is fixed separately. Signed-off-by: Tonis Tiigi --- commands/policy/eval.go | 1 + policy/signatures.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/commands/policy/eval.go b/commands/policy/eval.go index 77a84d07e..2d516215c 100644 --- a/commands/policy/eval.go +++ b/commands/policy/eval.go @@ -388,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://") { diff --git a/policy/signatures.go b/policy/signatures.go index 5c87b2c2c..daa0178d2 100644 --- a/policy/signatures.go +++ b/policy/signatures.go @@ -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)