Skip to content

Commit 50a327e

Browse files
ci: sign CI images with ddsign (#4034)
1 parent 7e61dc9 commit 50a327e

2 files changed

Lines changed: 72 additions & 10 deletions

File tree

.gitlab/generate-ci-images.php

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
* matrix value; the `image:` tag (with env vars resolved) is the published tag.
1212
* This script prints a literal preamble (stages, job templates), then loops
1313
* over the parsed compose services to emit, per Linux OS, one build matrix job
14-
* over PHP versions (bake builds and pushes the multi-arch image) plus a manual
15-
* publish matrix job that mirrors the tags to Docker Hub. Windows is emitted the
16-
* same way but single-arch (no manifest) with its own build runner/script.
14+
* over PHP versions (bake builds and pushes the multi-arch image, then ddsign
15+
* signs it) plus a manual publish matrix job that mirrors the tags to Docker
16+
* Hub. Windows is emitted the same way but single-arch (no manifest) with its
17+
* own build runner/script; its images are signed by a separate Linux job
18+
* (ddsign has no Windows binary), see .image_sign.
1719
*/
1820

1921
$root = dirname(__DIR__);
@@ -113,6 +115,11 @@ function parse_compose(string $path, array $env): array
113115
needs: []
114116
timeout: 4h
115117
image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/docker:29.4.0-noble
118+
# Requested so `ddsign sign` (run after the push, see script below) can
119+
# authenticate as this CI job. https://datadoghq.atlassian.net/wiki/spaces/SECENG/pages/2744681107
120+
id_tokens:
121+
DDSIGN_ID_TOKEN:
122+
aud: image-integrity
116123
variables:
117124
DDCI_CONFIGURE_OTEL_EXPORTER: "true"
118125
# Compile runs on the buildx "ci" builder instance, not this job pod, so the
@@ -138,6 +145,27 @@ function parse_compose(string $path, array $env): array
138145
IMG_SOURCES: "${CI_REGISTRY_IMAGE}:${TAG}"
139146
IMG_DESTINATIONS: "dd-trace-ci:${TAG}"
140147

148+
# Signs an already-pushed tag in registry.ddbuild.io. Used for the Windows
149+
# images: they're built without buildx (see .windows_image_build), so unlike
150+
# the Linux builds they can't sign via `ddsign --docker-metadata-file` right
151+
# after the push. Runs on Linux regardless of build platform: it only needs
152+
# network access to inspect the pushed manifest, not the build runner itself.
153+
.image_sign:
154+
stage: ci-publish
155+
rules:
156+
- when: manual
157+
allow_failure: true
158+
needs: []
159+
image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/docker:29.4.0-noble
160+
tags: ["arch:amd64"]
161+
id_tokens:
162+
DDSIGN_ID_TOKEN:
163+
aud: image-integrity
164+
# $TAG is supplied per matrix entry by the generated sign jobs.
165+
script:
166+
- DIGEST=$(docker buildx imagetools inspect "${CI_REGISTRY_IMAGE}:${TAG}" --format '{{.Manifest.Digest}}')
167+
- ddsign sign "${CI_REGISTRY_IMAGE}:${TAG}@${DIGEST}"
168+
141169
.windows_image_build:
142170
stage: ci-build
143171
rules:
@@ -250,6 +278,15 @@ function parse_compose(string $path, array $env): array
250278
<?php foreach ($winServices as $tag): ?>
251279
- "<?= $tag ?>"
252280
<?php endforeach; ?>
281+
282+
Windows sign:
283+
extends: .image_sign
284+
parallel:
285+
matrix:
286+
- TAG:
287+
<?php foreach ($winServices as $tag): ?>
288+
- "<?= $tag ?>"
289+
<?php endforeach; ?>
253290
<?php foreach ($osList as ['name' => $os, 'dir' => $dir, 'services' => $services]): ?>
254291
<?php /*
255292
One build job per PHP version. buildx bake reads the x-bake platforms from
@@ -262,13 +299,22 @@ function parse_compose(string $path, array $env): array
262299
tags: ["arch:amd64"]
263300
parallel:
264301
matrix:
265-
- PHP_VERSION:
266-
<?php foreach (array_keys($services) as $svc): ?>
267-
- <?= $svc, "\n" ?>
302+
<?php foreach ($services as $svc => $tag): ?>
303+
- PHP_VERSION: "<?= $svc ?>"
304+
TAG: "<?= $tag ?>"
268305
<?php endforeach; ?>
269306
script:
270307
- cd <?= $dir, "\n" ?>
271-
- docker buildx bake --no-cache --pull --push "${PHP_VERSION}"
308+
- METADATA_FILE=$(mktemp)
309+
- docker buildx bake --no-cache --pull --push --metadata-file "${METADATA_FILE}" "${PHP_VERSION}"
310+
# Unlike `docker buildx build`, `bake` nests its metadata under the target
311+
# name instead of writing it flat, so reslice it to what
312+
# `ddsign --docker-metadata-file` expects. Take the (only) top-level key
313+
# rather than looking up "${PHP_VERSION}" directly: bake sanitizes target
314+
# names for the metadata file (e.g. "7.0-alpine" -> "7_0-alpine"), so the
315+
# literal name doesn't always match.
316+
- jq 'keys[0] as $k | .[$k]' "${METADATA_FILE}" > "${METADATA_FILE}.flat"
317+
- ddsign sign "${CI_REGISTRY_IMAGE}:${TAG}" --docker-metadata-file "${METADATA_FILE}.flat"
272318
<?php /*
273319
Mirror to Docker Hub: one matrix job per OS, independent (needs: [] via
274320
.image_publish) so it can sync existing images without rebuilding.

dockerfiles/ci/README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ repo.
2626
build never needs a separate arm64 runner. The job pod only orchestrates;
2727
the actual compile happens on the builder, and `MAKE_JOBS` sets its
2828
parallelism.
29+
* **Signing:** every pushed image is signed with
30+
[`ddsign`](https://datadoghq.atlassian.net/wiki/spaces/SECENG/pages/2744681107/Image+Integrity+User+Guide),
31+
required for it to be pullable in Kubernetes clusters that enforce image
32+
signature verification. Linux images sign right after the `buildx bake`
33+
push, using the build's `--metadata-file`. Unlike plain `docker buildx
34+
build`, `bake` nests that file's contents under a (sanitized) target name
35+
instead of writing it flat, so it's resliced with `jq` first to the shape
36+
`ddsign --docker-metadata-file` expects. Windows images are built with plain
37+
`docker build` on a native Windows runner that has no `ddsign` binary
38+
(ddsign only ships for Linux/Mac), so they're signed by a separate `Windows
39+
sign` job that runs on Linux and looks up the pushed tag's digest with
40+
`docker buildx imagetools inspect` instead.
2941
* **Publish:** a `trigger` to the `DataDog/public-images` service mirrors the
3042
internal image to Docker Hub. It has no dependency on the build (see below).
3143

@@ -39,13 +51,17 @@ manually start the `ci-images` job (stage `ci-build`) to spawn the child
3951
pipeline. Per OS it has two kinds of jobs:
4052

4153
1. **`<OS> build: [<version>]`** (manual) — multi-arch build + push to
42-
`registry.ddbuild.io/ci/dd-trace-php/dd-trace-ci:<tag>`. Run the version(s)
43-
you need. Authentication to the internal registry is automatic via the
44-
runner's native credentials.
54+
`registry.ddbuild.io/ci/dd-trace-php/dd-trace-ci:<tag>`, then signs it with
55+
`ddsign`. Run the version(s) you need. Authentication to the internal
56+
registry is automatic via the runner's native credentials.
4557
2. **`<OS> publish`** (manual, a matrix with one instance per tag) — mirrors
4658
`…:<tag>` from the internal registry to the public Docker Hub
4759
(`datadog/dd-trace-ci`) via a downstream `public-images` pipeline.
4860

61+
Windows has an extra manual job, **`Windows sign`** (a matrix with one
62+
instance per tag), since `Windows build` can't sign its own images (see
63+
above). Run it after `Windows build`, before `Windows publish`.
64+
4965
### Publishing is independent of building
5066

5167
The `publish` jobs have **no dependencies**: they simply sync whatever currently

0 commit comments

Comments
 (0)