Skip to content

Rubygems and Bundler Best Practices Guide

Andrew Nutter-Upham edited this page Jul 19, 2024 · 11 revisions

This document aims to provide comprehensive guidance on security supply-chain best practices when using Ruby and Bundler for package management. It covers:

  1. An overview of security features in Ruby and Bundler in the context of supply-chain security
  2. Explicit recommendations for best practices
  3. Details or links to official documentation to implement these recommendations

This guide is intended to complement official Ruby and Bundler documentation, not replace it.

Table of Contents

CI Configuration

Follow the principle of least privilege for your CI configuration.

Recommendations:

  1. If using GitHub Actions, use a workflow without access to secrets and with read-only permissions where possible.
  2. Consider using the OpenSSF Scorecard Action to flag non-read permissions on your project.

Dependency Management

Evaluating New Gems

Before using a new dependency, assess its origin, trustworthiness, and security posture.

Recommendations:

  1. Be aware of typosquatting attacks. Verify the correct spelling of gem names and identify the official repository.
  2. Use tools like Ruby Security to check for known vulnerabilities in gems.
  3. Assess the gem's activity, number of contributors, and overall health on platforms like GitHub.

Gemfile Sources

In Ruby projects, dependencies are declared in the Gemfile. The Gemfile.lock file lists all direct and transitive dependencies with their exact versions.

Recommendations:

  1. Always use SSL (HTTPS) when specifying gem sources in your Gemfile. This ensures that gem downloads are encrypted and protects against man-in-the-middle attacks.

    Example of a secure Gemfile source definition:

    source 'https://rubygems.org'

    Avoid using non-SSL sources:

    # Insecure - Do not use
    source 'http://rubygems.org'
  2. If you must use a custom gem server, ensure it supports SSL and use the HTTPS URL:

    source 'https://your-custom-gem-server.com'
  3. For git sources, always use HTTPS or SSH URLs:

    gem 'rails', git: 'https://github.com/rails/rails.git'
    # or
    gem 'rails', git: '[email protected]:rails/rails.git'
  4. Regularly audit your Gemfile to ensure all sources are using secure connections.

Version Pinning in Gemfile

Proper version pinning in your Gemfile is crucial for balancing security, stability, and flexibility in your Ruby projects. The right approach can vary depending on the nature of your project and the gems you're using.

Conservative Approach

A conservative and generally safe approach to version pinning is using the ~> operator, commonly referred to as the "tilde-greater-than" operator.

This operator allows for a controlled range of version updates, providing a balance between stability and receiving important updates.

Recommendations:

Use the ~> operator for most gems. For example:

gem 'rails', '~> 6.1.0'

This allows for updates to patch versions (6.1.1, 6.1.2, etc.) but not to minor or major versions. It's equivalent to specifying '>= 6.1.0' and '< 6.2.0'.

Exceptions and Special Cases

Some gems don't follow Semantic Versioning (SemVer) and require different pinning strategies:

  1. Calendar Versioning (CalVer) versioning: Some gems use date-based version numbers . In these cases, you might pin to a specific year or month:

    gem 'footty', '~> 2024.5.10'
  2. Framework-dependent gems: Some gems version according to the major version of a related framework. For example, the activerecord-oracle_enhanced-adapter gem versions track Rails versions:

    gem 'activerecord-oracle_enhanced-adapter', '~> 7.0.0' # follows the major/minor of rails
  3. Rapidly evolving gems: For gems under active development, you might want to be more permissive:

    gem 'some_new_gem', '~> 0.2'  # allow minor version updates
  4. Very stable gems: For extremely stable gems, you might be comfortable with a more relaxed constraint:

    gem 'rake', '~> 13.0'  # allow any 13.x version

Best Practices

  1. Regularly review and update your gem versions, especially for security updates.

  2. Use bundle outdated to check for available updates and understand which gems are held back by your constraints.

  3. Consider using Dependabot or similar tools to automate version updates and security patches.

  4. For application projects, commit your Gemfile.lock to version control to ensure consistent builds across environments.

  5. For library projects (gems), specify a broader range of compatible versions in your .gemspec file to maximize compatibility, but use stricter pinning in your Gemfile for development.

  6. Always test thoroughly after updating dependencies, even patch versions.

  7. Be cautious with pre-release versions (alpha, beta, rc) in production environments.

Remember, while these guidelines provide a good starting point, the best pinning strategy can vary based on your specific project needs, the reliability of the gems you're using, and your team's capacity for managing updates.

Reproducible Installation

Ensuring reproducible installations is crucial for security and consistency.

Using a Gemfile.lock

The Gemfile.lock file lists all direct and transitive dependencies with their exact versions.

Recommendations:

  1. Always commit the Gemfile.lock file to your repository for application projects.
  2. For libraries (gems), do not commit the Gemfile.lock file. Instead, specify version constraints in the gemspec file.
  3. Use bundle install to generate or update the Gemfile.lock file.
  4. In CI and production environments, use bundle install --deployment to ensure exact dependency versions are installed.

Vendoring Dependencies

Vendoring dependencies (storing them directly in your project) is possible but not recommended for most Ruby projects due to maintenance overhead.

Maintenance

Regularly updating dependencies is crucial for security.

Recommendations:

  1. Use tools like Dependabot or Bundler-audit to automate dependency updates and security checks.
  2. Regularly run bundle update and review changes before committing.
  3. Consider using bundle outdated to check for available updates manually.

