You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Accept the .github publish-workflow signing identity before repointing publishers #242
The platform checks every tenant artifact's keyless signature against a certificate subject pinned to the publishing workflow in devantler-tech/actions: ^https://github\.com/devantler-tech/actions/\.github/workflows/<publish-workflow>\.yaml@[0-9a-f]{40}$, together with an approved-revisions list (platformscripts/tests/test-publish-workflow-signing-revisions.sh and test-publish-workflow-approved-revisions.sh). A signature's certificate names the reusable workflow that produced it. Once a publisher calls publish-app.yaml or publish-manifests.yaml from .github, its subject becomes https://github.com/devantler-tech/.github/.github/workflows/... and fails that check.
Publishers found on 2026-09-25: .github (its own deploy/ artifact), ascoachingogvaner, aws, data-product-controller, dotnet-template, platform-tenant-template, wedding-app. Several carry their own publish-pin-approved checks keyed on the old path.
Problem
Repointing any publisher first would stop the platform from accepting that tenant's next release, including the organization's own GitHub-as-code artifact.
Acceptance criteria
The platform's signature checks accept the .github-hosted publish workflows at reviewed revisions, alongside the current actions identity for as long as any tenant still publishes through it.
🤖 Generated with Claude Code in an interactive session with the maintainer.
Where platform hard-codes the actions identity (static read of main @ 9eed5255, 2026-09-25)
Live subjects, 7, each a single regex. Cosign and Kyverno take one identity entry, and guard-oci-repository-verify.sh and the approved-revisions lib both refuse a second one. So .github can only be added as an alternation inside the one regex, as devantler-tech/(actions|\.github)/…. The pin guard's exactly-one-@ rule rules out a whole-identity alternation.
Per consumer, narrowed to SHA sets: apps/{ascoachingogvaner,wedding-app,github-config}/oci-repository.yaml and providers/hetzner/apps/aws/oci-repository.yaml.
Generic @[0-9a-f]{40} form: verify-app-images.yaml, the tenant ResourceGraphDefinition and talos/cluster/verify-first-party-images.yaml.
Scripts and Go, all single prefixes:
publish-workflow-approved-revisions.lib.sh (SUBJECT_PREFIX, plus a yq test()).
report-publish-workflow-signing-revisions.sh (SUBJECT_PATTERN, and the pin_at_refuses: regex).
guard-shared-publish-workflow-pin.sh (SUBJECT_PATTERN, and EXPECTED_MIN_SUBJECTS=7).
generate-publish-workflow-approved-revisions.sh (a startswith filter, and the release-candidate lookup on devantler-tech/actions releases).
write-publish-workflow-matchers.sh, which rebuilds every subject from the one prefix. Left as is, it would move a .github-family consumer back to actions.
watch-actions-release-candidate.sh.
validate-eks-ci-role-policy/publish_matcher_projection.go, which projects to the actions pattern form and so feeds the EKS approval fingerprint.
Approved-revisions TSV: it has no repository column, so every SHA is implicitly an actions SHA. Tenant repos that sign in their own CI read its header and columns, so adding a column is a cross-repository change.
Proposed order
Behaviour-preserving first step: every script, guard and Go projection accepts a second prefix family (devantler-tech/.github/.github/workflows/publish-*), and the writer keeps each consumer's recorded family. The 7 live subjects, the TSV and the release resolver stay unchanged, so nothing is accepted live yet. Add .github fixtures to each test suite: a SHA is accepted and the family is kept; @main, devantler-tech/github/ and devantler-tech/actionsx/ are refused.
Record the repository per revision: either a TSV repository column, coordinated with the tenant guards, or the cross-product (actions|\.github)/…@(A|B) form. A consumer mid-switch needs actions@signer and .github@pin at once.
github-config must call its own publish workflow by full SHA (devantler-tech/.github/.github/workflows/publish-manifests.yaml@<sha>), never through a local ./ call. A local call signs with a ref, which the pin guard refuses.
Move the release-candidate source to .github releases.
🤖 Generated with Claude Code in an interactive session with the maintainer.
Design correction: skip the repository column, alternate the repository with an exact SHA set
Reading the approved-revisions lib closely, step 1 as proposed above would teach the guard to accept a .github-prefixed subject while the approved set still cannot say which repository each SHA belongs to. That widens what the guard admits without recording why, so it should not ship on its own.
A simpler design removes the need for a repository column:
Each narrowed subject stays one regex, alternating only the repository: ^https://github\.com/devantler-tech/(actions|\.github)/\.github/workflows/publish-<x>\.yaml@(<sha>|<sha>)$.
The SHA set stays exact. A commit SHA cannot be produced in another repository on purpose (that would need a second preimage), and both repositories belong to this organization, so (actions|\.github) admits no signer the exact set did not already admit.
A consumer mid-switch, with its signer on actions@A and its pin on .github@B, is covered by the same subject with no special case.
The TSV keeps its eight columns, so the tenant repositories' own pin guards need only accept the .github path in their uses: lines, not a new column.
What it changes:
The writer emits the alternated form for every consumer, and the lib, report and pin guard parse it.
The generator and report read pins and signers from either repository.
The release candidate and watcher move to .github once it is releasing.
Watch-out: rewriting the live subjects moves the EKS approval fingerprint (the Go projection). That change therefore needs the fingerprint re-approval path, and must land as its own reviewed step, not mixed with anything else.
I'm leaving the implementation for a dedicated session rather than squeezing it between review rounds.
🤖 Generated with Claude Code in an interactive session with the maintainer.
Interim note (2026-09-25): Dependabot's #290 auto-merged tonight. It moves cd.yaml's pin on the actions copy of publish-manifests.yaml from be13607c (13.7.0) to df7fd4f (13.10.3). The platform's approved set for github-config currently allows only be13607c. A .github release cut before the platform's regeneration picks up the new pin would be signed by df7fd4f and refused by Flux: the tenant would stay on v2.0.0 until the approved set moves, then recover by itself.
Regeneration runs several times a day and lands through a reviewed platform PR (#4206 is open now). Until it includes df7fd4f, avoid cutting a .github release. This is also one more reason to finish this issue: once cd.yaml calls this repository's own copy, Dependabot pin bumps here and the platform's approval move together by design.
Refreshed source evidence at Platform 2194d855bcf44029e398644049ec05f6a0c3dc55 and catalogue b6a20bd0b56e96ab941d4be6fe46987eb8dca8c0. Platform's approved-revision parser, generator and matching projection still accept the legacy publisher source. The catalogue CD caller also remains on that source.
The maintainer's coordinated exact-revision design remains the implementation boundary: update all projections together, complete the approval-fingerprint path, and prove deployed acceptance before repointing publishers. A prefix-only change would not deliver that contract. Release-promotion PR #381 does not replace this migration gate.
Blocker: coordinated source-identity support, deployed signature acceptance/readback and publisher repointing. This refresh reads source; it is not live Flux verification.
#235 is closed and no longer blocks this work. Actual catalogue CD succeeded at reviewed catalogue commit 0600006 while resolving legacy publish-manifests at 1d726483facb9340655489913f609082677be8f5. That is evidence for the existing publisher, not canonical publisher acceptance.
At observed Platform commit 028747a284cdcc23f274e757accf7bdd754f4ea2, the catalogue artifact's subject policy, approved-revision parser and EKS projection still accept only the legacy publishing family. Source identities and blob hashes were verified and the observation was rebound at 11:17:32 UTC. This is declared-source evidence; deployed canonical verification is unproven.
Implement the exact reviewed revisions for both families together across the policy generator, parser, writer, revision resolver, EKS projection and its fingerprint. Preserve the existing family while any publisher uses it. A prefix-only change cannot prove the approved-revision and deployed verifier contract. Require successful canonical artifact verification through the deployed consumer before catalogue CD or tenant publishers change. Blocker: coordinated dual-family implementation and actual deployed canonical acceptance. This continues to block #239 for publishing repositories.
The source-policy blocker has advanced: Platform #4568 is merged in 6de1a8b9f3cac3c4b92c7abc14ac610c1bf7eb21. A bounded live read on 2026-10-07 verifies the catalogue consumer's deployed matcher admits exactly legacy publish-manifests 1d726483facb9340655489913f609082677be8f5 and canonical publish-manifests 2fa404276b0ce5c0527683b080e39045045c4e42, under the existing GitHub OIDC issuer. Generation and observed generation agree; Ready and SourceVerified are true. The canonical-approval regression matrix passes at the integrated source, including wrong input, withdrawal and invalid-record rejection.
The currently selected public catalogue artifact is still produced by the legacy publisher. Its exact digest verifies under the deployed two-family matcher and the exact legacy identity; testing the exact canonical identity against the same artifact is rejected with a certificate identity mismatch. This proves retained legacy acceptance and deployed configuration, not native canonical publication or consumer acceptance of a canonically produced artifact.
The remaining outcome gate is an actual canonical publication at the reviewed revision followed by deployed consumer verification/readback, before publisher caller migration or compatibility retirement. This issue, #239, #284 and #371 remain open. No publisher was repointed and no verification requirement was relaxed by this read-only audit.
Canonical publication: CD run 37762433044, successful at the release commit
Signed artifact: ghcr.io/devantler-tech/github-config/manifests@sha256:6fbe25f41907416e676c07fdea2b8dbae759cc97ab6da0958bc21bdb95a30214
Independent Cosign verification passed against the exact canonical signing identity devantler-tech/.github/.github/workflows/publish-manifests.yaml@2fa404276b0ce5c0527683b080e39045045c4e42, including transparency-log and trusted-certificate validation
Live source: github-config/github-config reports 7.8.5@sha256:6fbe25f…, Ready=True, SourceVerified=True
Live consumer: github-config/github-config Kustomization has applied the same revision and reports Ready=True
The canonical publisher identity is therefore accepted by the deployed verifier and the released configuration is live. Closing as completed.
Activity
devantler commentedon Sep 25, 2026
Where
platformhard-codes theactionsidentity (static read ofmain@ 9eed5255, 2026-09-25)Live subjects, 7, each a single regex. Cosign and Kyverno take one identity entry, and
guard-oci-repository-verify.shand the approved-revisions lib both refuse a second one. So.githubcan only be added as an alternation inside the one regex, asdevantler-tech/(actions|\.github)/…. The pin guard's exactly-one-@rule rules out a whole-identity alternation.apps/{ascoachingogvaner,wedding-app,github-config}/oci-repository.yamlandproviders/hetzner/apps/aws/oci-repository.yaml.@[0-9a-f]{40}form:verify-app-images.yaml, the tenant ResourceGraphDefinition andtalos/cluster/verify-first-party-images.yaml.Scripts and Go, all single prefixes:
publish-workflow-approved-revisions.lib.sh(SUBJECT_PREFIX, plus a yqtest()).report-publish-workflow-signing-revisions.sh(SUBJECT_PATTERN, and thepin_at_refuses:regex).guard-shared-publish-workflow-pin.sh(SUBJECT_PATTERN, andEXPECTED_MIN_SUBJECTS=7).generate-publish-workflow-approved-revisions.sh(astartswithfilter, and the release-candidate lookup ondevantler-tech/actionsreleases).write-publish-workflow-matchers.sh, which rebuilds every subject from the one prefix. Left as is, it would move a.github-family consumer back toactions.watch-actions-release-candidate.sh.validate-eks-ci-role-policy/publish_matcher_projection.go, which projects to theactionspattern form and so feeds the EKS approval fingerprint.Approved-revisions TSV: it has no repository column, so every SHA is implicitly an
actionsSHA. Tenant repos that sign in their own CI read its header and columns, so adding a column is a cross-repository change.Proposed order
devantler-tech/.github/.github/workflows/publish-*), and the writer keeps each consumer's recorded family. The 7 live subjects, the TSV and the release resolver stay unchanged, so nothing is accepted live yet. Add.githubfixtures to each test suite: a SHA is accepted and the family is kept;@main,devantler-tech/github/anddevantler-tech/actionsx/are refused.(actions|\.github)/…@(A|B)form. A consumer mid-switch needsactions@signerand.github@pinat once..githubrevision once feat!: import the devantler-tech/actions catalogue #243 has merged and released, then widen the live subjects consumer by consumer as Repoint the remaining consumers from actions to .github #239 repoints each one. The EKS fingerprint re-approval follows the Go projection change.github-configmust call its own publish workflow by full SHA (devantler-tech/.github/.github/workflows/publish-manifests.yaml@<sha>), never through a local./call. A local call signs with a ref, which the pin guard refuses..githubreleases.devantler commentedon Sep 25, 2026
Design correction: skip the repository column, alternate the repository with an exact SHA set
Reading the approved-revisions lib closely, step 1 as proposed above would teach the guard to accept a
.github-prefixed subject while the approved set still cannot say which repository each SHA belongs to. That widens what the guard admits without recording why, so it should not ship on its own.A simpler design removes the need for a repository column:
^https://github\.com/devantler-tech/(actions|\.github)/\.github/workflows/publish-<x>\.yaml@(<sha>|<sha>)$.(actions|\.github)admits no signer the exact set did not already admit.actions@Aand its pin on.github@B, is covered by the same subject with no special case..githubpath in theiruses:lines, not a new column.What it changes:
.githubonce it is releasing.Watch-out: rewriting the live subjects moves the EKS approval fingerprint (the Go projection). That change therefore needs the fingerprint re-approval path, and must land as its own reviewed step, not mixed with anything else.
I'm leaving the implementation for a dedicated session rather than squeezing it between review rounds.
devantler commentedon Sep 25, 2026
Interim note (2026-09-25): Dependabot's #290 auto-merged tonight. It moves
cd.yaml's pin on theactionscopy ofpublish-manifests.yamlfrombe13607c(13.7.0) todf7fd4f(13.10.3). The platform's approved set forgithub-configcurrently allows onlybe13607c. A.githubrelease cut before the platform's regeneration picks up the new pin would be signed bydf7fd4fand refused by Flux: the tenant would stay onv2.0.0until the approved set moves, then recover by itself.Regeneration runs several times a day and lands through a reviewed platform PR (#4206 is open now). Until it includes
df7fd4f, avoid cutting a.githubrelease. This is also one more reason to finish this issue: oncecd.yamlcalls this repository's own copy, Dependabot pin bumps here and the platform's approval move together by design.ci: pin this repository's own catalogue references here instead of de…
devantler commentedon Oct 2, 2026
Refreshed source evidence at Platform
2194d855bcf44029e398644049ec05f6a0c3dc55and catalogueb6a20bd0b56e96ab941d4be6fe46987eb8dca8c0. Platform's approved-revision parser, generator and matching projection still accept the legacy publisher source. The catalogue CD caller also remains on that source.The maintainer's coordinated exact-revision design remains the implementation boundary: update all projections together, complete the approval-fingerprint path, and prove deployed acceptance before repointing publishers. A prefix-only change would not deliver that contract. Release-promotion PR #381 does not replace this migration gate.
Blocker: coordinated source-identity support, deployed signature acceptance/readback and publisher repointing. This refresh reads source; it is not live Flux verification.
devantler commentedon Oct 4, 2026
Verified publisher gate — 2026-10-04
#235 is closed and no longer blocks this work. Actual catalogue CD succeeded at reviewed catalogue commit 0600006 while resolving legacy publish-manifests at 1d726483facb9340655489913f609082677be8f5. That is evidence for the existing publisher, not canonical publisher acceptance.
At observed Platform commit 028747a284cdcc23f274e757accf7bdd754f4ea2, the catalogue artifact's subject policy, approved-revision parser and EKS projection still accept only the legacy publishing family. Source identities and blob hashes were verified and the observation was rebound at 11:17:32 UTC. This is declared-source evidence; deployed canonical verification is unproven.
Implement the exact reviewed revisions for both families together across the policy generator, parser, writer, revision resolver, EKS projection and its fingerprint. Preserve the existing family while any publisher uses it. A prefix-only change cannot prove the approved-revision and deployed verifier contract. Require successful canonical artifact verification through the deployed consumer before catalogue CD or tenant publishers change. Blocker: coordinated dual-family implementation and actual deployed canonical acceptance. This continues to block #239 for publishing repositories.
devantler commentedon Oct 7, 2026
The source-policy blocker has advanced: Platform #4568 is merged in
6de1a8b9f3cac3c4b92c7abc14ac610c1bf7eb21. A bounded live read on 2026-10-07 verifies the catalogue consumer's deployed matcher admits exactly legacy publish-manifests1d726483facb9340655489913f609082677be8f5and canonical publish-manifests2fa404276b0ce5c0527683b080e39045045c4e42, under the existing GitHub OIDC issuer. Generation and observed generation agree; Ready and SourceVerified are true. The canonical-approval regression matrix passes at the integrated source, including wrong input, withdrawal and invalid-record rejection.The currently selected public catalogue artifact is still produced by the legacy publisher. Its exact digest verifies under the deployed two-family matcher and the exact legacy identity; testing the exact canonical identity against the same artifact is rejected with a certificate identity mismatch. This proves retained legacy acceptance and deployed configuration, not native canonical publication or consumer acceptance of a canonically produced artifact.
The remaining outcome gate is an actual canonical publication at the reviewed revision followed by deployed consumer verification/readback, before publisher caller migration or compatibility retirement. This issue, #239, #284 and #371 remain open. No publisher was repointed and no verification requirement was relaxed by this read-only audit.
chore: agent-claim #242 nonce=fdd89985e42bc6f5d1a7452842804c14
devantler commentedon Oct 8, 2026
Delivered through #541.
Production proof
fca583ac795d56928c5b542944b6e0963ea1e270ghcr.io/devantler-tech/github-config/manifests@sha256:6fbe25f41907416e676c07fdea2b8dbae759cc97ab6da0958bc21bdb95a30214devantler-tech/.github/.github/workflows/publish-manifests.yaml@2fa404276b0ce5c0527683b080e39045045c4e42, including transparency-log and trusted-certificate validationgithub-config/github-configreports7.8.5@sha256:6fbe25f…,Ready=True,SourceVerified=Truegithub-config/github-configKustomization has applied the same revision and reportsReady=TrueThe canonical publisher identity is therefore accepted by the deployed verifier and the released configuration is live. Closing as completed.