Securing the Rails session secret
The recent Rails SQL injection vulnerability sparked very large discussions. One of the discussion threads in particular caught my attention.
In one of the exploitable scenarios, the attacker must know the session secret key. This is not so problematic for proprietary apps where the session secret is kept hidden, but is problematic for many open source Rails apps because the session secret is stored in version control for all to see. Commenter Dan Kaminsky and some other people were of the opinion that this is a bug in Rails. I disagreed because I saw it as the responsibility of the developer to omit secret keys upon public distribution.
However, regardless of whose fault it is, can this problem be prevent? Rails allows storing the session secret key in version control by default for the following reasons:
- It is not a problem for the majority of the users, who write proprietary apps. The number of open source Rails apps is quite small in comparison.
- It is convenient. Requiring the user (which in production environments is the system administrator, and in development environments is the developer) to set a secret key adds one more installation step which is not user friendly.
Is there a way to avoid the secret key from leaking out upon public distribution, without making things less convenient? Let’s explore a few possibilities.
- Omit the secret key from version control, and require the user to manually generate one.
This works but is inconvenient. You can make it more convenient by adding a hypothetical
rake save_secret
task to automate it, but that’s still a manual step. It also raises the learning curve for new users. You can haverails new
automatically run it for you, but that doesn’t solve things in production. You will have to generate a secret key on the production server for every app you deploy, and modify your Capistrano script to symlink to that key on every new deploy. As authors of Phusion Passenger we dislike any kind of installation-time sit-ups. -
Omit the secret key from version control, but auto-generate a random key if missing.
The problem with this is that every time you redeploy with Capistrano, the secret key would be regenerated. This would invalidate all previous session cookies, but not in a nice way. Rails would detect invalid signatures on previous cookies and throw an exception. There are two solutions to this:
- Make Rails silently discard invalid cookies instead of throwing an exception. Users would still be logged out on every deploy, but I believe this is a minor problem for the people who can’t be bothered to do (2).
- Create the secret key on the production server once and symlink to it on every deploy, but you have to know about it and it makes your Capistrano scripts slightly larger.
- A reader said that Capistrano can be modified to generate this key and store it in the ‘shared’ directory if it doesn’t exist.
- Omit the secret key from version control, but auto-generate a non-random key if missing.
Instead of generating a random key, the key would depend on something that is unique to the system so that the key changes across different machines but not on the same machine. However secret keys are supposed to have high entropy so you will have to choose your “something unique” very carefully. What options do we have? From the top of my head, this is what I’ve come up with:
- Host name – low entropy and can be guessed.
- MAC address – it’s not inconceivable that it can be guessed.
- IP address – this is public information, so not a good idea.
- Modification time of the root filesystem – low entropy. There’s a high chance that the server was installed in the past 5 years.
- SHA-512 of all file contents in /etc – slow, and changes your key every time you modify something in /etc.
None of these are very good sources. But maybe we can generate a machine-unique property that has high entropy. The property would be stored in /etc/machine-uuid and would be world-readable because it’s only used for deriving secret keys. During startup, Rails would check for this file and tell you to run
rake secret | sudo tee /etc/machine-uuid
if it doesn’t exist. This only has to be done once per machine. machine-uuid is never supposed to be distributed outside the local machine.You don’t want all apps on the same machine to have the same HMAC key, so Rails would derive the HMAC key as follows:
hash("#{machine_uuid}-#{hostname}-#{app_name}")
where
hash
is a cryptographically secure hashing function.But let’s be paranoid for a bit. If the attacker has gained access to a random local user account then he can very easily derive the HMAC key. But in this case you probably have more things to worry about than whether the attacker can tamper session data.
-
Allow storing the secret key in version control, but use the secret key and unique machine properties to sign HMACs.
Like with (3), machine properties must have sufficient entropy. The HMAC key would be derived as follows:
hash("#{machine_uuid}-#{hostname}-#{secret_key}")
This is more secure than (3) if you’ve set proper permissions on your application files. An attacker that has obtained access to a local user account, but not the one that your app is running on, knows machine_uuid and hostname but not secret_key. Of course he can still do other nasty things so I’m not sure whether one should worry about this scenario.
-
Store a default secret key in version control, but allow customizing it through an environment variable
A reader said he prefers the following:
Rails.application.config.secret_token = ENV['SECRET_TOKEN'] || 'fallback_token_for_development'
Unfortunately this does not solve the convenience problem. It requires the administrator to know about the secret token and it requires the administrator to perform some setup.
I prefer (2) in combination with patching Rails to discard invalid cookies instead of throwing an exception. What do you think? Can you think of any other ways to make it more secure without sacrificing convenience? Are my security analyses correct? Please leave a comment here, on Hacker News or on Reddit.