Repository navigation
publish.yml: security fixes for explicit tags, artifact hand-off and OCI checks - #6
Merged
Merged
Conversation
… secrets - tags / moving-tags: publish exactly these tags instead of the derived <sha>-<timestamp> tag. Tags are immutable unless listed in moving-tags; an existing tag with a different digest fails before any push. - platforms: build linux/amd64 and linux/arm64 on native runners, one matrix job each, merge the archives into one OCI index and sign it once. - build-args: literal KEY=VALUE build arguments. - workload-identity-provider / service-account / image may come from secrets, so public repositories do not print them in workflow logs. image-name appends a plain name to a secret image prefix. Without the new inputs the workflow behaves exactly as before.
- Reject explicit tags that start with prod- or have the derived <hex>-<number> form, so only derived tags look derived. - Download every build artifact by ID; any job in the run can replace a named artifact. - Require every blob an image references to be in the artifact before authentication, so publish cannot sign a digest it did not receive. - Remove the reference, URL and quoted text from registry errors before reading error codes; the tag name can appear in all of them. - Bound manifest, config and layout metadata reads. - Install crane with curl and the pinned checksum instead of a third-party action in the job that holds the OIDC token. - Pin the BuildKit image by digest. - lint: actionlint 1.7.12 by digest without network, zizmor 1.31.1 on all of .github. Dependabot: 7-day cooldown.
build-environment must be a valid name and must differ from environment, so the build job never receives the publish environment's secrets.
There was a problem hiding this comment.
🟡 Changes recommended
Manifest validation accepts structural-key collisions and mislabeled index blobs, allowing the workflow to skip pushing and sign digests with missing layers or child images.
2 open issues
What changed in this PR
This PR hardens the reusable image-publishing workflow’s build-to-publish boundary and artifact validation before signing.
Changes:
- Restricts explicit tags and build environments, and downloads platform artifacts by ID.
- Checks referenced OCI blobs and read sizes, strengthens registry-error parsing, and pins BuildKit and crane installation.
- Hardens lint execution, adds a Dependabot cooldown, and documents tag restrictions and branch verification.
| File | Description |
|---|---|
| README.md | Documents tag restrictions, environment separation, and branch verification. |
| .github/workflows/publish.yml | Hardens artifact hand-off, OCI validation, input checks, and tool installation. |
| .github/workflows/lint.yml | Pins and isolates actionlint; upgrades zizmor and expands its scope. |
| .github/dependabot.yml | Adds a seven-day cooldown for action updates. |
Review effort: Balanced
crane decodes OCI JSON with Go, which matches keys case-insensitively, so a manifest could show one layer list to the checks and another to crane. Reject duplicate keys and keys that fold to a known field without matching it exactly, in index.json, manifests, configs and descriptors. Annotation and label maps keep their keys as they are. The manifest blob must also be what its descriptor says: schema version 2, the same media type, and no manifests list.
There was a problem hiding this comment.
🔵 Needs a closer look
The security-sensitive artifact hand-off lacks a live caller run validating matrix-output aggregation, artifact-ID downloads on both native architectures, and subsequent OIDC authentication and signing.
No open issues 🎉
2 resolved since last review
Review effort: Balanced
DeviaVir
added a commit
that referenced
this pull request
Oct 30, 2026
publish.yml: security fixes for explicit tags, artifact hand-off and OCI checks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Security fixes after a review of #4 (released in v1.1.0) and of the repository. The diff against
maincontains only the fixes.Note:
v1has the explicit-tag issue below now. A release of these fixes tov1closes it.Fixes to #4 (live in v1.1.0)
tags, so a run in a non-production environment could publishprod-<hex>-<timestamp>(or<hex>-<timestamp>). Derived tags getprod-only in theproductionenvironment, and consumers can rely on that. Explicit tags that start withprod-(any case) or have the<hex>-<number>form are now rejected inprepare, before any build.upload-artifactwithoverwrite: true) betweenbuildandpublish.publishnow downloads every artifact by ID, as the single-platform path already did. Each matrix leg sets its own job output, andpublishchecks that each ID is numeric.publishcould sign a digest that the artifact does not contain. It took the digest fromindex.jsonand did not check that the manifest, config and layers are in the archive. If every explicit tag already pointed to that digest, nothing was pushed and the digest was still signed. Both paths now require every blob that the image references to be in the archive, with the declared size, before authentication.layersto the checks andLayersto crane, or an index blob could pose as a manifest. Duplicate keys and keys that fold to a known field without an exact match are rejected, and the manifest blob must match its descriptor.MANIFEST_UNKNOWNmade a 403 or 503 count as absent. The parser now removes the reference, the URL and quoted text first.index.jsonandoci-layoutare limited to 1 MiB.Repository hardening
build-environmentmust be a valid name and must differ fromenvironment(case-insensitive), so caller code inbuildnever gets the publish environment's secrets. No current caller sets it.publishholds the OIDC token. It no longer runs a third-party installer action: crane is downloaded withcurland checked against the same pinned release checksum.buildpins the BuildKit image by version and digest instead of the moving default, because BuildKit produces the image thatpublishsigns.lint.yml: actionlint 1.7.12 pinned by digest, with no network and a read-only mount. zizmor 1.31.1 (from 1.5.2), which now also auditsdependabot.yml.--certificate-github-workflow-reffor consumers that must accept only one branch.Validation
actionlint1.7.12: clean.zizmor 1.31.1 --min-severity low .github: no findings.prod-abc-1,PROD-x,deadbeef-20261030101010and2026-10. It acceptsv1.2.3,latest,1.2,sha-abc,abc123and a 40-character commit SHA.linux/amd64archive on both paths. It rejects an archive without the manifest, the same real archive with one layer removed, a blob with a false declared size, and an oversizedindex.json.layers/Layers, duplicatelayers, an index blob posing as a manifest (two variants),ſize/SizeandURLskeys are rejected on both paths; annotation keys that differ only by case are accepted.MANIFEST_UNKNOWNanswer, also for tags namedMANIFEST_UNKNOWNorNAME_UNKNOWN. It fails on 403, 503 andDENIED.build) needs the caller to attack its own workflow; thebuild-environmentguard covers the useful part of it.publish.ymlwith a fake identity: one amd64+arm64 call and one single-platform call.publishreceived each matrix job's artifact ID and downloaded each artifact by its ID.invalid_target, because the provider does not exist).crane push(including--indexto Artifact Registry) and cosign signing. These steps did not change in this PR, but the--indexpush from publish.yml: explicit tags, native multi-arch builds, masked identity secrets #4 has not run against Artifact Registry.