Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- .github/workflows/ci.yaml
- .github/workflows/verify.yaml
- .github/workflows/release.yaml
- .github/workflows/rekor-monitor.yaml
- .github/actions/**
- .gitattributes
- actions/**
Expand All @@ -25,6 +26,7 @@ on:
- .github/workflows/ci.yaml
- .github/workflows/verify.yaml
- .github/workflows/release.yaml
- .github/workflows/rekor-monitor.yaml
- .github/actions/**
- .gitattributes
- actions/**
Expand Down Expand Up @@ -59,6 +61,10 @@ jobs:
run: go vet ./...
- name: Test Go packages
run: go test ./...
- name: Vet Rekor proof helper
run: go -C tools/rekor-entry vet ./...
- name: Test Rekor proof helper
run: go -C tools/rekor-entry test ./...

shell-tests:
runs-on: ubuntu-24.04
Expand Down
138 changes: 138 additions & 0 deletions .github/workflows/rekor-monitor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: Rekor identity monitor

on:
schedule:
- cron: "23 * * * *"
workflow_dispatch:
inputs:
bootstrap:
description: "Initialize the first checkpoint (first run only; no historical coverage)."
type: boolean
default: false

permissions:
contents: read
actions: read

concurrency:
group: rekor-identity-monitor
cancel-in-progress: false

jobs:
monitor:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 50
env:
GOTOOLCHAIN: local
GOFLAGS: -mod=readonly
GOPROXY: https://proxy.golang.org
GOSUMDB: sum.golang.org
MONITOR_VERSION: v0.0.0-20261019132539-47017b2379cb
# Test-channel placeholder only; no webhook or production routing is configured.
ALERT_CHANNEL: "#ops-testing-new-alerts"
steps:
- name: Checkout monitor adapter
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false

- name: Enforce runner CA trust
uses: ./.github/actions/ca-trust-guard

- name: Install locked Go toolchain
uses: ./.github/actions/locked-tools
with:
tools: go

- name: Restore newest valid monitor state
id: restore
env:
GH_TOKEN: ${{ github.token }}
BOOTSTRAP: ${{ inputs.bootstrap || false }}
run: |
set -euo pipefail
mkdir -p monitor-state
workflow_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/rekor-monitor.yaml" --jq '.id')"
candidates="$(gh api --paginate --slurp \
"repos/$GITHUB_REPOSITORY/actions/artifacts?name=rekor-monitor-state&per_page=100" |
jq -r '[.[].artifacts[] | select(.name == "rekor-monitor-state" and .expired == false and .workflow_run.head_branch == "main")] | sort_by(.created_at) | reverse | .[].workflow_run.id')"
run_id=""
candidate_dir=""
trap 'if [ -n "$candidate_dir" ]; then rm -rf "$candidate_dir"; fi' EXIT
while IFS= read -r candidate; do
[ -n "$candidate" ] || continue
candidate_workflow="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$candidate" --jq '.workflow_id')"
if [ "$candidate_workflow" = "$workflow_id" ]; then
candidate_dir="$(mktemp -d "./.rekor-state.XXXXXX")"
if ! gh run download "$candidate" --repo "$GITHUB_REPOSITORY" \
--name rekor-monitor-state --dir "$candidate_dir"; then
echo "::error::download failed for run $candidate" >&2
exit 1
fi
if test -s "$candidate_dir/checkpoint.txt" &&
test -s "$candidate_dir/reconciliation.json"; then
rmdir monitor-state
mv "$candidate_dir" monitor-state
candidate_dir=""
run_id="$candidate"
break
fi
echo "::warning::skipping incomplete state artifact from run $candidate" >&2
rm -rf "$candidate_dir"
candidate_dir=""
fi
done <<< "$candidates"
if [ -z "$run_id" ] && [ "$BOOTSTRAP" != true ]; then
echo "::error::No monitor state exists; initialize once with the bootstrap dispatch input."
exit 1
elif [ -z "$run_id" ]; then
echo "bootstrap=true" >> "$GITHUB_OUTPUT"
fi

- name: Install pinned upstream monitor
run: go install "github.com/sigstore/rekor-monitor/cmd/rekor_monitor@$MONITOR_VERSION"

- name: Build single-purpose entry proof verifier
working-directory: tools/rekor-entry
run: |
set -euo pipefail
go build -o "$RUNNER_TEMP/rekor-entry" .

- name: Verify checkpoint and scan identities
run: |
set -euo pipefail
"$(go env GOPATH)/bin/rekor_monitor" --once=true --monitor-port=0 \
--url https://rekor.sigstore.dev \
--file monitor-state/checkpoint.txt --config-file tools/rekor-monitor.yaml \
2>&1 | tee monitor-state/diagnostics.log
touch monitor-state/identities.jsonl

- name: Check signed digests and checkpoint progress
env:
GH_TOKEN: ${{ github.token }}
REKOR_ENTRY_HELPER: ${{ runner.temp }}/rekor-entry
MONITOR_BOOTSTRAP: ${{ steps.restore.outputs.bootstrap || false }}
run: bash scripts/rekor_digest_check.sh

- name: Check monitor state completeness
id: state
if: always()
run: |
if [ -s monitor-state/checkpoint.txt ] && [ -s monitor-state/reconciliation.json ]; then
echo "state_complete=true" >> "$GITHUB_OUTPUT"
fi

- name: Persist verified checkpoint and reconciliation state
if: always() && steps.state.outputs.state_complete == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: rekor-monitor-state
path: monitor-state/
if-no-files-found: error
retention-days: 30

- name: Alert delivery placeholder
if: failure()
run: |
echo '::warning::Rekor monitor needs attention. Alert delivery is not wired; intended test channel: #ops-testing-new-alerts.'
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ test: test-go test-scripts
test-go:
go test ./cmd/extract-images ./cmd/record-release ./cmd/generate-manifest ./cmd/merge-manifests ./cmd/publish-public-ecr-release-tags
GOTOOLCHAIN=local GOPROXY=off go test ./cmd/ca-trust-guard/...
GOTOOLCHAIN=local go -C tools/rekor-entry test ./...

.PHONY: test-scripts
test-scripts:
bash scripts/test-derive-iam-role-name.sh
bash scripts/test-normalize-release-options.sh
python3 scripts/test-release-config-templates.py
python3 scripts/test-rekor-monitor.py
python3 scripts/test-check-image-locks.py
python3 scripts/test-windows-pre-build-script.py
python3 scripts/test-safe-relative-path.py
Expand Down
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,108 @@ Use the [CA Trust Guard action](.github/actions/ca-trust-guard/README.md) as a
job's first step for offline incident-chain checks, fingerprint-checked OS
denial and a committed clean-bundle environment. No caller checkout is needed.

## Rekor Identity Monitor

`.github/workflows/rekor-monitor.yaml` runs hourly on `main`, watching the three
public release-workflow SANs and the GitHub Actions issuer configured in
`tools/rekor-monitor.yaml`. It installs the upstream monitor at
`v0.0.0-20261019132539-47017b2379cb` through the Go module proxy and checksum
database, with `GOTOOLCHAIN=local` and `GOFLAGS=-mod=readonly`.
The module sum is `h1:+ICdEtSt8tWgvUR8YdwoMOavuNf2yWEqH5FXSpxVdhI=`.
The existing locked-tools action supplies the Go toolchain.

The upstream binary verifies signed checkpoints and consistency with the prior
checkpoint; it does **not** verify matched entries' inclusion. The small isolated
`tools/rekor-entry` Go helper verifies the fetched entry's RFC 6962 inclusion,
body/UUID binding, signed timestamp, checkpoint signature and consistency with
the monitor's verified view. A proof's signed checkpoint may be newer than the
saved view, which is why simply comparing roots in Python would be insufficient.
The helper emits authenticated certificate metadata for hashedrekord entries;
the stdlib-only Python adapter performs the public-data joins.

Fulcio chain checks use committed public roots and intermediates extracted from
the public-good TUF `trusted_root.json` after a Sigstore Go TUF refresh:

| Material | SHA256 |
| --- | --- |
| `tools/rekor-fulcio-roots.pem` (2 roots) | `d543eb6e067c069c6b2fd2c22258452db2f923b221597303a6bce4c664209ee6` |
| `tools/rekor-fulcio-intermediates.pem` (1 intermediate) | `ac5a60ca80f473f8b7742111f9eb49bf7dbd8f03f41a5b39d58b62b6f0de4d0f` |
| Verified `trusted_root.json` source | `6494e21ea73fa7ee769f85f57d5a3e6a08725eae1e38c755fc3517c9e6bc0b66` |

Extraction used TUF snapshot version 166, targets version 14 and root version 16.
Root changes require a reviewed refresh; this does not install a new OS trust
anchor. Non-chaining certificates are skipped by upstream before identity
matching and retained as informational chain-failure diagnostics. The entry
helper likewise reports `UNAUTHENTICATED-SAN`, never a run-identity match.

Each supported entry has separate **run-identity-match** and
**artifact-authorized** verdicts. A real concluded GitHub run, pinned reusable
workflow reference, matching caller commit and plausible signature time prove
the former, not the latter. Certificate repository/tag extensions locate the
public catalog index, connector index and exact release manifest at
`https://dist.conductorone.com`. Known archive/checksum hashes and the hash of
the published manifest bytes are joinable signed-byte digests. Missing runs,
unexpected workflow refs and unrecorded digests on artifact-only releases are
anomalies. In-flight runs and not-yet-published digests stay `PENDING` for at most
six hours; the pending observations persist across cron runs.

`UNKNOWN` is not success or an anomaly: hidden catalogs (including HTTP 403),
unknown storage aliases, missing certificate extensions, DSSE/intoto payloads
and non-joinable signatures are never given invented joins. A hashedrekord OCI
signature records the hash of simple-signing payload bytes, **not** the image
digest. The current manifest has no `signed` flag: the ambiguity budget is the
number of public-ECR `images` entries with `isIndex: true`; private Lambda
images do not count. At most that many distinct unrecorded hashes per release
remain `UNKNOWN` after reconciliation; the next is an anomaly. The budget
persists across runs, counts distinct hashes rather than duplicate log entries,
and must be revisited if the signed-image set or manifest schema changes.

### Initialization, state and alert handoff

After review/merge, dispatch the workflow once on `main` with `bootstrap: true`.
The first checkpoint establishes a baseline, **not** historical coverage.
Subsequent runs restore the newest non-expired `rekor-monitor-state` artifact
from a main-branch run of this workflow (30-day retention), preserving the
checkpoint, pending entries, digest budget and reports. Artifact-less cancelled
or early-failed runs cannot wedge restoration. Each candidate is downloaded to
an isolated temporary directory and accepted only when `checkpoint.txt` and
`reconciliation.json` are both non-empty; incomplete candidates fall through to
older artifacts with a warning. Download failures instead fail closed without
falling back, so an older budget or observation clock cannot be re-published as
the newest state. An always-run completeness check applies the same requirement
before uploading state, so partial first attempts do not perpetuate incomplete
artifacts. Missing or expired usable state fails closed rather than silently
restarting; an operator must recover the last valid artifact before resuming.
Artifacts and job logs remain the evidence; no state is committed by the bot.

Signed checkpoints contain no timestamp. The adapter persists the first
wall-clock observation of each origin/size/root and fails if the same view is
unchanged for more than two hours. Changing only its note signature does not
reset the clock. Initial-view currency is unproven, and a quiet log cannot be
distinguished from a stalled feed at first observation.

Alerts are **not wired**: reports and failure annotations name the test-channel
placeholder `#ops-testing-new-alerts`; no webhook or production routing is
configured. Configure and exercise delivery to that test channel before any
production alerting. No attacker tokens, live signing or self-signed canary
entries are requested by this workflow.

Recorded low-priority follow-ups: add a scheduled dedicated-identity canary to
close the initial-view/liveness gap; compare DSSE payload hashes to payloads in
published provenance/SBOM bundles; schedule reviewed TUF root refreshes; and
index published OCI simple-signing payload hashes. Until the last item, replay
inside an image release's ambiguity budget is undetected. A replay over an
already-recorded digest is inherently indistinguishable. This is a detection
aid over published data, not a replacement for registry signature verification
or proof that a run intended particular bytes.

Offline checks: `python3 scripts/test-rekor-monitor.py`,
`go -C tools/rekor-entry test ./...`, and `make test-scripts`. They use synthetic
fixtures, including replay, missing run, frozen checkpoint, pending/resolved
publication, image-budget exhaustion, altered proofs and untrusted certificates.
Live fleet/axiomatic release observation and alert delivery remain rollout
rehearsals, not claims established by fixture tests.

## PR Review Workflow

Runs Claude-powered PR review for same-repository pull requests. Fork PRs are skipped
Expand Down
Loading
Loading