skip to content

When you publish version 1.0 of a gem with gem push, how do RubyGems API keys, MFA and --otp work, and which gemspec metadata hardens the release?

level: seniorimportance: must knowfreq 42%

answer

  1. gem signin writes a credentials file
  2. push_rubygem scope
  3. --otp or GEM_HOST_OTP_CODE
  4. rubygems_mfa_required = "true"
  5. allowed_push_host

basics

~20 s

gem push uploads a built .gem using an API key from gem signin or GEM_HOST_API_KEY. With MFA enabled, the server demands a one-time code passed via --otp or GEM_HOST_OTP_CODE. Metadata rubygems_mfa_required and allowed_push_host harden pushes.

solid answer

~40 s

`gem signin` creates an **API key** on rubygems.org, with scopes such as `push_rubygem` or `yank_rubygem`, and stores it in the credentials file (`~/.gem/credentials`); `GEM_HOST_API_KEY` overrides it, and `--key NAME` picks a named key. `gem push hue_palette-1.0.0.gem` sends the file with that key. If the account uses **MFA**, the server rejects the request and `gem` asks for a one-time code, or shows a WebAuthn URL; `--otp 123456` or `GEM_HOST_OTP_CODE` supplies it up front, and push, yank, owner and signin all accept it. In the gemspec, `metadata["rubygems_mfa_required"] = "true"` makes rubygems.org require MFA for pushes of this gem, and `allowed_push_host` makes `gem push` refuse any other server.

code

ruby · 7 lines
ruby
Gem::Specification.new do |spec|
  spec.name    = "hue_palette"
  spec.version = "1.0.0"
  # ...
  spec.metadata["rubygems_mfa_required"] = "true"
  spec.metadata["allowed_push_host"]     = "https://rubygems.org"
end

go deeper

for a junior

Recall the two steps, gem build then gem push, and that pushing needs an API key saved by gem signin.

for a middle

Explain where the API key comes from, what scopes are, how MFA prompts for a code and how --otp or GEM_HOST_OTP_CODE supplies it.

for a senior

Harden a release: MFA on the account plus rubygems_mfa_required, narrowly scoped keys, allowed_push_host for private gems and several owners.

for a principal

Decide who may publish a team's gems and from where, balancing personal MFA-gated pushes against automated releases and key custody.

## The publishing path Publishing a gem is two commands: `gem build` writes `hue_palette-1.0.0.gem`, and `gem push hue_palette-1.0.0.gem` uploads it to the push server (rubygems.org unless told otherwise), which adds it to the index so `gem install hue_palette` can find it. Everything interesting is in **how the push is authorised**. ## API keys and scopes `gem push` authenticates with an **API key**, looked up in this order: 1. the `GEM_HOST_API_KEY` environment variable; 2. a named key chosen with `-k, --key NAME` from the credentials file; 3. a key stored for that specific host; 4. the default rubygems.org key. If there is none, `gem push` signs you in first. Signing in asks for your username or email and password and creates a new API key with **scopes**: - `index_rubygems` and `push_rubygem` are offered by default; - `yank_rubygem`, `add_owner`, `remove_owner` and `access_webhooks` can be added; - `show_dashboard` is an exclusive scope: a key that has it gets no other scope. The key is written to the **credentials file**, `~/.gem/credentials` (or the XDG data directory when that file does not exist); `gem env credentials` prints the path. If a key lacks the scope a command needs, `gem` says the existing key doesn't have that access and asks you to sign in again to add the scope. ## MFA and --otp When the account has **multi-factor authentication** enabled, the server answers a push, yank, owner change or sign-in with an unauthorised response that starts `You have enabled multifactor authentication`. `gem` then: - prints `You have enabled multi-factor authentication. Please enter OTP code.` and waits for a code from your authenticator app, or - when a security key is registered, prints a **WebAuthn** URL to visit and polls until you have verified. To avoid the prompt, pass the code up front: - `gem push hue_palette-1.0.0.gem --otp 123456` - or `GEM_HOST_OTP_CODE=123456 gem push hue_palette-1.0.0.gem` The `--otp` option exists on `gem push`, `gem yank`, `gem owner` and `gem signin`. Codes are short-lived, so a script that needs one is really an interactive release. ## Metadata that hardens a gem Two gemspec `metadata` keys change what publishing allows: | Key | Effect | |---|---| | `rubygems_mfa_required` = `"true"` | rubygems.org requires MFA for pushes of this gem, so every owner must pass a second factor | | `allowed_push_host` = a URL | `gem push` pushes there by default and refuses any other host | `allowed_push_host` protects against the opposite accident as well: a private gem whose gemspec names an internal server cannot be pushed to rubygems.org by mistake, because `gem push` stops with `... is not allowed by the gemspec, which only allows ...`. For a public gem, set it to `https://rubygems.org` or leave it out; the placeholder that `bundle gem` generates must not be left as is. ## Owners and automation - **Owners.** `gem owner hue_palette --add USER` and `--remove USER` manage who may push; these calls also need the matching `add_owner` or `remove_owner` scope and an MFA code when MFA is on. - **Several owners** mean a release does not depend on one person's account, and each owner's account should have MFA. - **Automated pushes** read the key from `GEM_HOST_API_KEY`, which a CI system stores as a secret. Give that key only `push_rubygem`, and remember that `rubygems_mfa_required` will then refuse pushes that cannot supply a code; keyless publishing from CI is a separate mechanism with its own setup. ## Habits for a 1.0 release - Enable MFA on the account and set `rubygems_mfa_required`, so a leaked API key alone cannot publish. - Keep API keys narrow: a key used for pushing does not need `add_owner`. - Never commit the credentials file or put `GEM_HOST_API_KEY` in a repository; treat it like a password. - Push from a clean checkout and a freshly built `.gem`, after inspecting its file list. - Add co-owners with `gem owner hue_palette --add ...` so a release does not depend on one person's account.

  • What happens if you push without --otp while MFA is enabled?
    The first request is rejected with an unauthorised response saying multifactor authentication is enabled. `gem` then prompts `Please enter OTP code.`, or shows a WebAuthn URL when a security key is registered, and retries the request with the code. Only non-interactive runs fail outright, which is why scripts pass `--otp` or `GEM_HOST_OTP_CODE`.
  • What does allowed_push_host protect against for an internal gem?
    Publishing it to the public rubygems.org by mistake. With `allowed_push_host` set to the internal server, `gem push` targets that server by default and refuses any other host with an error saying the host is not allowed by the gemspec, so a mistyped command cannot leak private code.

An API key is the house key and the OTP is a fresh code shown on the owner's phone for every delivery: a copied key still cannot open the door. rubygems_mfa_required is a sign on the door saying no delivery without the code, whoever holds a key.

saying these in an interview costs you the question

  • Believes a leaked API key cannot publish because the password is separate
  • Thinks --otp is an option of gem build
  • Commits ~/.gem/credentials or a GEM_HOST_API_KEY value to a repository
  • Thinks allowed_push_host only adds a host rather than restricting pushes
  • Assumes rubygems_mfa_required affects gem install for users