Skip to content

build: pin the actions that can reach the publishing key - #7

Open
simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:fix/pin-workflow-actions
Open

simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:fix/pin-workflow-actions

Conversation

@simonx1

@simonx1 simonx1 commented Sep 18, 2026

Copy link
Copy Markdown

The release job carries id-token: write, so every step in it can ask GitHub for the OIDC token RubyGems trades for a short-lived publishing key. All three actions in that job were referenced by tag. A tag is a mutable pointer: whoever can move it decides what runs inside the job that can publish this gem. Trusted publishing removes the stored secret without removing that.

ruby/setup-ruby@v1 was not even a tag:

$ gh api repos/ruby/setup-ruby/git/matching-refs/tags/v1 --jq '.[] | select(.ref=="refs/tags/v1")'
   (nothing)
$ gh api repos/ruby/setup-ruby/branches/v1 --jq '.commit.sha'
a0102e0972be65f351c307e2d64b9314a57c8073

It's a branch, so the reference resolved to whatever its head was that morning.

The fix

All three pinned to commit SHAs with the version in a trailing comment for whoever bumps them next:

  • actions/checkout@11d5960… — v4.3.1
  • ruby/setup-ruby@a0102e0… — v1.324.0 (what v1 pointed at)
  • rubygems/configure-rubygems-credentials@dc5a8d8… — v2.1.0

Checkout in the release job also gets persist-credentials: false; the job never pushes, and a token left in .git/config is one more thing a later step could pick up. CI declares permissions: contents: read rather than inheriting the repository default.

One thing that is hardening, not a bug

The publish check appended everything rake release:status wrote to stdout into $GITHUB_OUTPUT. I initially wrote this up as an injection risk and it isn't — I checked, and the task emits exactly one clean line (14 bytes, nothing on stderr); bundler's warnings go to stderr (bundler/ui/shell.rb: warn → tell_err). But that line decides whether credentials are fetched and a gem is pushed, and $GITHUB_OUTPUT is parsed as key=value pairs, so the step now takes the one anchored publish= line and fails loudly if there isn't exactly one. Take it or leave it independently of the pins.

Both workflow files validate as YAML and the changed shell step parses under bash -n.


One of a series from a security and API-coverage audit. Branches are independent, each off main.

🤖 Generated with Claude Code

The release job carries id-token: write, so every step in it can ask
GitHub for the OIDC token RubyGems trades for a short-lived publishing
key. All three actions in that job were referenced by tag. A tag is a
mutable pointer: whoever can move it decides what runs inside the job
that can publish this gem, and trusted publishing removes the stored
secret without removing that.

ruby/setup-ruby@v1 was not even a tag. The repository has no v1 tag; v1
is a branch, so the reference resolved to whatever its head was that
morning -- currently a0102e0 (v1.324.0).

All three are pinned to commit SHAs with the version in a trailing
comment for whoever bumps them. Checkout in the release job also gets
persist-credentials: false; the job never pushes, and a token left in
.git/config is one more thing a later step could pick up.

CI declares permissions: contents: read rather than inheriting the
repository default.

Last, hardening rather than a fix: the publish check appended everything
rake release:status wrote to stdout into GITHUB_OUTPUT. Today that is
exactly one line -- verified, and bundler's warnings go to stderr -- so
there is no bug here. But that line decides whether credentials are
fetched and a gem is pushed, and GITHUB_OUTPUT is parsed as key=value
pairs, so the step now takes the one anchored publish= line and fails
loudly if there is not exactly one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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