Open Source Security

Bundler Lockfile Security Practices

How to use Gemfile.lock as a real security artifact: checksums, frozen mode, reproducible resolves, and what changed in Bundler 2.5's expanded lockfile format.

Nayan Dey
Senior Security Engineer
8 min read

The Gemfile.lock file has been part of Bundler since the 0.9 series back in 2010, and for most of its history it has been treated as a convenience that ensured everyone on a team saw the same gem versions. Security teams have largely ignored it except when chasing down a specific CVE. That has changed over the last two years, driven partly by the Bundler 2.5 release in December 2023 which added SHA-256 checksums to the lockfile format, and partly by the broader industry realization that pinned versions without content verification are a false sense of security. This post is about what Gemfile.lock does and does not guarantee today, how to configure it to give you real security properties, and where the remaining gaps still are.

The short version is that a modern Gemfile.lock, properly used with bundle install --frozen in CI and the checksum directive enabled, gives you a strong guarantee that the gem content installed today matches the content your build pipeline previously vetted. Older setups without the 2.5 format give you version pinning but not content pinning, which is meaningfully weaker.

What does the lockfile actually record?

A pre-2.5 Gemfile.lock records the resolved gem graph with exact version numbers, the platform targets those versions were resolved against, and the list of direct dependencies from the Gemfile. It does not record the content of the gems themselves. When you run bundle install, Bundler downloads the gems that match those version numbers and platforms from your configured source, typically RubyGems.org. If the gem at that version has been republished with different content, Bundler will happily install the new content because it has no way to detect the change.

Republication of a specific version is not common on RubyGems.org, but it has happened. The classic pattern is yank-then-republish, where a maintainer yanks a broken version and pushes a corrected one under the same version number. RubyGems.org blocks direct re-upload of the same version and requires a yank cycle, but the cycle is permitted, and a determined maintainer, or a compromised maintainer account, can use it to swap content while keeping the version string identical. This is the attack vector the content checksum addresses.

How do checksums work in Bundler 2.5?

Bundler 2.5, released December 19, 2023, added SHA-256 checksums to the lockfile format. Each resolved gem entry now includes a checksum of the gem artifact as downloaded from the source at resolve time. Subsequent bundle install operations verify the downloaded gem against the recorded checksum and fail loudly if they do not match. The lockfile section looks like:

CHECKSUMS
  rake (13.1.0) sha256=abc123...
  rack (3.0.8) sha256=def456...

This format is backwards-compatible in the sense that older Bundler versions ignore the CHECKSUMS block, so a team can adopt 2.5 on the critical path and tolerate developers on older Bundler versions for a while. The upgrade path that teams should aim for is: upgrade CI to Bundler 2.5 first, which is where the strictest verification matters, then push developers to upgrade at their own pace, then eventually enforce a minimum Bundler version in your Gemfile using required_rubygems_version.

A nuance: the checksum is per-platform. A gem with native extensions that ships separate binaries for darwin-arm64, linux-x86_64, and so on will have separate checksum entries per platform in the lockfile. Your CI and your developers need to resolve for all the platforms they care about, which is handled by bundle lock --add-platform as needed. If a platform is missing from the lockfile, Bundler falls back to downloading and verifying without a recorded checksum, which is weaker.

What does frozen mode do?

bundle install --frozen tells Bundler to refuse to modify the lockfile during install. If the Gemfile and the lockfile are out of sync, the install fails rather than silently updating the lockfile to match. This is essential for CI because it catches cases where a developer modified the Gemfile without regenerating the lockfile locally, which is a common source of surprise.

In production deployments, --frozen is equivalent to --deployment, which additionally installs gems into vendor/bundle instead of the system gem path. Both flags should be on in any environment where you care about predictable dependency resolution, and both should fail loudly when the lockfile is not self-consistent. A related flag, --no-cache, forces Bundler to re-download gems rather than using the local cache, which is useful in scenarios where you want maximum assurance that you are pulling from a verified source rather than a possibly-tainted local cache.

Reproducible resolves: the missing piece

Even with checksums, reproducing a resolve from a Gemfile on a new machine is not guaranteed to produce the same Gemfile.lock. Bundler's resolver is deterministic given identical inputs, but the inputs include the set of gems available on RubyGems.org at resolve time. If a new version of a gem is published between your first resolve and your attempted reproduction, Bundler may prefer the newer version depending on your version constraints. This is by design, not a bug, but it means that the Gemfile alone is not enough to reproduce a specific lockfile.

The practical answer is to treat Gemfile.lock as the source of truth once you have accepted it. Do not regenerate it casually. The bundle update command is what you use when you want to intentionally refresh the lockfile, and it should be a deliberate act with its own review process. bundle install should never modify the lockfile in an environment where reproducibility matters, which is what --frozen enforces.

For teams that want maximum reproducibility, the Bundler 2.5 lockfile plus a vendored copy of the gem artifacts in vendor/cache (produced by bundle cache or bundle package) is the strongest available combination. With the cache, you are not dependent on RubyGems.org being available or unchanged at install time, and the checksums give you content verification. The tradeoff is a larger repository or artifact store, typically a few hundred megabytes for a mid-sized Rails application.

How should the lockfile be reviewed?

Gemfile.lock diffs should be reviewed like any other security-sensitive change. A straightforward gem version bump usually shows up as a few lines of change: the gem version, its checksum, and possibly the resolved versions of its transitive dependencies. A diff that shows a surprising set of transitive changes is worth a second look because it may indicate a gem has pulled in new dependencies as part of an upgrade.

Large, multi-gem lockfile changes produced by bundle update without arguments are hard to review and should be avoided in normal workflow. The better pattern is targeted updates: bundle update <gem> for a specific gem, or bundle update --group <group> for a logical slice. This keeps each pull request's dependency delta small enough to actually reason about.

A useful CI check is to run bundle lock --compare against the previous lockfile on every pull request and generate a human-readable diff. The output separates direct-dependency changes from transitive ones, which dramatically cuts review time. Several open-source tools wrap this, including the lockfile-diff gem that many Rails shops have adopted.

Where do the remaining gaps sit?

The Bundler 2.5 checksum format validates that the gem content matches what was recorded at resolve time, but it does not validate that the recorded checksum matches what RubyGems.org has ever published. A compromised maintainer could republish a gem with different content, then someone in your team runs bundle update <gem> and records the new malicious checksum into your lockfile. The check in subsequent installs is only meaningful if the initial checksum was captured when the gem was known-good.

The upcoming Sigstore integration on RubyGems.org will partially address this by providing a transparency log you can cross-reference against, but it is not yet live as of mid-2024. For now, the practical defense is to couple lockfile checksums with external monitoring, bundler-audit, SBOM-based CVE tracking, typosquat monitoring, so that a surprise republication has a chance of being caught before your build pipeline anoints the new content as canonical.

How Safeguard Helps

Safeguard ingests your Gemfile.lock including the Bundler 2.5 checksum block and cross-references each resolved gem against published checksums and known vulnerability feeds. We alert when a gem in your lockfile has been republished on RubyGems.org with different content, catching the yank-and-republish attack that version-only pinning misses. Our CI integration fails builds that drop below your team's minimum Bundler version threshold, so you never accidentally lose the checksum protection by rolling back to an older toolchain. Together this lets you treat Gemfile.lock as the verifiable, reproducible artifact it is supposed to be.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.