Nothing Special   »   [go: up one dir, main page]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails 7.2 "secret_key_base must be a type of String" issue when running tests in parallel in a CI build #53661

Open
denisahearn opened this issue Nov 18, 2024 · 4 comments

Comments

@denisahearn
Copy link
denisahearn commented Nov 18, 2024

Ever since we upgraded our Rails app to Rails 7.2, we have been experiencing a random raise ArgumentError, "'secret_key_base' for #{Rails.env} environment must be a type of String" error in our CI/CD build when running the app's tests. This issue doesn't always occur. When it does, sometimes just restarting the build works. Other times you have to restart the build several times before you get a successful build.

The stack trace in the build log shows that when the issue happens, it happens on this line of code in railties:

https://github.com/rails/rails/blob/v7.2.1.2/railties/lib/rails/application/configuration.rb#L517

Digging into this, I found that Rails 7.2 removed support for the legacy way that secrets were managed in a Rails app, e.g. via Rails.application.secrets, leaving Rails.application.credentials as the Rails-approved way to manage secrets in your Rails app.

Here is the PR in Rails 7.2 that removed support for Rails.application.secrets.

It turns out that this PR caused some regressions that were found during beta testing of Rails 7.2, and were corrected in this PR. Most importantly, this second PR introduced the behavior that when Rails.env.local? returns true, which it does when the rails environment is test (such as when running tests in a CI build), railties will now always generate a local secret_key_base stored in a file under tmp/local_secret.txt, and not use the one that is defined in config/credentials.yml.enc, which was the behavior in Rails 7.1 when we didn't have this CI issue.

Here is the code in railties that generates the secret for local Rails environments:

def generate_local_secret
  key_file = root.join("tmp/local_secret.txt")

  unless File.exist?(key_file)
    random_key = SecureRandom.hex(64)
    FileUtils.mkdir_p(key_file.dirname)
    File.binwrite(key_file, random_key)
  end

  File.binread(key_file)
end

I believe the reason why our CI build randomly experiences the raise ArgumentError, "'secret_key_base' for #{Rails.env} environment must be a type of String" error is that the generate_local_secret method above is not thread-safe, and our CI build runs the app's tests in multiple processes using the parallel_tests gem. See below:

# Run the tests with code coverage in parallel groups to speed up things
rake parallel:drop
rake parallel:setup
CI=1 COVERAGE=1 bundle exec parallel_test spec/ --type rspec --group-by runtime --runtime-log spec/parallel_runtime_rspec.log
rake coverage:merge_report

One process must be opening the tmp/local_secret.txt file and another process must be reading the file before the first process has written to the file and closed it.

To work around this issue, we added the following to our CI build process to force the creation of the tmp/local_secret.txt file prior to running the tests in parallel:

echo $(ruby -e "require 'securerandom'; puts SecureRandom.hex(64)") > tmp/local_secret.txt

Once we made this change we have not experienced the issue with any subsequent CI builds (there have been 25+ builds).

Steps to reproduce

Run a Rails app's tests in parallel using the parallel_tests gem, making sure that two or more processes are used. The issue doesn't always happen, but happens frequently enough that it becomes annoying.

Expected behavior

The generate_local_secret method in railties should work in a use case that is using multiple processes on a build machine to run the Rails app's tests.

Here's a thought - our Rails app defines a secret_key_base in config/credentials.yml.enc for all environments, including the test environment. The behavior in versions of Rails < 7.2 of using secret_key_base in config/credentials.yml.enc if it exists worked for our use case. Could that behavior be restored?

Actual behavior

Railties occasionally fails during app startup and throws an raise ArgumentError, "'secret_key_base' for #{Rails.env} environment must be a type of String" error.

System configuration

Rails version: We are using 7.2.1.2, but started noticing this issue when we first upgraded to 7.2

Ruby version: We are using 3.3.5, but I don't believe the Ruby version has any effect on this issue

@denisahearn denisahearn changed the title Rails 7.2 "secret_key_base must be a type of String" issue when running tests in parallel in the CI build Rails 7.2 "secret_key_base must be a type of String" issue when running tests in parallel in a CI build Nov 18, 2024
@frederikspang
Copy link
Contributor
frederikspang commented Nov 18, 2024

This fails on 8.0.0 as well as master, I believe due to a race-condition in railties lib/rails/application/configuration.rb in generate_local_secret. I'm having trouble recreating on my local dev, but we're seeing this every once in a while in CI as well.

I'm working on a solution for this currently.

We could introduce a new ENV["SECRET_KEY_BASE_LOCAL"] that takes precedence for Rails.env.local? - Or just assume, given that credentials may be enviroment specific in config/credentials/#{Rails.env}.yml.enc, that if it's set there - Trust it, and generate ONLY when blank/dummy.

@skipkayhil
Copy link
Member

It sounds like there may be two things that could potentially be changed:

  • better handling of race condition if multiple processes generate_local_secret at the same time
  • restore reading from credentials in local environments (removed in 0c76f17, probably not intentionally)

I don't think a new ENV should be necessary

@frederikspang
Copy link
Contributor

I agree that reading from credentials should be optimal.

I can put together a PR for this, unless the removal IS in fact intentional.

@denisahearn
Copy link
Author

@frederikspang Thanks for looking into this. I wonder if something like this could be used to make the generate_local_secret method multi-process tolerant?

https://stackoverflow.com/questions/1461253/mutex-for-rails-processes

p8 added a commit to p8/rails that referenced this issue Nov 22, 2024
Since 0c76f17  a `tmp/local_secret.txt`
is always used for `Rails.config.secret_key_base` in the test and
development environments, where previously `ENV["SECRET_KEY_BASE"]` or
`Rails.application.credentials.secret_key_base` were used if present.

The race condition in generating the local secret file, as reported in
rails#53661, should be avoided by
setting `ENV["SECRET_KEY_BASE"]` or
`Rails.application.credentials.secret_key_base`, but this currently
doesn't work.

When ENV["SECRET_KEY_BASE"] or
`Rails.application.credentials.secret_key_base` is set for test or
development, it should be used for the `Rails.config.secret_key_base`,
instead of generating a `tmp/local_secret.txt`.
p8 added a commit to p8/rails that referenced this issue Nov 22, 2024
Since 0c76f17  a `tmp/local_secret.txt`
is always used for `Rails.config.secret_key_base` in the test and
development environments, where previously `ENV["SECRET_KEY_BASE"]` or
`Rails.application.credentials.secret_key_base` were used if present.

The race condition in generating the local secret file, as reported in
rails#53661, should be avoided by
setting `ENV["SECRET_KEY_BASE"]` or
`Rails.application.credentials.secret_key_base`, but this currently
doesn't work.

When ENV["SECRET_KEY_BASE"] or
`Rails.application.credentials.secret_key_base` is set for test or
development, it should be used for the `Rails.config.secret_key_base`,
instead of generating a `tmp/local_secret.txt`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants