Skip to content

Sign published images with cosign - #8

Closed
tomj12k wants to merge 2 commits into
mainfrom
security/t64-sign-published-images
Closed

Sign published images with cosign#8
tomj12k wants to merge 2 commits into
mainfrom
security/t64-sign-published-images

Conversation

@tomj12k

@tomj12k tomj12k commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Threat: T64 (Morelitea/security, Boundary 11). Severity Medium. NIST SR-4, SR-11, SI-7.
Work item: WI-12.

Why

Nothing this organization publishes is signed, so nothing downstream can tell an image this workflow built from one someone else pushed to the same tag.

initiative_infra's PLAN.md records that signing has to start in the app repos' publish workflows before Kyverno can verify anything. The admission policy is blocked on this, not the reverse — which is why this repository is a reasonable place to start.

Keyless, deliberately

cosign exchanges the job's OIDC token for a short-lived certificate from Fulcio and records the signature in the public Rekor log.

No private key exists to hold, rotate or lose. That matters here specifically: the alternative is another long-lived secret in a repository whose threat model already notes the GitHub App private key and the encryption key living as plain environment variables. Adding a third would be a poor trade for a supply-chain control.

The certificate carries the workflow identity, so a verifier can require that this repository's release workflow produced the image — rather than merely someone with push access to the registry. That is the property a signature is actually for.

Signed by digest

A tag moves; a digest is what was built. One signature covers every tag pointing at it, so latest and the semver tags all verify without signing three times.

Verifying one, once this ships

cosign verify ghcr.io/morelitea/initiative-github@sha256:… \
  --certificate-identity-regexp '^https://github.com/Morelitea/initiative-github/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Verification

Workflow parses; the job carries id-token: write; the build step exposes its digest; the generated shell passes bash -n with the Actions expressions stubbed; and every action reference including the new one is pinned to a commit SHA.

The signing itself runs only on a tag push, so it is unexercised until the next release. That is the honest limit — the first release is the test.

A note from building this

My first attempt branched from a stale local main and reported eight unpinned actions, which looked like #6 having been reverted. It had not — origin/main carries all eight pins. Rebuilt on the real tip. Worth mentioning only because "the pinning disappeared" would have been an alarming thing to report and would have been wrong.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1

tomj12k and others added 2 commits September 9, 2026 21:56
Nothing this organization publishes was signed, so nothing downstream could tell
an image this workflow built from one someone else pushed to the same tag.
initiative_infra's PLAN.md records that signing has to start in the app repos'
publish workflows before Kyverno can verify anything, which makes the admission
policy blocked on this rather than the other way round.

Keyless. cosign exchanges the job's OIDC token for a short-lived certificate from
Fulcio and records the signature in the public Rekor log, so no private key
exists to hold, rotate or lose — which matters here, because the alternative
would be another long-lived secret in a repository whose threat model already
notes two of those.

The certificate carries the workflow identity, so a verifier can require that
THIS repository's release workflow produced the image rather than merely someone
with push access to the registry. That is the property a signature is for.

Signed by digest rather than tag: a tag moves, a digest is what was built, and
one signature covers every tag pointing at it.

Verified: the workflow parses, the job carries id-token: write, the build step
exposes its digest, the generated shell passes bash -n with the Actions
expressions stubbed, and every action reference including the new one is pinned
to a commit SHA.

Traced from T64 in the estate threat model, work item WI-12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1
Two points from adversarial review, both small.

A stray blank line sat between `- uses: docker/build-push-action@…` and
`id: build`. It parses correctly — blank lines are legal inside a YAML block
mapping, and `id: build` does land on the build step — but a reader has to
verify that rather than see it. Removed.

The image name came from `cut -d: -f1`, which returns "localhost" for any
registry carrying a port. Not true of ghcr.io, so nothing was broken; it is
just a trap for whoever points this at a different registry. Now uses
${tag%:*}, which strips the tag and leaves a port alone:

  ghcr.io/morelitea/initiative-github:1.2.3 -> ghcr.io/morelitea/initiative-github
  localhost:5000/foo:1.0                    -> localhost:5000/foo

The workflow parses, the step passes `bash -n` with Actions expressions
stubbed, and permissions are contents/packages/id-token: write as intended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1
@tomj12k

tomj12k commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Production-readiness review completed with no defect found. The publish job signs the digest-qualified image using keyless cosign after pushing, and the workflow identity permissions are scoped to the job. The remaining operational trade-off is transparency-log/OIDC availability: signing failure correctly blocks publication evidence rather than silently producing an unsigned release.

@tomj12k

tomj12k commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Superseded — reopened with the finding details moved to the private security repository. This repository is public. Tracked as T64.

@tomj12k tomj12k closed this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant