Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions build/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,30 @@ var sendGitQueryAsInput = sync.OnceValue(func() bool {
return false
})

// defaultPolicyEnabled reports whether the builtin default source policy is
// enabled via the BUILDX_DEFAULT_POLICY environment variable. It is opt-in
// for now; a future release may flip the default to on.
var defaultPolicyEnabled = sync.OnceValue(func() bool {
if v, ok := os.LookupEnv("BUILDX_DEFAULT_POLICY"); ok {
if vv, err := strconv.ParseBool(v); err == nil {
return vv
}
}
return false
})

// policyExplicitlyDisabled reports whether the user passed `--policy
// disabled=true`, which suppresses both user-defined and builtin default
// policies.
func policyExplicitlyDisabled(configs []buildflags.PolicyConfig) bool {
for _, cfg := range configs {
if cfg.Disabled {
return true
}
}
return false
}

type policyProgressLogger struct {
ch chan *client.SolveStatus
done chan struct{}
Expand Down Expand Up @@ -624,6 +648,22 @@ func configureSourcePolicy(ctx context.Context, np *noderesolver.ResolvedNode, o
if err != nil {
return nil, err
}

// Prepend the builtin default policy when enabled and not explicitly
// disabled. The default policy verifies trust for Docker-managed images
// (docker/dockerfile, docker/dockerfile-upstream) that may be implicitly
// loaded during a build, and passes through any other source so user
// policies retain full control.
if defaultPolicyEnabled() && !policyExplicitlyDisabled(opt.Policy) {
builtin := policyOpt{
Files: []policyFileSpec{{
Filename: policy.DefaultPolicyFilename,
Data: policy.DefaultPolicyData(),
}},
}
popts = append([]policyOpt{builtin}, popts...)
}

if len(popts) == 0 {
so.SourcePolicyProvider = nil
return nil, nil
Expand Down
17 changes: 17 additions & 0 deletions policy/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package policy

import (
_ "embed"
)

// DefaultPolicyFilename is the synthetic filename used for the embedded
// default policy when it is loaded as a regular policy file.
const DefaultPolicyFilename = "buildx_default_policy.rego"

//go:embed default.rego
var defaultPolicyModule []byte

// DefaultPolicyData returns the embedded default policy module bytes.
func DefaultPolicyData() []byte {
return defaultPolicyModule
}
162 changes: 162 additions & 0 deletions policy/default.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package docker

# Default policy embedded in Buildx. It verifies trust for images shipped
# by Docker that may be implicitly loaded during a build:
#
# - docker/dockerfile
# - docker/dockerfile-upstream
# - docker/buildkit-syft-scanner
#
# Any image outside this managed set is allowed and passes through to user
# policies unchanged. Access by digest is always allowed. For tag-based
# access the rules below enforce a signed release from the expected GitHub
# source repository using the existing docker_github_builder_signature
# helper from builtins.rego.

is_dockerfile if {
input.image
input.image.fullRepo == "docker.io/docker/dockerfile"
}

is_dockerfile if {
input.image
input.image.fullRepo == "docker.io/docker/dockerfile-upstream"
}

is_syft_scanner if {
input.image
input.image.fullRepo == "docker.io/docker/buildkit-syft-scanner"
}

dockerfile_floating_tag(tag) if tag == "latest"
dockerfile_floating_tag(tag) if tag == "labs"
dockerfile_floating_tag(tag) if tag == "master"

dockerfile_tag_requires_sig(tag) if dockerfile_floating_tag(tag)
dockerfile_tag_requires_sig(tag) if version_tag_ge(tag, 1, 21)

syft_scanner_floating_tag(tag) if tag == "latest"

syft_scanner_tag_requires_sig(tag) if syft_scanner_floating_tag(tag)
syft_scanner_tag_requires_sig(tag) if version_tag_ge(tag, 1, 10)


default_policy_deny_msgs contains msg if {
is_dockerfile
tag := input.image.tag
tag != ""
dockerfile_tag_requires_sig(tag)
not dockerfile_sig_ok(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_syft_scanner
tag := input.image.tag
tag != ""
syft_scanner_tag_requires_sig(tag)
not syft_scanner_sig_ok(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])
}

dockerfile_sig_ok(tag) if {
dockerfile_floating_tag(tag)
some sig in input.image.signatures
docker_github_builder_signature(sig, "moby/buildkit")
}

dockerfile_sig_ok(tag) if {
not dockerfile_floating_tag(tag)
some sig in input.image.signatures
docker_github_builder_signature(sig, "moby/buildkit")
dockerfile_sig_ref_matches(sig, tag)
}

syft_scanner_sig_ok(tag) if {
syft_scanner_floating_tag(tag)
some sig in input.image.signatures
docker_github_builder_signature(sig, "docker/buildkit-syft-scanner")
}

syft_scanner_sig_ok(tag) if {
not syft_scanner_floating_tag(tag)
some sig in input.image.signatures
docker_github_builder_signature(sig, "docker/buildkit-syft-scanner")
syft_scanner_sig_ref_matches(sig, tag)
}


decision := {
"allow": count(default_policy_deny_msgs) == 0,
"deny_msg": [msg | some msg in default_policy_deny_msgs],
}

# ---- helpers ----

# parse_version returns [major, minor] when tag matches a version pattern
# like "1", "1.21", "1.21.0", "1.21.0-labs". For a major-only tag such as
# "1", the minor component is treated as effectively unbounded so floating
# major tags are handled like the newest release in that major line.
parse_version(tag) := [maj, min] if {
m := regex.find_all_string_submatch_n(`^(\d+)\.(\d+)(?:\.\d+)?(?:-labs)?$`, tag, 1)
count(m) == 1
maj := to_number(m[0][1])
min := to_number(m[0][2])
}

parse_version(tag) := [maj, 999999] if {
m := regex.find_all_string_submatch_n(`^(\d+)(?:-labs)?$`, tag, 1)
count(m) == 1
maj := to_number(m[0][1])
}

version_tag_ge(tag, target_major, _) if {
v := parse_version(tag)
v[0] > target_major
}

version_tag_ge(tag, target_major, target_minor) if {
v := parse_version(tag)
v[0] == target_major
v[1] >= target_minor
}

dockerfile_sig_ref_matches(sig, tag) if {
sig_ref_matches(sig.signer.sourceRepositoryRef, tag, "refs/tags/dockerfile/")
}

syft_scanner_sig_ref_matches(sig, tag) if {
ref := trim_prefix(sig.signer.sourceRepositoryRef, "refs/tags/")
ref != sig.signer.sourceRepositoryRef
version_tag_selector_matches(tag, ref)
}

sig_ref_matches(ref, tag, prefix) if {
stripped_ref := trim_prefix(ref, prefix)
stripped_ref != ref
tag_labs := endswith(tag, "-labs")
ref_labs := endswith(stripped_ref, "-labs")
tag_labs == ref_labs
version_tag_selector_matches(
trim_suffix(tag, "-labs"),
trim_suffix(stripped_ref, "-labs"),
)
}

version_tag_selector_matches(selector, candidate) if {
selector == candidate
}

version_tag_selector_matches(selector, candidate) if {
m := regex.find_all_string_submatch_n(`^(\d+)\.(\d+)$`, selector, 1)
count(m) == 1
parse_version(selector) == parse_version(candidate)
}

version_tag_selector_matches(selector, candidate) if {
m := regex.find_all_string_submatch_n(`^(\d+)$`, selector, 1)
count(m) == 1
sel := parse_version(selector)
cand := parse_version(candidate)
sel[0] == cand[0]
}
Loading
Loading