Skip to content

Commit cf35dbc

Browse files
[patch] Harden verified image publication (#15)
1 parent a06cbaa commit cf35dbc

4 files changed

Lines changed: 143 additions & 4 deletions

File tree

.github/workflows/build-push.yml

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
name: build-push
22
on:
3+
pull_request:
34
push:
5+
branches:
6+
- main
7+
tags:
8+
- "*"
49
workflow_dispatch:
510

11+
permissions:
12+
contents: read
13+
614
jobs:
715
lint-test:
816
runs-on: ubuntu-24.04
@@ -46,10 +54,66 @@ jobs:
4654
env:
4755
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
4856

57+
image-check:
58+
if: github.event_name == 'pull_request'
59+
strategy:
60+
fail-fast: false
61+
matrix:
62+
architecture:
63+
- runner: ubuntu-24.04
64+
platform: linux/amd64
65+
suffix: amd64
66+
- runner: ubuntu-24.04-arm
67+
platform: linux/arm64
68+
suffix: arm64
69+
runs-on: ${{ matrix.architecture.runner }}
70+
steps:
71+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
72+
with:
73+
persist-credentials: false
74+
75+
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
76+
77+
- name: Build native image without credentials
78+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
79+
with:
80+
context: .
81+
load: true
82+
platforms: ${{ matrix.architecture.platform }}
83+
provenance: false
84+
push: false
85+
tags: cap:ci-${{ matrix.architecture.suffix }}
86+
87+
- name: Scan native image
88+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
89+
with:
90+
image-ref: cap:ci-${{ matrix.architecture.suffix }}
91+
format: table
92+
exit-code: "1"
93+
ignore-unfixed: true
94+
severity: HIGH,CRITICAL
95+
vuln-type: os,library
96+
4997
run:
98+
name: run
99+
if: ${{ always() && github.event_name == 'pull_request' }}
100+
needs: [lint-test, image-check]
101+
permissions: {}
102+
uses: libops/.github/.github/workflows/pr-status.yaml@057262171193c6d563e7b08ae93d0c4aebfce2bd # credential-free-required-status
103+
with:
104+
needs-json: ${{ toJSON(needs) }}
105+
106+
publish:
107+
if: github.ref == 'refs/heads/main' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
50108
needs: [lint-test]
51-
uses: libops/.github/.github/workflows/build-push.yaml@main
109+
uses: libops/.github/.github/workflows/build-push.yaml@a86300fb8020d0f7141bb9f833d89b5dbd7aa4d7 # guarded-signed-image-publisher
110+
with:
111+
ref: ${{ github.sha }}
112+
expected-main-sha: ${{ github.ref == 'refs/heads/main' && github.sha || '' }}
113+
scan: true
114+
sign: true
115+
certificate-identity: https://github.com/libops/.github/.github/workflows/build-push.yaml@a86300fb8020d0f7141bb9f833d89b5dbd7aa4d7
52116
permissions:
53117
contents: read
54118
packages: write
55-
secrets: inherit
119+
id-token: write

.trivyignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Cap links the Prometheus parser/export client, not Prometheus's remote-read
2+
# HTTP handler where this decompression issue exists. Remove this exception
3+
# when prometheus-engine exposes a supported exporter on the patched line.
4+
CVE-2026-42154 exp:2026-10-01

Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.26.3-alpine3.22@sha256:be93003ee861b3b91b6ebcb22678524947e0cd786c2df3f32af520006b1e54f5 AS builder
1+
FROM golang:1.25.12-alpine@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder
22

33
SHELL ["/bin/ash", "-o", "pipefail", "-ex", "-c"]
44

@@ -15,7 +15,9 @@ COPY scraper ./scraper
1515
RUN --mount=type=cache,target=/root/.cache/go-build \
1616
CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/binary .
1717

18-
FROM golang:1.26.3-alpine3.22@sha256:be93003ee861b3b91b6ebcb22678524947e0cd786c2df3f32af520006b1e54f5
18+
FROM alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
19+
20+
RUN apk add --no-cache ca-certificates
1921

2022
COPY --from=builder /app/binary /app/binary
2123

ci/workflow_contract_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package ci
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
)
8+
9+
const sharedPublisherSHA = "a86300fb8020d0f7141bb9f833d89b5dbd7aa4d7"
10+
11+
func TestImagePublicationWorkflowContract(t *testing.T) {
12+
workflow, err := os.ReadFile("../.github/workflows/build-push.yml")
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
contents := string(workflow)
17+
18+
required := []string{
19+
"pull_request:",
20+
"if: github.event_name == 'pull_request'",
21+
"if: github.ref == 'refs/heads/main' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))",
22+
"Build native image without credentials",
23+
"libops/.github/.github/workflows/build-push.yaml@" + sharedPublisherSHA,
24+
"ref: ${{ github.sha }}",
25+
"expected-main-sha: ${{ github.ref == 'refs/heads/main' && github.sha || '' }}",
26+
"scan: true",
27+
"sign: true",
28+
"certificate-identity: https://github.com/libops/.github/.github/workflows/build-push.yaml@" + sharedPublisherSHA,
29+
"packages: write",
30+
"id-token: write",
31+
}
32+
for _, value := range required {
33+
if !strings.Contains(contents, value) {
34+
t.Errorf("image workflow must contain %q", value)
35+
}
36+
}
37+
38+
forbidden := []string{
39+
"build-push.yaml@main",
40+
"build-push-ghcr.yaml",
41+
"secrets: inherit",
42+
"docker-registry:",
43+
"additional-gar-registry:",
44+
}
45+
for _, value := range forbidden {
46+
if strings.Contains(contents, value) {
47+
t.Errorf("image workflow must not contain %q", value)
48+
}
49+
}
50+
}
51+
52+
func TestTrivyExceptionIsNarrowAndExpiring(t *testing.T) {
53+
ignore, err := os.ReadFile("../.trivyignore")
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
58+
lines := strings.Split(strings.TrimSpace(string(ignore)), "\n")
59+
var rules []string
60+
for _, line := range lines {
61+
line = strings.TrimSpace(line)
62+
if line != "" && !strings.HasPrefix(line, "#") {
63+
rules = append(rules, line)
64+
}
65+
}
66+
if len(rules) != 1 || rules[0] != "CVE-2026-42154 exp:2026-10-01" {
67+
t.Fatalf("unexpected Trivy exception rules: %q", rules)
68+
}
69+
}

0 commit comments

Comments
 (0)