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>
241 lines
7.5 KiB
Rego
241 lines
7.5 KiB
Rego
package docker
|
|
|
|
# Default policy embedded in Buildx. It verifies trust for images shipped
|
|
# by Docker that may be implicitly loaded during a build or used to run a
|
|
# build:
|
|
#
|
|
# - docker/dockerfile
|
|
# - docker/dockerfile-upstream
|
|
# - docker/buildkit-syft-scanner
|
|
# - moby/buildkit
|
|
#
|
|
# 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.
|
|
#
|
|
# 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 {
|
|
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"
|
|
}
|
|
|
|
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 == "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)
|
|
|
|
# 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 {
|
|
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])
|
|
}
|
|
|
|
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_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]
|
|
}
|