Using bundler-audit

Bundler-audit is a crucial tool for maintaining security in Ruby projects. It checks for vulnerable versions of gems in your Gemfile.lock against a database of known security advisories.

Recommendations:

  1. Install bundler-audit:

    gem install bundler-audit
    
  2. Regularly update the advisory database:

    bundle audit update
    
  3. Run bundler-audit in your project directory:

    bundle audit check
    
  4. Integrate bundler-audit into your CI/CD pipeline to catch vulnerabilities early.

  5. Address any vulnerabilities found by bundler-audit promptly. This may involve updating gems, applying patches, or finding alternative gems if updates are not available.

  6. Use the --ignore flag to temporarily ignore specific vulnerabilities if they cannot be immediately addressed, but ensure to document and revisit these:

    bundle audit check --ignore CVE-2020-12345
    
  7. Consider using the --format option to generate reports in different formats (like JSON or YAML) for integration with other tools:

    bundle audit check --format json
    

Example of integrating bundler-audit in a GitHub Actions workflow:

name: Security Audit

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * 0'

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.0.0
    - name: Install bundler-audit
      run: gem install bundler-audit
    - name: Update audit database
      run: bundle audit update
    - name: Run audit
      run: bundle audit check

By regularly using bundler-audit, you can catch and address security vulnerabilities in your dependencies early, significantly improving your project's overall security posture.

Vulnerability Disclosure

Follow the OpenSSF recommendations for vulnerability disclosure.

Recommendations:

  1. Create a SECURITY.md file in your repository with instructions for reporting vulnerabilities.
  2. When disclosing vulnerabilities, use CVE numbers and provide detailed information about affected versions and remediation steps.

Release

Account

Publishing gems on RubyGems.org requires a user account.

Recommendations:

  1. Enable multi-factor authentication (MFA) on your RubyGems.org account.
  2. Use a strong, unique password for your RubyGems.org account.

Signing and Verification

Ruby gems can be cryptographically signed to ensure integrity.

Recommendations:

  1. Sign your gems using gem cert --build [email protected].
  2. When installing gems, use the --trust-policy flag to verify signatures: gem install --trust-policy HighSecurity gem_name.

Publishing

Recommendations:

  1. Use gem push to publish your gem to RubyGems.org.
  2. For CI/CD pipelines, use RubyGems API keys with limited scope and expiration.

Private Gems

Using Private Gem Servers

To mitigate risks associated with dependency confusion attacks:

  1. Use a private gem server like Gemfury or Geminabox for internal gems.
  2. Configure Bundler to use your private gem server by adding it to your .gemrc file or using the source directive in your Gemfile.
  3. Use gem naming conventions that clearly distinguish your internal gems from public ones.

Resilience Against RubyGems Outages

Using Gemstash

Gemstash is a tool that acts as a local RubyGems server, providing a cache and proxy for RubyGems.org. By using Gemstash, you can improve the resilience of your development and deployment processes against potential RubyGems.org outages.

Benefits of Gemstash

  1. Faster gem installation: Gemstash caches gems locally, speeding up subsequent installations.
  2. Offline access: Once cached, gems can be accessed even without an internet connection.
  3. Reduced dependency on RubyGems.org: Minimizes the impact of RubyGems.org outages on your development and deployment processes.
  4. Private gem hosting: Gemstash can also host private gems, providing a centralized gem server for your organization.

Setting up Gemstash

  1. Install Gemstash:

    gem install gemstash
    
  2. Start the Gemstash server:

    gemstash start
    
  3. Configure Bundler to use Gemstash as a source. In your Gemfile, add:

    source "http://localhost:9292"
  4. Or, configure Bundler globally:

    bundle config mirror.https://rubygems.org http://localhost:9292
    

Best Practices

  1. Monitoring: Set up monitoring for your Gemstash server to ensure it's always available.

  2. Backup: Regularly backup your Gemstash cache to prevent loss of data.

  3. CI/CD Integration: Configure your CI/CD pipelines to use Gemstash for more reliable builds.

  4. Fallback mechanism: Configure your systems to fall back to RubyGems.org if Gemstash is unavailable:

    source "http://localhost:9292"
    source "https://rubygems.org"

By implementing Gemstash, you can significantly improve the reliability of your Ruby development and deployment processes, reducing your dependence on the availability of RubyGems.org.

Ruby-specific Security Considerations

Refinements vs. Monkeypatching

Ruby allows for easy modification of existing classes and modules, which can lead to security risks if not used carefully.

Recommendations:

  1. Prefer using Refinements over global monkeypatching when extending or modifying existing classes.
  2. If you must use monkeypatching, clearly document the changes and potential impacts.
  3. Be cautious when using gems that heavily rely on monkeypatching, as they can introduce unexpected behavior and potential security vulnerabilities.

Example of using Refinements:

module StringExtensions
  refine String do
    def to_slug
      downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
    end
  end
end

class MyClass
  using StringExtensions
  
  def process_title(title)
    title.to_slug
  end
end

This approach limits the scope of the modification and reduces the risk of unintended side effects.

Remember to always keep your Ruby version up to date and follow general secure coding practices to maintain the overall security of your Ruby projects.


Based on https://github.com/ossf/package-manager-best-practices/blob/main/published/npm.md