Skip to content

Commit a6516bc

Browse files
committed
feat(ci): own Rekor v2 identity monitoring via tools/rekor-monitor
Replace the merged-but-broken reusable-workflow approach (#1727) with an owned monitor that can actually watch the public-good Rekor v2. The upstream sigstore/rekor-monitor reusable workflow resolves its Rekor API version and shards from Sigstore's default signing config (signing_config.v0.2.json), which is v1-only and stays that way for the foreseeable future. AICR opted into Rekor v2 early via the separate signing_config_rekor_v2.v0.2.json TUF target, which upstream never reads and cannot be pointed at, so passing it a v2 shard URL falls through to v1 and its v1 client times out against the v2 tile endpoint. That is why the merged workflow fails every hour on main. tools/rekor-monitor closes only the AICR-specific gap: it reads the v2 signing config AICR signs against (pkg/trust) and reuses the upstream rekor-monitor library packages (pkg/rekor/v2, pkg/tiles, pkg/identity) for the security-critical consistency proof and identity search, so we do not reimplement transparency-log verification. When Sigstore makes v2 the ecosystem default the reusable workflow can monitor v2 directly and this tool can be retired. - tools/rekor-monitor: doc.go (rationale), main.go (run orchestration), monitor.go (shards + identity checks + outcome), checkpoint.go (cursor + native zip restore), with unit tests and a README. - .github/workflows/rekor-monitor.yaml: run the tool; fetch the prior checkpoint artifact via the API and restore it natively; open/close a deduplicated alert issue on failure/success. New artifact name so the stale v1 checkpoint is ignored (no migration). - go.mod/vendor: add sigstore/rekor-monitor + transparency-dev/formats. - docs/contributor/maintaining.md: document why upstream cannot do this yet. Signed-off-by: Brian Lockwood <lockwobr@gmail.com>
1 parent 2e0bc38 commit a6516bc

40 files changed

Lines changed: 5940 additions & 116 deletions

File tree

.github/workflows/rekor-monitor.yaml

Lines changed: 189 additions & 71 deletions
Large diffs are not rendered by default.

THIRD_PARTY_NOTICES.md

Lines changed: 428 additions & 0 deletions
Large diffs are not rendered by default.

docs/contributor/maintaining.md

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ failure during the first 60s after tag publish. Re-run the workflow.
5252
## Release Supply-Chain Monitoring
5353

5454
The `Rekor Monitor` workflow (`.github/workflows/rekor-monitor.yaml`) runs
55-
hourly and calls the upstream `sigstore/rekor-monitor` reusable workflow. It
56-
watches the **Rekor v2** transparency log (where AICR release signing writes
57-
since [#1650](https://github.com/NVIDIA/aicr/issues/1650)) for two things, both
58-
in one job: that the log stays append-only (consistency), and that no entry
59-
appears under AICR's release signing identity that a release did not produce
60-
(identity). On either failure it opens an issue.
55+
hourly and runs our own monitor, `tools/rekor-monitor`, against the **Rekor v2**
56+
transparency log (where AICR release signing writes since
57+
[#1650](https://github.com/NVIDIA/aicr/issues/1650)). In one job it checks two
58+
things: that the log stays append-only (consistency), and that no entry appears
59+
under AICR's release signing identity that a release did not produce (identity).
60+
On any failure it opens a tracking issue; a later clean run closes it.
6161

6262
This protects the trust root every AICR consumer depends on: the release
6363
binaries, the signed recipe catalog, and the container images all chain to that
@@ -75,48 +75,49 @@ grows, so it can never keep up inside a bounded CI job: the earlier v1
7575
identity config timed out on every run and never completed a single scan
7676
([#1623](https://github.com/NVIDIA/aicr/issues/1623)). Rekor **v2** is
7777
tile-based: bulk 256-entry reads let a single worker outpace the log, so the
78-
identity scan is a cheap job that always finishes. This is why the whole design
79-
is a single unbounded scan again rather than sharded paging.
80-
81-
### Shard selection (automatic, no manual re-tune)
82-
83-
The monitor selects Rekor v2 by pointing its `url` at a v2 shard listed in the
84-
Sigstore `SigningConfig`; a match on a v2 service switches it to v2, after which
85-
it auto-discovers the **full** shard set from TUF and refreshes it every run.
86-
87-
The shard URL is **not hardcoded**. The `resolve-v2-shard` job computes it at run
88-
time from the same TUF-distributed signing config that release signing resolves,
89-
then feeds it to the monitor:
78+
identity scan is a cheap job that always finishes.
79+
80+
### Why our own tool, not the upstream reusable workflow
81+
82+
The upstream `sigstore/rekor-monitor` reusable workflow selects its Rekor API
83+
version and discovers shards from Sigstore's **default** signing config,
84+
`signing_config.v0.2.json`. That config lists only Rekor v1 and, per Sigstore's
85+
[rekor-evolution](https://blog.sigstore.dev/rekor-evolution/) plan, keeps v1 as
86+
the ecosystem default "for the foreseeable future". AICR opted into v2 **early**
87+
via a separate TUF target, `signing_config_rekor_v2.v0.2.json` (see `pkg/trust`),
88+
which the upstream tool never reads and exposes no flag to select. So pointing
89+
it at a v2 shard URL just falls through to v1 and fails.
90+
91+
`tools/rekor-monitor` closes exactly that gap: it reads the v2 signing config
92+
AICR actually signs against (`trust.ResolveSigningConfig`) and then reuses the
93+
upstream rekor-monitor **library** packages for the security-critical work (tile
94+
consistency proofs and identity search), so we do not reimplement
95+
transparency-log verification. To inspect the current v2 shard the way the tool
96+
resolves it:
9097

9198
```bash
92-
# Same command the resolve-v2-shard job runs (works from a fresh checkout).
9399
go run ./cmd/aicr trust update --emit-signing-config signing-config.json
94100
jq -er '[.rekorTlogUrls[] | select(.majorApiVersion == 2)] | sort_by(.validFor.start) | last | .url' signing-config.json
95101
```
96102

97-
Because it reads the signing config directly, the monitor provably watches where
98-
releases actually write, and yearly shard rotation (`log2025-1` -> `log2026-1`
99-
-> ...) needs no change to this workflow. If the resolve job ever fails (for
100-
example the TUF CDN is unreachable), the monitor job is skipped for that run and
101-
retries on the next hourly tick.
102-
103-
### First run after switching from v1: reset the checkpoint
104-
105-
The `checkpoint` artifact persists a **v1** checkpoint from the prior config; a
106-
v2 run cannot parse it and will fail. After merging a change that moves this
107-
workflow to v2, delete the stale artifact once so the first v2 run establishes a
108-
fresh v2 baseline (it saves the current v2 tree head and scans forward from
109-
there):
110-
111-
```bash
112-
gh api "repos/NVIDIA/aicr/actions/artifacts?name=checkpoint" \
113-
--jq '.artifacts[].id' \
114-
| xargs -I{} gh api -X DELETE "repos/NVIDIA/aicr/actions/artifacts/{}"
115-
```
116-
117-
The first v2 run then watches forward from the current head; historical entries
118-
predating the baseline are covered by release-time verification (the `aicr
119-
verify` path), not by this monitor.
103+
When Sigstore makes v2 the ecosystem default, `signing_config.v0.2.json` will
104+
list the v2 shards, the upstream reusable workflow can monitor v2 directly, and
105+
this tool can be retired. Until then upstream exposes no flag to point the
106+
monitor at a non-default signing config (it always reads
107+
`signing_config.v0.2.json`); a feature request for that would let early v2
108+
adopters drop this tool.
109+
110+
### Checkpoint and first run
111+
112+
The monitor persists its cursor as the `rekor-v2-checkpoint` artifact between
113+
runs (a deliberately fresh name, so the stale v1 `checkpoint` artifact from the
114+
earlier design is simply ignored, no migration). The **first** run has no prior
115+
checkpoint, so it establishes a baseline at the current v2 tree head and skips
116+
the identity scan; every run after that scans only the newly-added window.
117+
Entries predating the baseline are covered by release-time verification (the
118+
`aicr verify` path), not by this monitor. Shard rotation (`log2025-1` ->
119+
`log2026-1` -> ...) needs no change here: the tool reads the live shard set from
120+
the signing config every run.
120121

121122
## Reviewing Recipe Contributions
122123

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/opencontainers/image-spec v1.1.1
1818
github.com/prometheus/client_golang v1.23.2
1919
github.com/sigstore/protobuf-specs v0.5.1
20+
github.com/sigstore/rekor-monitor v0.0.0-20260622135518-170374c6119e
2021
github.com/sigstore/sigstore v1.10.8
2122
github.com/sigstore/sigstore-go v1.2.2
2223
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8
@@ -25,6 +26,7 @@ require (
2526
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8
2627
github.com/stretchr/testify v1.11.1
2728
github.com/theupdateframework/go-tuf/v2 v2.4.2
29+
github.com/transparency-dev/formats v0.1.1
2830
github.com/urfave/cli/v3 v3.10.1
2931
golang.org/x/mod v0.38.0
3032
golang.org/x/sync v0.22.0
@@ -189,8 +191,8 @@ require (
189191
github.com/spf13/pflag v1.0.10 // indirect
190192
github.com/stretchr/objx v0.5.3 // indirect
191193
github.com/theupdateframework/go-tuf v0.7.0 // indirect
192-
github.com/transparency-dev/formats v0.1.1 // indirect
193194
github.com/transparency-dev/merkle v0.0.2 // indirect
195+
github.com/transparency-dev/tessera v1.0.2 // indirect
194196
github.com/x448/float16 v0.8.4 // indirect
195197
github.com/xlab/treeprint v1.2.0 // indirect
196198
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect

go.sum

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9
286286
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
287287
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
288288
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
289+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
290+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
289291
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
290292
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
291293
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
@@ -417,6 +419,8 @@ github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah
417419
github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
418420
github.com/sigstore/rekor v1.5.3 h1:0Tyolw3zreRgm7PUW8dccFLXGBThi08278jI8EXNSr4=
419421
github.com/sigstore/rekor v1.5.3/go.mod h1:h3GK5dDqCcWJJZUJwdpKGSSmEV2GEjPUjJy3WTjBwzA=
422+
github.com/sigstore/rekor-monitor v0.0.0-20260622135518-170374c6119e h1:Ny+E65WYMRlniv/Z7el+d3FZ+9HAU/WCBk0d10qh6kU=
423+
github.com/sigstore/rekor-monitor v0.0.0-20260622135518-170374c6119e/go.mod h1:gR0/nivRiMAwhovKEomBpLn4t0R5W5IGoyxMyDSLpoI=
420424
github.com/sigstore/rekor-tiles/v2 v2.3.0 h1:HhMgH61UP0t899V8Fjt7pz1YdgOBptbaQdnCF+79cdc=
421425
github.com/sigstore/rekor-tiles/v2 v2.3.0/go.mod h1:DEFiKSyQ4nF75QRVNdOPaIH3cmvMkO2B6xDZjNYngPc=
422426
github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4=
@@ -473,6 +477,8 @@ github.com/transparency-dev/formats v0.1.1 h1:4bVHJc+KdBgpA1OJD1yjI+g0i5Z1graCpp
473477
github.com/transparency-dev/formats v0.1.1/go.mod h1:qtZ8goRuJ8FTBG9c9+Bj0rn2rUG7eG/AUTkr+Aw3jFw=
474478
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
475479
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
480+
github.com/transparency-dev/tessera v1.0.2 h1:PNfGPFfJHpCFVswlrsQvRghxqYz2xy/OtP8qTjJuzFQ=
481+
github.com/transparency-dev/tessera v1.0.2/go.mod h1:WD/EMM6RXWRyImk9yyJ2hrs8xdknN/lpwUrFR2GemfU=
476482
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
477483
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
478484
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -517,8 +523,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk
517523
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
518524
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
519525
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
520-
go.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA=
521-
go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ=
526+
go.step.sm/crypto v0.83.0 h1:llCPEiL2f+kUPY+CUJrOCsuSBoJZ2qooFG9EqGast6w=
527+
go.step.sm/crypto v0.83.0/go.mod h1:qyLTv666WJ6ImFPUjljux+684Y/GGYUjAZcKCnc6yBs=
522528
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
523529
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
524530
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=

tools/rekor-monitor/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# rekor-monitor: AICR release-signer transparency-log monitor (Rekor v2)
2+
3+
Watches the Rekor **v2** transparency log for AICR's release supply chain. On
4+
each run it does two checks and exits non-zero if either is unhappy (so the
5+
calling workflow can alert):
6+
7+
- **Consistency**: proves the log stayed append-only from the last checkpoint
8+
to the current tree head (an O(log n) tile proof). Its main job here is to
9+
*anchor the incremental identity scan*: it guarantees nothing was removed or
10+
rewritten between runs, which is what makes "scan only the new window" sound
11+
(otherwise a rewritten view could hide a malicious entry). It is also the
12+
standard append-only tamper check, though ecosystem witnesses are the primary
13+
guarantee of that.
14+
- **Identity**: scans the entries added since the last checkpoint for AICR's
15+
release signing identity. An entry under that identity that no release
16+
produced signals OIDC/key compromise. This is the AICR-specific check.
17+
18+
It is run hourly by `.github/workflows/rekor-monitor.yaml`.
19+
20+
## Why not the upstream `sigstore/rekor-monitor` reusable workflow?
21+
22+
**Because upstream cannot monitor the public-good Rekor v2 that AICR signs to.**
23+
24+
The upstream reusable workflow decides which Rekor API version to talk to (and
25+
which shards to read) from Sigstore's **default** signing config,
26+
`signing_config.v0.2.json`. That config lists **only Rekor v1**, and per
27+
Sigstore's [rekor-evolution](https://blog.sigstore.dev/rekor-evolution/) plan it
28+
keeps v1 as the ecosystem default "for the foreseeable future".
29+
30+
AICR opted into Rekor v2 **early** (see [#1650](https://github.com/NVIDIA/aicr/issues/1650))
31+
via a *separate* TUF target, `signing_config_rekor_v2.v0.2.json`, which the
32+
upstream tool never reads and exposes no flag to select. Both its version
33+
detection (`getRekorVersion`) and its v2 shard discovery (`RefreshSigningConfig`)
34+
go through `root.GetSigningConfig`, which is hardcoded to the v1-only default
35+
target. So passing it a v2 shard URL just falls through to v1, and its v1 client
36+
then times out against the v2 tile endpoint. (We tried exactly that first; see
37+
the history on [#1623](https://github.com/NVIDIA/aicr/issues/1623).)
38+
39+
This tool changes **only that one AICR-specific bit**: it reads the v2 signing
40+
config AICR actually signs against (`pkg/trust`), and otherwise **reuses the
41+
upstream rekor-monitor library packages** (`pkg/rekor/v2`, `pkg/tiles`,
42+
`pkg/identity`) for the security-critical verification, so we are not
43+
reimplementing transparency-log crypto, just pointing it at the right config.
44+
45+
When Sigstore makes v2 the ecosystem default, `signing_config.v0.2.json` will
46+
list the v2 shards, the upstream reusable workflow can monitor v2 directly, and
47+
**this tool can be retired**. Until then, upstream has no flag to target a
48+
non-default signing config.
49+
50+
## Layout
51+
52+
| File | Responsibility |
53+
|------|----------------|
54+
| `doc.go` | Package doc: rationale (above) and structure, surfaced by `go doc`. |
55+
| `main.go` | Flags, `run()` orchestration, process exit codes. |
56+
| `monitor.go` | `monitor` (resolved v2 shards + watched identity) and its two checks; the `outcome` of a pass. |
57+
| `checkpoint.go` | `checkpointStore`: restore the cursor from the fetched artifact zip, read the last checkpoint, write the new one. |
58+
59+
## Usage
60+
61+
```bash
62+
go run ./tools/rekor-monitor \
63+
--file checkpoint_v2.txt \
64+
--restore-zip checkpoint.zip \
65+
--cert-subject '^https://github\.com/NVIDIA/aicr/\.github/workflows/on-tag\.yaml@refs/tags/.*$' \
66+
--cert-issuer '^https://token\.actions\.githubusercontent\.com$'
67+
```
68+
69+
| Flag | Purpose |
70+
|------|---------|
71+
| `--file` | Path to the persisted v2 checkpoint (the cursor). Missing/empty = first run: baseline at the current head and skip the identity scan. |
72+
| `--restore-zip` | Optional GitHub-artifact zip to seed `--file` from before monitoring. Missing file = first run. |
73+
| `--cert-subject` | Regex for the monitored certificate SAN. Empty = consistency-only (no identity scan). |
74+
| `--cert-issuer` | Regex for the monitored certificate issuer (requires `--cert-subject`). |
75+
| `--user-agent` | User-Agent for requests to the log. |
76+
77+
Exit status: `0` clean; non-zero on a consistency break, a scan error, or an
78+
identity match.

0 commit comments

Comments
 (0)