Skip to content

Commit a4b77ab

Browse files
authored
fix(wfctl): use docker buildx build in hardened mode (#425)
The classic docker build driver rejects --provenance and --sbom flags with "Attestation is not supported for the docker driver." Only buildx with the docker-container driver supports supply-chain attestation. When hardened=true, prepend "buildx" to the docker args so the command becomes "docker buildx build ...". Non-hardened builds keep plain "docker build" for backward compat. Also adds a driver readiness check (docker buildx inspect --bootstrap) before the live hardened build with an actionable error message guiding users to run "docker buildx create --use" or add setup-buildx-action@v3.
1 parent b189565 commit a4b77ab

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

cmd/wfctl/build_image.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,13 @@ func buildWithDockerfile(ctr config.CIContainerTarget, tag string, dryRun bool,
9999
}
100100

101101
imageRef := imageRefForContainer(ctr, tag, registries)
102-
args := []string{"build", "--file", dockerfile, "--tag", imageRef}
102+
// hardened mode uses buildx for provenance/SBOM attestation support.
103+
var args []string
104+
if hardened {
105+
args = []string{"buildx", "build", "--file", dockerfile, "--tag", imageRef}
106+
} else {
107+
args = []string{"build", "--file", dockerfile, "--tag", imageRef}
108+
}
103109

104110
// Platforms (BuildKit multi-arch).
105111
if len(ctr.Platforms) > 0 {
@@ -157,6 +163,15 @@ func buildWithDockerfile(ctr config.CIContainerTarget, tag string, dryRun bool,
157163
return nil
158164
}
159165

166+
if hardened {
167+
// buildx with the docker-container driver is required for attestation flags.
168+
// Verify a non-default builder is active; the default "docker" driver rejects --provenance.
169+
if err := exec.Command("docker", "buildx", "inspect", "--bootstrap").Run(); err != nil {
170+
return fmt.Errorf("hardened build requires docker buildx: run 'docker buildx create --use' " +
171+
"or add 'docker/setup-buildx-action@v3' to your CI workflow (%w)", err)
172+
}
173+
}
174+
160175
//nolint:gosec // G204: docker command constructed from validated config fields
161176
cmd := exec.Command("docker", args...)
162177
cmd.Stdout = out

cmd/wfctl/build_image_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,68 @@ func TestRunBuildImage_NotHardenedNoProvenanceArgs(t *testing.T) {
156156
}
157157
}
158158

159+
// TestRunBuildImage_HardenedUsesBuildx verifies hardened=true produces "docker buildx build".
160+
func TestRunBuildImage_HardenedUsesBuildx(t *testing.T) {
161+
dir := t.TempDir()
162+
cfg := `ci:
163+
build:
164+
security:
165+
hardened: true
166+
containers:
167+
- name: app
168+
method: dockerfile
169+
dockerfile: Dockerfile
170+
`
171+
cfgPath := filepath.Join(dir, "ci.yaml")
172+
if err := os.WriteFile(cfgPath, []byte(cfg), 0o600); err != nil {
173+
t.Fatal(err)
174+
}
175+
t.Setenv("WFCTL_BUILD_DRY_RUN", "1")
176+
t.Setenv("DOCKER_BUILDKIT", "1")
177+
178+
var buf bytes.Buffer
179+
if err := runBuildImageWithOutput([]string{"--config", cfgPath}, &buf); err != nil {
180+
t.Fatalf("hardened buildx dry-run: %v", err)
181+
}
182+
183+
out := buf.String()
184+
if !strings.Contains(out, "docker buildx build") {
185+
t.Errorf("expected 'docker buildx build' in hardened dry-run output, got: %q", out)
186+
}
187+
}
188+
189+
// TestRunBuildImage_NonHardenedUsesPlainDocker verifies hardened=false produces "docker build" (no buildx).
190+
func TestRunBuildImage_NonHardenedUsesPlainDocker(t *testing.T) {
191+
dir := t.TempDir()
192+
cfg := `ci:
193+
build:
194+
security:
195+
hardened: false
196+
containers:
197+
- name: app
198+
method: dockerfile
199+
dockerfile: Dockerfile
200+
`
201+
cfgPath := filepath.Join(dir, "ci.yaml")
202+
if err := os.WriteFile(cfgPath, []byte(cfg), 0o600); err != nil {
203+
t.Fatal(err)
204+
}
205+
t.Setenv("WFCTL_BUILD_DRY_RUN", "1")
206+
207+
var buf bytes.Buffer
208+
if err := runBuildImageWithOutput([]string{"--config", cfgPath}, &buf); err != nil {
209+
t.Fatalf("non-hardened dry-run: %v", err)
210+
}
211+
212+
out := buf.String()
213+
if strings.Contains(out, "buildx") {
214+
t.Errorf("expected no 'buildx' in non-hardened dry-run output, got: %q", out)
215+
}
216+
if !strings.Contains(out, "docker build") {
217+
t.Errorf("expected 'docker build' in non-hardened dry-run output, got: %q", out)
218+
}
219+
}
220+
159221
// TestImageRefForContainer_RegistryNameResolvesToPath verifies that imageRefForContainer
160222
// resolves a registry name to its path via ci.registries.
161223
func TestImageRefForContainer_RegistryNameResolvesToPath(t *testing.T) {

0 commit comments

Comments
 (0)