BytesafeDependency Firewall

RubyGems

Point Bundler and gem at your RubyGems firewall endpoint. Publishing and yanking, CI, platform-specific gems, and troubleshooting.

A RubyGems firewall endpoint sits between Bundler or gem and rubygems.org. Point either client at it, and every request passes through your rules before it's served: known vulnerabilities and malware blocked, licenses checked, new releases optionally delayed. It is not a registry of its own, and no plugin is needed: it speaks RubyGems' own protocol and fetches from rubygems.org on your behalf. The operations it covers are listed in Client operations.

The firewall fetches from rubygems.org on your behalf, so nothing you depend on has to move: no Gemfile change, and Gemfile.lock keeps naming rubygems.org, so the same lockfile still works for someone building without the firewall.

The examples use <namespace-id> and <firewall-id> placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's Setup tab shows these snippets with both values already filled in.

Mirror endpoint

https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id>

A RubyGems firewall created without upstreams gets rubygems (https://rubygems.org) as its default upstream; point it elsewhere on the firewall's Upstreams tab.

Configure Bundler

Mirroring rubygems.org routes every existing dependency through the firewall, with no Gemfile change:

bundle config set --global mirror.https://rubygems.org https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id>
bundle config set --global https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id>/ <your token>

Then install as usual:

bundle install

Do not set a fallback_timeout on the mirror. It is off by default, and any non-zero value lets Bundler race the firewall against rubygems.org and use whichever answers first, which silently routes around every rule.

gem install without Bundler

Pass the firewall as the source on the command itself, with the token in the URL:

gem install <gem> --clear-sources --source https://bytesafe:<your token>@eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id>/

Use --clear-sources --source, not gem sources --add. Adding a source makes RubyGems, through 4.0, also fetch specs.4.8.gz, the legacy Marshal index, which this firewall does not serve; passing the source per command uses the compact index instead, which it does serve.

Verify

bundle fetch <gem>

If a rule matched along the way, it has a firewall log entry:

bsfw logs --firewall <firewall-id>

CI

Use a Service Access Token (SAT) instead of a PAT. Set it as a secret in your CI system and build the Bundler and gem install commands above from it.

To correlate all requests from one pipeline run, append an interaction ID to the token:

bundle config set --global https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id>/ "${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}"

The ID shows up on each log entry generated by a matching logging rule or exception from that run.

Publishing

gem push and gem yank go through the firewall's upload rules, which is where secrets scanning applies. Neither reads Bundler's stored credentials: both need the token in GEM_HOST_API_KEY or ~/.gem/credentials, so a client set up for Bundler alone cannot authenticate to push. Interactive gem signin is not supported.

export GEM_HOST_API_KEY=<your token>
gem push --host https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id> pkg/your-gem-1.0.0.gem
gem yank --host https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/rubygems/<firewall-id> your-gem -v 1.0.0

Set a publish target first: the upstream that should receive pushes and yanks. Without one the firewall is install-only. The firewall stores nothing of its own; a publish that passes the rules is forwarded to the publish target, and that registry's reply is passed back.

There is no gem unyank on rubygems.org itself, so once a version is yanked through the firewall there is no undo through the client.

What the firewall serves

PathPurpose
/versionsThe merged catalogue of every gem and version. Never rule-filtered; see below
/info/{gem}Per-gem version list. Versions rules apply here
/gems/{full_name}.gemGem download. Download rules apply here
/quick/Marshal.4.8/{full_name}.gemspec.rzLegacy gemspec, served for RubyGems releases up to and including 4.0.x that still request it
/api/v1/gemsgem push. Upload rules apply here
/api/v1/gems/yankgem yank. Upload rules apply here

/versions itself is never filtered: it is a bulk catalogue every gem's existence is checked against. A rule that blocks a gem still lets it appear there; Bundler or gem then requests /info/{gem}, gets a filtered response, and resolution fails at that point instead. The end result is the same block, by a different route than the versions list itself being trimmed.

RubyGems' own account, token, owner-management, and search endpoints are outside what a firewall implements; see Client operations.

How rules apply

Each rule runs on one execution phase, and the phase decides how a block reaches the developer:

  • Versions rules remove the version from /info/{gem} before it is picked.
  • Download rules let the version be picked and then refuse the .gem file.
  • Upload rules apply to gem push and gem yank.

Gem names match case-sensitively: /info/RedCloth and /info/redcloth are different gems on rubygems.org, so a selector has to be written the way the gem is actually published. Most gems are lowercase, but exceptions like RedCloth are real gems, not typos.

versionRange selectors use RubyGems' own scheme, not semver. It looks similar but ~> is precision-sensitive: ~> 1.2 means >= 1.2, < 2.0, while ~> 1.2.3 means >= 1.2.3, < 1.3. Write a range the way you would write it in a Gemfile or .gemspec.

Platform-specific gems

A native gem can ship one build per platform, such as nokogiri publishing separate .gem files for x86_64-linux-gnu, arm64-darwin, and others under the same version number. A selector's version field and a versionRange both match on the plain version number with any platform suffix stripped, so a rule written against 1.19.4 blocks every platform build of it, not just one. Currently no way to write a rule that blocks only the musl build of a version and lets the others through; block by upstreamId or a version range instead if you need finer control.

The same stripping is what makes vulnerability and malware findings match a native build at all. An advisory is recorded against the plain version, not against 1.19.4-x86_64-linux-gnu, so without it a vulnerabilities or malware rule would silently never match a platform-specific gem.

Delay rules and publication time

A delay rule needs to know when a version was published. RubyGems' compact index carries a created_at timestamp per version, and rubygems.org backfilled it all the way back to 2009, so in practice every version on the public upstream has one. Worth checking before relying on one against a private RubyGems-compatible server: if its index leaves the field out, the firewall has no age to compare against.

Selector function support

Four selector functions are ecosystem-specific and not available for RubyGems. See the selector function matrix:

  • install-scripts is currently not available. It is implemented for npm, PyPI, and Composer.
  • deprecated is not available. RubyGems has no per-version flag for a yank: a yanked version simply disappears from /versions rather than being marked, so there is nothing for the selector to match on.
  • unlisted is implemented for NuGet only, and trust-downgrade for npm only.

Everything else applies: vulnerabilities, vulnerability-fix, malware, secrets, and license.

What a blocked install looks like

A versions rule removes the gem from /info/{gem}, so Bundler's resolver never sees it and reports that no matching version was found.

A download rule lets the version be picked and then refuses the file. The firewall answers 404, but unlike npm, Go, or Cargo, neither Bundler nor gem prints anything from the response body on this route, so a blocked gem looks like a plain failed download with no further explanation in the terminal. The descriptive message naming the request ID reaches only the firewall log:

<gem>@<version> is blocked. Check firewall log entries for request <request-id> for details

Take the firewall log to find which rule fired. See Investigate a blocked install.

An upload rule blocks a push with 403 and <gem>@<version> upload blocked, or a yank with yank of <gem>@<version> blocked; both reach gem push/gem yank's own output, since those clients do print the response body.

Troubleshooting

The token is unset, expired, or revoked, or gem install was run without --source carrying the credentials. Confirm the token is still listed and unexpired in the dashboard.

Bundler's mirror credentials and gem install's inline credentials both carry the token in a URL, so printing that config anywhere prints a live credential. Keep it out of tickets, chat, and CI logs.

If an install that should match a block rule succeeds with no matching log entry, check for a non-zero fallback_timeout on the Bundler mirror, which lets Bundler fall back to rubygems.org directly. Also check that gem install used --source rather than gem sources --add, since an added source can resolve some requests against the legacy Marshal index instead of going through the firewall's compact index route.

gem push and gem yank do not read Bundler's stored mirror credentials. Set GEM_HOST_API_KEY (or ~/.gem/credentials) separately; interactive gem signin is not supported.

Most likely a versions rule filtered it. Check the firewall logs before assuming an upstream problem, and check the gem's exact spelling: gem names match case-sensitively.

On this page