How a compressed RubyGems sign-in response leaked legacy keys
RubyGems revoked legacy API keys after a CDN caching flaw. Compression changed the response headers, and shared cache hits bypassed authentication at the origin.
A plain request could make the RubyGems sign-in endpoint look correctly configured while the real gem client exercised a vulnerable path. The difference was compression. The RubyGems security advisory describes how a gzip response could become shared cache content even though its body was a newly created API key. Another caller reaching the same CDN edge could receive that key without the origin authenticating the request again.
The advisory's timeline records revocation of legacy keys and public disclosure on July 23. Its URL and displayed article date say July 22, but the timeline identifies when these actions occurred. The root-cause fix and cache purge happened earlier, on July 9, after Luke Marshall reported the issue on July 6.
Ruby's consistency and readable object model are what I have long liked about the language. Its package infrastructure has a different responsibility from the language itself. A concise client command can cross several HTTP and middleware boundaries before it has obtained a credential. This incident matters because the behavior of those boundaries changed which account the command authenticated as.
The response body was the credential
The legacy GET /api/v1/api_key endpoint used HTTP Basic authentication to create a key and return it in a successful response. A correct origin response therefore depended on the caller's identity. Serving the body to another caller disclosed the authority represented by the key, rather than merely disclosing an account name or other descriptive data.
The advisory traces the header change through Rack middleware. Ruby's HTTP client requested gzip compression. Rack::Deflater wrapped the body in a GzipStream, which Rack::ETag could not read. The resulting response had bare Cache-Control: no-cache, without private, and no Set-Cookie. It also lacked Vary: Authorization.
Under that configuration, Fastly could cache the successful response for up to an hour at the edge node handling it. Cache hits returned the stored key without contacting the origin. The advisory says unauthenticated callers could poll the endpoint to obtain a key present in the cache. A legitimate second user could also receive the wrong key without any attacker participating.
That last case is especially useful for understanding the failure. The second client wrote the returned key into its own credentials file. Subsequent operations then carried another account's authority. The client had followed its ordinary sign-in procedure, but the shared response severed the relationship between the identity used to sign in and the credential stored locally.
Compression changed what a diagnostic request observed
The advisory says an ordinary curl request did not send the gzip header and received a response with Cache-Control: private, must-revalidate. That path looked correct. The path used by the Ruby client did not have the same headers after middleware processed the body.
The engineering lesson is specific to response transformation. An inspection at one point in the stack cannot establish what every downstream component receives. When compression changes the response representation, it can also change what later middleware can inspect and which fallback headers it selects. A client-compatible diagnostic needs to exercise the representation negotiation that selects that path.
This is why testing an authenticated endpoint only at the origin would answer an incomplete question. The origin can authenticate a request correctly while a shared cache serves a previous response without reaching it. Identity-dependent response behavior must survive the complete route to the caller, including the case where the request never reaches the application.
The fix now sets Cache-Control: private, no-store, sets Surrogate-Control: max-age=0, and appends Authorization to Vary on the key response and other authenticated API endpoints. RubyGems also purged affected Fastly objects before revoking keys. That order prevented newly issued credentials from being exposed through an old cached response during remediation.
The old client path remained a live server path
RubyGems 3.2.0 changed sign-in to POST /api/v1/api_key on December 10, 2020, when scoped keys arrived. The old GET endpoint stayed available for older clients. The advisory reports that 18 percent of gem signin requests still came from an affected client version, including the system-provided client on current macOS.
Changing the modern client did not retire the server capability. Direct callers could still reach the old endpoint. Its continued presence also meant that a locally outdated command could obtain a new legacy key long after scoped keys became available. The age of a protocol did not make the affected server route inactive.
The application-side trigger dates to the addition of Rack::Deflater on October 10, 2016. RubyGems cannot establish from the repository when the response first became cacheable at the edge. The advisory therefore treats a long exposure period as a conservative assumption. It does not claim to have reconstructed every deployment state throughout that period.
That distinction is necessary when discussing old infrastructure failures. The date a relevant application change landed is evidence about one part of the mechanism. CDN configuration and operational history are separate evidence. A precise source-code date cannot, on its own, establish the exact first vulnerable request.
Immutable releases constrained the impact
Legacy keys had broad gem-management authority and no expiry. They could publish new versions, yank versions and modify ownership or related settings across gems owned by the account. They were not full account-login credentials and could not change account passwords, email addresses or MFA settings.
The advisory explicitly states that an existing name, version and platform combination cannot be rewritten. A leaked key could publish a new higher version, but it could not replace the bytes of a previously published release. A new platform variant is a distinct artifact. These limits matter to consumers evaluating what kind of alteration was possible.
Installation was not affected by the key revocation. The immediate breakage falls on publishing and management operations using a revoked legacy key, which return HTTP 401. Automated publishing jobs can consequently fail even while dependency installation succeeds. That is the expected result of invalidating the credential used to write to the registry.
An owner investigating the incident should therefore distinguish unauthorized new artifacts from replacement of existing artifacts. The registry's immutability rule limits the latter without ruling out the former.
Revocation does not undo delegated authority
RubyGems asks account holders to examine unexpected versions, yanks, owners, trusted publishers and webhooks. The reason extends beyond whether an exposed key remains usable. An added owner or trusted publisher can preserve access after the original key is revoked. Recovery therefore needs to account for changes made using the credential.
The trusted publishing guide explains that a configured CI identity can exchange an OIDC token for a short-lived publishing token. The configuration records which repository and workflow the gem trusts. That arrangement reduces dependence on stored long-lived keys, but the authorization configuration itself needs to be correct.
I favor that model for release automation because it gives the publishing identity a defined relationship to the workflow. This incident also makes the configuration's importance concrete. A short token lifetime cannot compensate for a persistent authorization entry pointing to someone else's workflow. Credential replacement and authority review address different parts of recovery.
The API key guide describes keys limited by operation and optionally by gem. Those restrictions reduce what one credential can authorize. The advisory recommends scoped keys, MFA for API operations, and trusted publishing for CI. It says scoped keys and short-lived trusted-publishing tokens were not exposed through this legacy sign-in path.
The available logs cannot settle the whole history
RubyGems found no sign of malicious legacy-key use in retained logs. The advisory also states that the logs cover only a recent part of the possible exposure period. Actions using a key appear under the legitimate key holder, with source address and user-agent among the limited distinguishing signals. The absence of a detected incident therefore does not establish that the complete historical period was free of misuse.
The MFA setup guide distinguishes authentication levels. The advisory identifies API-enforced MFA as the setting that blocks sensitive operations using a leaked key. Protecting only interactive login does not establish the same protection for an API operation.
The response consequently combines a repaired endpoint, cache purging and revocation with a request to inspect persistent account changes. Older clients can no longer use the legacy GET sign-in route. Their users can create a scoped key through the website and load it into local configuration, while publishing automation must replace any revoked legacy credential before its next write operation can succeed.