The go-to resource for upgrading Ruby, Rails, and your dependencies.

Securing Your Gemfile: How to Use Bundler Checksums to Prevent Supply Chain Attacks


In the ancient world, sensitive correspondence was often secured not by hiding the message, but by applying a wax seal stamped with a unique signet ring. This wax seal did not prevent a courier from reading or destroying the message, nor did it encrypt the contents. Rather, it provided a mechanism for tamper evidence. If the seal was broken or replaced with a forgery, the recipient immediately knew the message could no longer be trusted.

Similarly, software engineering teams face a parallel problem with the software supply chain. When we build a Ruby on Rails application, we routinely pull in dozens or even hundreds of third-party dependencies. A supply chain attack occurs when a malicious actor compromises one of these dependencies — perhaps by gaining access to a maintainer’s RubyGems account — and injects malicious code.

One may wonder: if we use a Gemfile.lock, aren’t we already protected against this? The answer is straightforward, but perhaps a bit counterintuitive. Your Gemfile.lock ensures that Bundler installs the exact same version of a gem across all environments. However, it does not inherently verify that the contents of that specific version remain unaltered from the original author’s intent. If an attacker manages to replace version 1.2.3 of a popular gem with a compromised payload of the exact same version number, a standard bundle install will download and execute the altered code.

We need a modern equivalent of the wax seal.

What Are Bundler Checksums?

To defend against these threats, we can use Bundler checksums. A checksum, strictly speaking, is a cryptographic hash generated from the contents of a file. When you download a gem, Bundler can calculate its SHA-256 hash and compare it against a known, trusted value. If the values match, the gem is authentic. If they diverge, the gem has been modified, and the installation process halts.

Bundler introduced support for this feature to help ensure the integrity of your dependencies. When enabled, Bundler records these hashes into a Gemfile.checksums file. During subsequent installations, Bundler strictly enforces that the downloaded gems match these recorded hashes.

Implementing Checksums in Your Dependency Management

Enabling checksum verification requires a deliberate update to our workflow. Before you use any of these configuration commands on a real project, however, it’s wise to ensure the latest ‘known good’ version of your Gemfile and Gemfile.lock are committed to source control.

Let’s illustrate this with a somewhat contrived directory tree. First, we’ll create our root directory and a basic Gemfile:

$ mkdir checksum-test
$ cd checksum-test
$ echo "source 'https://rubygems.org'" > Gemfile
$ echo "gem 'faker', '~> 3.2.0'" >> Gemfile

Now, we need to instruct Bundler to require checksums. There are a few ways to do this, but configuring it locally for the specific project is generally the safest starting point:

$ bundle config set --local require_checksums true

You also may notice that this creates a .bundle/config file in your directory. With this configuration active, Bundler will refuse to install any gem that does not have a corresponding, verified checksum.

Next, we need to generate our initial checksums. Let’s run a standard install:

$ bundle install
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Fetching faker 3.2.1
Installing faker 3.2.1
Bundle complete! 1 Gemfile dependency, 2 gems now installed.

When we execute this command, Bundler communicates with the RubyGems API to retrieve the official checksums for the exact versions specified in our Gemfile.lock. It then creates the Gemfile.checksums file. Let’s see if our script created it for us:

$ ls -a
.  ..  .bundle  Gemfile  Gemfile.checksums  Gemfile.lock

Indeed it did. If we inspect this file, we’ll see the cryptographic hashes for our dependencies:

$ cat Gemfile.checksums
....snip.....
faker-3.2.1 x86_64-linux:
sha256: 3a1b2c...snip...
....snip.....

(I’ve abbreviated the above file for the sake of brevity, but you will see full SHA-256 hashes in your actual project.)

You must commit this Gemfile.checksums file to your version control system alongside your Gemfile and Gemfile.lock.

$ git add Gemfile Gemfile.lock Gemfile.checksums .bundle/config
$ git commit -m "Require Bundler checksums to secure dependencies"

Handling Discrepancies and False Positives

Of course, things don’t always go perfectly. Occasionally, you might encounter a situation where a checksum verification fails during a deployment or a routine bundle install.

While this immediately flags a potential security risk, it can sometimes result from false positives. For example, corporate network proxies that aggressively cache or alter file payloads can interfere with the download, altering the calculated hash.

When a failure occurs, we must investigate the discrepancy before proceeding. It can be tempting to bypass the checksum verification to unblock a deployment — however, doing so defeats the entire purpose of the system. Instead, verify the checksum manually against the RubyGems API or the gem author’s official release notes.

RubyGems has a strict immutability policy; once a gem version is published, it generally cannot be changed without yanking it and pushing a new version. Therefore, if the checksum for an existing gem version suddenly changes, one must assume the package or the network transit has been compromised until proven otherwise.

Maintaining Security During Upgrades

The risk of supply chain compromise is particularly elevated during a major framework update, such as a Rails upgrade. As we execute a migration to a new version, we often update dozens or even hundreds of dependencies simultaneously. This high volume of change creates noise, making it easier for a compromised gem to slip through unnoticed.

By enforcing Gemfile.checksums throughout the upgrade process, we ensure that every new dependency version is verified upon introduction. This approach helps focus our post-upgrade support efforts on application stability, rather than investigating unexpected code execution from a tampered dependency.

Alternative Approaches

There are three major approaches to securing dependencies; depending on the particular circumstances you find yourself in, one of them may be more useful than the other two.

The first is relying entirely on Bundler checksums, which is what we’ve discussed here; this is my preferred method. This is most useful, in my experience, for modern Rails applications, as it strikes a good balance between security and developer friction.

The second is gem vendor caching (via bundle package or bundle cache). By committing the actual .gem files into your repository, you ensure that the exact same bytes are deployed to production. This approach is highly secure, though it can significantly bloat your repository size.

The third option is cryptographic gem signing using X.509 certificates. While RubyGems supports this, the web of trust required to make it effective has historically seen low adoption in the broader Ruby community.

Generally speaking, the first option is simpler. The second option, though, will often make more sense if your deployment environment has strict constraints on network access. Enabling require_checksums is a pragmatic first step for an engineering team looking to secure their dependency pipeline. It provides a verifiable wax seal for your application, helping ensure that the code you tested is exactly the code you deploy.

Sponsored by Durable Programming

Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.

Hire Durable Programming