-
Notifications
You must be signed in to change notification settings - Fork 0
383 lines (357 loc) · 15.1 KB
/
Copy pathdocker.yaml
File metadata and controls
383 lines (357 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# Reusable Docker build/release workflow for Yandex Container Registry.
#
# Jobs:
# lint (hadolint)
# └─ scan (build amd64, Trivy, smoke test) — gate; nothing is pushed
# └─ push (build multi-arch, push, SBOM + SLSA provenance, cosign sign)
#
# Why split scan and push into separate jobs?
# • A scan failure can never trigger a push (push `needs: scan` and runs only on success).
# • Each job carries least-privilege permissions (scan gets security-events:write
# for SARIF; push gets id-token:write for cosign — neither has the other's power).
# • In the Actions UI you immediately see which phase failed instead of digging
# through one job's steps.
# • Re-running just `push` (e.g. after a transient registry error) doesn't redo
# the scan, since BuildKit layers come from the shared GHA cache.
#
# Hardcoded in this file (not exposed as inputs):
# REGISTRY = cr.yandex
# REGISTRY_ID = ${{ vars.YC_REGISTRY_ID }} (GitHub variable)
# PLATFORMS = linux/amd64,linux/arm64
#
# Push is opt-out via the `push` input (default true). Set `push: false` to stop
# after lint + scan — the gate runs, nothing is pushed. Useful for testing the
# pipeline without side effects.
#
# Usage from another repo:
#
# jobs:
# docker:
# permissions:
# contents: read # checkout
# security-events: write # Trivy SARIF -> Code Scanning (scan job)
# id-token: write # cosign keyless OIDC (push job)
# uses: <org>/github-actions/.github/workflows/docker.yaml@main
# with:
# image-name: myapp
# # optional: dockerfile, context, scan-severity,
# # smoke-test-args, provenance, sbom, registry-repo, push
# secrets: inherit # REQUIRED — forwards YC_CR_SA_AUTH_JSON (no explicit secret declared)
#
# WHY the caller must grant permissions:
# Reusable workflows can only *maintain or reduce* permissions, never elevate
# them. The called jobs request `security-events: write` (scan) and
# `id-token: write` (push), so the caller must grant both — otherwise GitHub
# rejects the workflow as invalid ("only allowed '...: none'").
#
# Required caller repo variable + secret:
# vars.YC_REGISTRY_ID - Yandex Container Registry id
# secrets.YC_CR_SA_AUTH_JSON - YC service account JSON key (used as `json_key` password)
#
# Note on `secrets: inherit`: this workflow does NOT declare a `secrets:` block, so the
# caller MUST pass `secrets: inherit`. Without it, YC_CR_SA_AUTH_JSON is empty and login fails.
#
# The full image reference is built as:
# cr.yandex/<YC_REGISTRY_ID>/<registry-repo|caller-repo>/<image-name>
# mirroring the existing cr.yandex/<id>/<repo>/<image> convention.
name: Build & Push Docker Image (YC CR)
on:
workflow_call:
inputs:
image-name:
description: Image name within the registry (last path segment).
required: true
type: string
dockerfile:
description: Path to the Dockerfile, relative to context.
required: false
default: ./Dockerfile
type: string
context:
description: Build context path.
required: false
default: .
type: string
scan-severity:
description: Trivy severity levels that fail the build.
required: false
default: HIGH,CRITICAL
type: string
smoke-test-args:
description: >-
Args appended to `docker run` against the built amd64 image. Empty = skip.
May be a full command (e.g. "fish --version") or, for images with a CMD
but no ENTRYPOINT, bare flags (e.g. "--version") which are auto-prepended
with CMD[0]. Images WITH an ENTRYPOINT receive the args appended to it.
required: false
default: ''
type: string
provenance:
description: SLSA provenance mode (mode=max | mode=min | false).
required: false
default: mode=max
type: string
sbom:
description: Attach an SBOM attestation (true/false).
required: false
default: 'true'
type: string
registry-repo:
description: Repository segment between registry id and image name. Empty = caller repo name.
required: false
default: ''
type: string
push:
description: >-
Run the push job (multi-arch build, registry push, SBOM + SLSA provenance,
cosign sign). When false, the workflow stops after lint + scan (the gate)
— useful for testing the pipeline without pushing. Default true (release).
required: false
default: 'true'
type: string
permissions:
contents: read # least-privilege default; expanded per job below
env:
REGISTRY: cr.yandex
REGISTRY_ID: ${{ vars.YC_REGISTRY_ID }}
PLATFORMS: linux/amd64,linux/arm64
concurrency:
group: ${{ inputs.image-name }}-${{ github.ref }}
cancel-in-progress: false # never cancel a multi-arch push mid-flight
jobs:
lint:
name: Lint Dockerfile
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Run hadolint
uses: hadolint/hadolint-action@v3.3.0
with:
dockerfile: ${{ inputs.dockerfile }}
scan:
name: Build & scan (amd64)
needs: lint
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # upload Trivy SARIF to Code Scanning
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Prepare image reference
env:
REPO_IN: ${{ inputs.registry-repo }}
REPO_DEFAULT: ${{ github.event.repository.name }}
IMAGE_NAME: ${{ inputs.image-name }}
run: |
REPO="${REPO_IN:-$REPO_DEFAULT}"
FULL="${REGISTRY}/${REGISTRY_ID}/${REPO}/${IMAGE_NAME}"
echo "FULL_IMAGE=${FULL}" >> "$GITHUB_ENV"
echo "::notice::Scanning ${FULL}:${{ github.sha }}"
- name: Set up Buildx
uses: docker/setup-buildx-action@v4
# Login is needed when the Dockerfile pulls a base image from the same YC registry.
- name: Log in to Yandex Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: json_key
password: ${{ secrets.YC_CR_SA_AUTH_JSON }}
# Build a single-arch image into the daemon so it can be scanned & smoke-tested.
# (Multi-arch cannot be loaded locally, hence the amd64-only scan image.)
- name: Build (amd64, load for scan)
uses: docker/build-push-action@v7
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: linux/amd64
load: true
tags: ${{ env.FULL_IMAGE }}:${{ github.sha }}
cache-from: type=gha,key=${{ inputs.image-name }}
cache-to: type=gha,mode=max,key=${{ inputs.image-name }}
provenance: false
- name: Scan image (Trivy)
uses: aquasecurity/trivy-action@v0.36.0 # v-prefixed tag (post-2025 supply-chain migration)
with:
image-ref: ${{ env.FULL_IMAGE }}:${{ github.sha }}
severity: ${{ inputs.scan-severity }}
ignore-unfixed: true
exit-code: '1'
format: sarif
output: trivy-results.sarif
# Print the findings as a readable table in the job log so scan failures
# are debuggable without leaving the Actions UI. Runs even when the scan
# step failed (exit-code: 1) so the CVEs that tripped the gate are visible.
- name: Print Trivy findings
if: always()
shell: bash
run: |
if [ ! -f trivy-results.sarif ]; then echo "No SARIF file produced."; exit 0; fi
python3 - <<'PY'
import json,re,sys
from collections import defaultdict
try:
d=json.load(open('trivy-results.sarif'))
except Exception as e:
print("Could not parse SARIF:",e); sys.exit(0)
sev_filter={"HIGH","CRITICAL"}
g=defaultdict(list)
for run in d.get('runs',[]):
for r in run.get('results',[]):
t=r.get('message',{}).get('text','')
pkg=re.search(r'Package:\s*(\S+)',t)
inst=re.search(r'Installed Version:\s*(\S+)',t)
sev=re.search(r'Severity:\s*(\S+)',t)
fix=re.search(r'Fixed Version:\s*(.+)',t)
cve=r.get('ruleId','')
s=sev.group(1) if sev else ''
if s not in sev_filter or not fix: continue
g[(pkg.group(1) if pkg else '?', inst.group(1) if inst else '?')].append((s,fix.group(1).strip(),cve))
if not g:
print("No fixable HIGH/CRITICAL vulnerabilities found.")
else:
print("=== Fixable HIGH/CRITICAL findings (fail the gate) ===")
print()
for (pkg,inst),items in sorted(g.items()):
cves=', '.join(sorted({c for _,_,c in items}))
fixes='; '.join(sorted({f for _,f,_ in items}))
print(f"{pkg} {inst} [{','.join(sorted({s for s,_,_ in items}))}]")
print(f" CVEs: {cves}")
print(f" Fix to: {fixes}")
print()
PY
- name: Upload Trivy SARIF
if: always()
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif
# Smoke test: run `docker run --rm <image> <smoke-test-args>`.
# If smoke-test-args begins with a flag (e.g. "--version"), `docker run <img> --version`
# would REPLACE the image CMD and try to exec the flag as a binary (OCI error:
# `exec: "--version": executable file not found`). For images that have no
# ENTRYPOINT but do have a CMD (typical for dev containers whose CMD is a shell),
# we prepend CMD[0] so `--version` becomes `<cmd0> --version`. Images WITH an
# ENTRYPOINT already receive args appended to it, so we leave those untouched.
# Pass a full command (e.g. "fish --version") to bypass the heuristic entirely.
- name: Smoke test
if: ${{ inputs.smoke-test-args != '' }}
run: |
IMG="${{ env.FULL_IMAGE }}:${{ github.sha }}"
ARGS="${{ inputs.smoke-test-args }}"
case "$ARGS" in
-*)
EP_LEN=$(docker inspect -f '{{len .Config.Entrypoint}}' "$IMG")
if [ "$EP_LEN" = "0" ]; then
CMD0=$(docker inspect -f '{{if .Config.Cmd}}{{index .Config.Cmd 0}}{{end}}' "$IMG")
if [ -z "$CMD0" ]; then
echo "::error::smoke-test-args start with '-' ('$ARGS') but the image has no ENTRYPOINT/CMD to prepend. Pass a full command (e.g. 'fish --version') instead."
exit 1
fi
echo "::notice::smoke-test-args start with a flag; prepending image CMD[0] ('$CMD0') -> running: $CMD0 $ARGS"
ARGS="$CMD0 $ARGS"
fi
;;
esac
docker run --rm $IMG $ARGS
- name: Scan summary
if: always()
run: |
{
echo "### Scan summary"
echo ""
echo "| Key | Value |"
echo "|---|---|"
echo "| Image | \`${{ env.FULL_IMAGE }}:${{ github.sha }}\` |"
echo "| Severity gate | \`${{ inputs.scan-severity }}\` |"
echo "| Smoke test | ${{ inputs.smoke-test-args != '' && 'ran' || 'skipped' }} |"
echo "| SARIF | Security › Code scanning alerts |"
echo "| Push | ${{ inputs.push == 'true' && '→ next job (multi-arch)' || 'skipped (push=false)' }} |"
} >> "$GITHUB_STEP_SUMMARY"
push:
name: Build & push (multi-arch) + sign
needs: scan
if: ${{ inputs.push == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC for cosign keyless signing
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Prepare image reference
env:
REPO_IN: ${{ inputs.registry-repo }}
REPO_DEFAULT: ${{ github.event.repository.name }}
IMAGE_NAME: ${{ inputs.image-name }}
run: |
REPO="${REPO_IN:-$REPO_DEFAULT}"
FULL="${REGISTRY}/${REGISTRY_ID}/${REPO}/${IMAGE_NAME}"
echo "FULL_IMAGE=${FULL}" >> "$GITHUB_ENV"
- name: Set up QEMU (multi-arch)
uses: docker/setup-qemu-action@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Yandex Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: json_key
password: ${{ secrets.YC_CR_SA_AUTH_JSON }}
- name: Derive tags & labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.FULL_IMAGE }}
# Published tags: the full commit SHA (hardcoded in the build-push
# step below) plus the git tag verbatim (e.g. 1.5). Do NOT use
# type=semver here: it demands strict X.Y.Z, so a `1.5` tag is
# rejected ("not a valid semver") and no image tag is generated.
# type=ref,event=tag is safe because callers gate which tags trigger
# a release (e.g. push tags: ['*.*']).
tags: |
type=ref,event=tag
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
# Rebuild multi-arch from the GHA cache populated by `scan`, then push with
# SBOM + SLSA provenance attestations attached.
- name: Build & push (multi-arch + attestations)
id: push
uses: docker/build-push-action@v7
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: |
${{ env.FULL_IMAGE }}:${{ github.sha }}
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: ${{ inputs.provenance }}
sbom: ${{ inputs.sbom == 'true' }}
cache-from: type=gha,key=${{ inputs.image-name }}
cache-to: type=gha,mode=max,key=${{ inputs.image-name }}
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign image (keyless via GitHub OIDC)
env:
COSIGN_EXPERIMENTAL: '1'
run: cosign sign --yes "${{ env.FULL_IMAGE }}@${{ steps.push.outputs.digest }}"
- name: Release summary
run: |
{
echo "### Docker release summary"
echo ""
echo "| Key | Value |"
echo "|---|---|"
echo "| Image | \`${{ env.FULL_IMAGE }}\` |"
echo "| Platforms | \`${{ env.PLATFORMS }}\` |"
echo "| Digest | \`${{ steps.push.outputs.digest }}\` |"
echo "| Attestations | provenance=\`${{ inputs.provenance }}\` sbom=\`${{ inputs.sbom }}\` |"
echo "| Signed | ✅ cosign keyless (OIDC) |"
echo "| Scan | Security › Code scanning alerts (Trivy SARIF) |"
} >> "$GITHUB_STEP_SUMMARY"