๐ Hi there and welcome to my 5th post on dev.to about Ruby On Rails learning. People started to ask me what were my motivations for writing articles (instead of doing 100% coding!). It's simple: I write the articles I wish I had found online. Everytime I spend one afternoon digging a particular subject, assembling various documentation and articles then I feel like I need to write it down. Both for me and for other people interested in the subject.
As a developer learning a new platform but already knowing how to program, the area where I spend the most of my time is: decision making. When you have potentially three ways to do one thing then you have to decide which way to take.
While this article is linked to Ruby On Rails, the challenge and solutions as for storing configuration is the same in all other platforms so you should keep on reading even if not using Rails.
As always, if you want to discuss and update this article with your experience and point of view, just add a comment and I'll be happy to reply.
Table of contents:
- Custom configuration: an example use case
- Problems with "environment variables for everything"
- Rails credentials
- Configuration files
- Environment variables
- Going further
Custom configuration: an example use case
Today we'll talk about environment variables, config files & credentials. People (including me!) frequently wonder: "I have this configuration need, where do I store it?". Especially in Ruby On Rails where you are offered multiple ways to store configuration information.
Let's say you have three values you want to store and access in your Rails application:
- Something that can be public: A Slack application client id
- A token that must stay private: A Slack application client secret
- The
port
your rails server runs on
Where to store all of that? Knowing that: you don't want to compromise on security, want be able to easily access the data (without parsing it) and have a clear indication in your code about the type of configuration you're dealing with. Turns out that the decision about where to store this configuration actually depends on the values it holds. Is this infrastructure configuration (port)? Public application configuration (client id)? Or private application configuration (client secret)?
First, let's revisit the often overlooked advice: "Use environment variables for everything".
Problems with "environment variables for everything"
As with every best practices, we should be careful about this one. It comes from the 12factor apps recommendations. When programming in Node.js I used to store everything in a .env
file and environment variables in production. This usually looks like:
.env
SLACK_CLIENT_ID=somevalue # manual namespacing
SLACK_CLIENT_SECRET=somevalue
ALGOLIA_API_KEY=somevalue
ALGOLIA_APPLICATION_ID=somevalue
WEB_CACHE=true # will this be a boolean in my code or a string containing 'true'?
DATA_CACHE_IN_SECONDS=60 # will this be a string in my running app? an integer?
# ... dozen of other configuration values
And the mighty parsing side (Node.js code example):
const cacheInSeconds = parseInt(process.env["DATA_CACHE_IN_S"], 10);
Not so practical.
Now when deploying, you have to decide if you should push this file to server or maybe configure your hosting provider to provide those environment variables to your production application (more decision making!). Which means deduplicating some of those variables again.
Here are the the issues of using only .env
files for configuration needs:
- the risk of committing those files
- the risk of leaking ENV variables to another service like error handling and logging.
- no parsing of booleans, integers, you have to do that yourself via a thing layer or add more dependencies to handle that
- no namespacing but manual via conventions like
ALGOLIA_
- the challenge of deploying such configuration
Here's a good writeup on some of those issues in more details: gist https://gist.github.com/telent/9742059.
So, what should we use instead of environment variables?
Rails credentials
The official documentation on Rails credentials, once you manage to find it, is well written but lacking some details. Here's the tl;dr; for you:
- Rails credentials are encrypted YAML files, the default one being config/credentials.yml.enc. Open it, you'll see it's only a big encrypted string)
- the encryption key is either a
master.key
file (automatically created and.gitignore
d when usingrails new app
) or theRAILS_MASTER_KEY
environment variable (in production you would have to create it) - Their default content is the
secret_key_base
key (different from RAILS_MASTER_KEY), used to encrypt and sign cookies for example - Since it's YAML, you automatically get namespacing and parsing included
- You can access credentials from your code via
Rails.application.credentials.slack[:client_secret]
- You can edit credentials via
rails credentials:edit
, it opens your default editor - You can have per-environment credentials or global ones. To edit environment specific credentials use
rails credentials:edit --environment production
Pretty neat!
Going back to our example, we would use Rails credentials to store the Slack application client secret
this way:
in a terminal:
rails credentials:edit
You'll notice the previous command takes some time to open your editor, because it first decrypts the file
in your editor
# content
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: ***
slack:
client_secret: '***'
Save and then in your own code, you can use this via Rails.application.credentials.slack[:client_secret]
.
But wait wait, since the master key encrypting the credential files needs to be an environment variable, how is this better than just storing secrets in environment variables?
That's a question I asked myself too. The difference is that if an attacker manages to access your environment variables, for example because it leaked on your logging service that got compromised, they would still have to access your hosting server and its filesystem to be able to use that key and read the secrets. Just like you can lose the key of your house, it doesn't mean people will be able to break into your house, because they don't know where you live.
Configuration files
You could decide to store everything in Rails credentials because it's tempting to put everything in a single place, like with environment variables. But I like to be able to tell, from the code, what's public and what's private so I don't make mistakes later on. Plus for many configuration needs, it's great to be able to easily open a file and edit its content from my editor, without having to use rails credentials:edit
every time I have the need.
For this need, you can use Rails configuration files. There are two main ways to add custom configuration:
- Edit config/environments/*.rb files and add your own namespace there
- Create
YAML
files in config/
I like to keep config/environments/*.rb files for actual Rails configuration and use other files to create application configuration. This is actually what the Rails guides suggest in the Custom Configuration section.
So in our case this means that we would use (2.) a custom YAML configuration file to store our ok-to-be-public Slack configuration.
config/slack.yml
default: &default
sign_in_scopes: identity.basic,identity.email,identity.team,identity.avatar
bot_scopes: chat:write,usergroups:read,users:read
client_id: "858087363780.858555035760"
development:
<<: *default
production:
<<: *default
Note that you have to define the various environments in the file otherwise it won't work. Here they just inherit from the default configuration, this is using YAML anchors just like the regular Rails configuration files.
Then load this file:
config/application.rb
# ... bunch of code
module MyApp
class Application < Rails::Application
# ... bunch of code
config.slack = config_for(:slack) # this line loads the config/slack.yml file and store it in this namespace
end
end
That's it! Now you can access your Slack configuration at any moment this way: Rails.configuration.slack[:sign_in_scopes]
.
One might think that this is not so great to have to use either Rails.configuration.slack[:sign_in_scopes]
or Rails.application.credentials.slack[:client_secret]
. But again, I enjoy being able to tell what's a private credential from what's just regular configuration: your call!
Environment variables
We've seen how to store sensitive credentials and regular configuration with Rails. What are environment variables good for then? Again good question, nowadays I use them for either
- Rails application PORT configuration if needed: on which port does Puma runs (the default Rails web server). See config/puma.rb in your project
- Developer setup configuration, I use overmind to easily start and manage Rails, webpacker and docker-compose via a single command. You can configure overmind via a
.env
file that is also injected, by Overmind, into your Rails application automatically (no need for a specific gem). - Any configuration that is related to the infrastructure supporting my application rather than application and business logic configuration
- Any configuration I know I am gonna edit in production, like debugging purposes with
LOG_LEVEL
,DEBUG
- Any configuration my hosting provider already manages via environment variables, like the DATABASE_URL environment variable
In general, anything truly dynamic and non sensitive could be stored in environment variables.
Going further
While I am satisfied with Rails semantics as for configuration and secrets handling, you might find some companies going even further and using dedicated software to manage configuration and secrets. Especially if you have many servers and platforms willing to share secrets and configuration. For example Vault by HashiCorp for secrets and comfygure for configuration. And there are many more secret management softwares.
๐ That's it!
๐ Thank you for reading.
Any advice as for secret and configuration handling in Rails? Add your point of view or thank you note in the comments.
If you enjoyed this post, click below to share it:
Top comments (4)
Great article!
Is there any way to have a default value with "master" credentials ?
I explain myself :
If
Rails.application.credentials.foo
isnil
on staging (config/credentials/staging.yml.enc),we look to config/credentials.ymc.enc to find the default value
BAR
Hey there, I am no more using Rails so really I dunno! You can ask on their GitHub I guess and reply here if you find something. Good luck
I created Dynaconf (for Python) which resolves everything you mentiones including yaml, toml, json files, secrets, envvars and vault services.
are there any rails alternative?
Github.com/rochacbruno/dynaconf
Hi there, I would say maybe the closest to that would be github.com/laserlemon/figaro but not sure so I hope people with experience in the Rails community will jump in also!
For now, Rails semantics are completely fine for most applications though.