Keeping Passwords in Source Control

I learned a neat tip from my co-worker, Craig Silverstein (more on Craig joining Khan Academy), recently and I thought others might find it to be useful.

It has to deal with the eternal question: How do you store sensitive configuration options (such as usernames, passwords, etc.) in source control? Typically what I’ve done is to just punt on the problem entirely. I create a dummy configuration file, such as conf/sample-settings.json which has the basic structure but none of the details filled out. For example:

conf/sample-settings.json

// Copy to conf/settings.json
// and fill these in with your login details!
{
  "db": {
    "username": "",
    "password": ""
  }
}

If someone else needed the details I would just email it to them, or some such (not ideal). Especially when it came time to add additional information to the file or make other changes.

The technique I picked up from Craig was to, instead, keep an encrypted version of the configuration file in source control and then provide a means through which the user can encrypt and decrypt that data.

In this case you can still have the a dummy config file, if you wish.

To start you’ll want to make sure you have your source control ignore the configuration file — just to make super-sure that no one ever accidentally commits it. In Git you’d add a line like this to your .gitignore file:

.gitignore

conf/settings.json

Next you’ll want to create your actual config file and populate it with the real values.

conf/settings.json (* Do not check this in to source control!!)

{
  "db": {
    "username": "cool_guy",
    "password": "A1B2C3!"
  }
}

Finally you’ll want to create a script (I’m using a Makefile) that the user can run to encrypt and decrypt the file. This script uses OpenSSL, and specifically CAST5, to encrypt/decrypt the file. OpenSSL was chosen in particular as it worked out-of-the-box on both Linux and Mac machines.

OpenSSL reads in the appropriate files (depending upon if you’re encrypting or decrypting) then will prompt you for a password to encrypt/decrypt the file. (You’re free to use any encryption scheme that OpenSSL supports, of course.)

Makefile

.PHONY: _pwd_prompt decrypt_conf encrypt_conf

CONF_FILE=conf/settings.json

# 'private' task for echoing instructions
_pwd_prompt:
    @echo "Contact [email protected] for the password."

# to create conf/settings.json
decrypt_conf: _pwd_prompt
    openssl cast5-cbc -d -in ${CONF_FILE}.cast5 -out ${CONF_FILE}
    chmod 600 ${CONF_FILE}

# for updating conf/settings.json
encrypt_conf: _pwd_prompt
    openssl cast5-cbc -e -in ${CONF_FILE} -out ${CONF_FILE}.cast5

With all this in place the next step is simple, you’ll run:

make encrypt_conf

and you’ll enter in a password with which to encrypt the config file:

Contact [email protected] for the password.
enter cast5-cbc decryption password:

Make sure you write this down and don’t forget it — it’ll be very hard (if not impossible) to get your config file back if you forget the password.

At this point you’ll have a conf/settings.json.cast5 file and you can commit all the changes, using something like:

git add .gitignore Makefile conf/settings.json.cast5
git commit -m "Adding in an encrypted config file."

Now whenever someone downloads the code from source control they’ll need to either fill in their own values into the config file or they’ll need to get the password from you (the one you entered when you ran make encrypt_conf — or even better, use a shared password safe to manage this). Once they have the password they just run the following and enter it:

make decrypt_conf

If you ever need to update the values in the config file, it’s really straight-forward. Just update the config file, run make encrypt_conf again, and commit the new conf/settings.json.cast5 file.

One extra bit that you can add to your application, to make this process more intuitive, is a check for a missing config file and output with instructions for using the Makefile.

For example if you were using Node.js you could do:

if (!fs.existsSync("conf/settings.json")) {
  console.error("Config file [conf/settings.json] missing!");
  console.error("Did you forget to run `make decrypt_conf`?");
  process.exit(1);
}

Also, you may want to consider having a check to see if the decrypted file is out of date (which can happen if some changes were made in the source control, then were checked out, but you didn’t also run make decrypt_conf). Perhaps something like the following:

(function() {
var conf_time = fs.statSync(“conf/settings.json”).mtime.getTime();
var cast5_time = fs.statSync(“conf/settings.json.cast5”).mtime.getTime();

if (conf_time < cast5_time) { console.error("Your config file is out of date!"); console.error("You need to run `make decrypt_conf` to update it."); process.exit(1); } })();[/js] And that's it! Simpler than passing around config files manually and you still get all the benefit of using revision control to manage the file and changes.

Posted: February 6th, 2013


Subscribe for email updates

40 Comments (Show Comments)



Comments are closed.
Comments are automatically turned off two weeks after the original post. If you have a question concerning the content of this post, please feel free to contact me.


Secrets of the JavaScript Ninja

Secrets of the JS Ninja

Secret techniques of top JavaScript programmers. Published by Manning.

John Resig Twitter Updates

@jeresig / Mastodon

Infrequent, short, updates and links.