Hacker News new | ask | show | jobs
by AngryParsley 4887 days ago
First: I completely agree. Keeping plaintext secrets in source control is a bad idea. Encrypting them is a good idea. If you have plaintext secrets, encrypt them now using this makefile or git-crypt. Then rotate them.

That said, this solution has a couple of issues:

1. It encrypts the entire file instead of individual secrets in the settings file. Encrypted files can't take advantage of many version control features. A small change in plaintext creates a huge diff in ciphertext. Git blame doesn't work anymore. Git diff gets a lot more spammy, since you'll see a diff for the entire settings file if there's the slightest change in it.

2. It uses symmetric key encryption. If a developer knows the password to encrypt a secret setting, they can decrypt all the other secret settings. This is true until someone rotates the passphrase and re-encrypts the file.

To fix both of these problems, I recommend using Keyczar (http://code.google.com/p/keyczar/). If you write the right wrappers, it allows you to encrypt individual settings with a public key. Decrypting them requires a private key that exists only on production servers.

At a past job (Cloudkick), sensitive things in our settings.py looked like this:

  from cloudkick.crypto_wrappers import kz_decrypt
  ...
  BORING_THING = "whatever"
  SECRET_THING = kz_decrypt("kz::xxxx....", "/path/to/private/key")
kz_decrypt did exactly what you'd think: given an encrypted string and a private key, return the decrypted string. The private key was only on production servers, so the risk of leaking a secret was minimal. The public key was in source control, so anyone could encrypt a secret. For debugging or testing, one could also replace the call to kz_decrypt with a plaintext string. I wish the code had been released. It was only 100 lines or so.

This set-up would require a some extra work for settings files that don't allow code execution. Still, once you've set it up, it's pretty close to the most secure and convenient way to store secrets.

2 comments

This is neat. I did something similar using a set of files with tight permissions deployed only on production servers. Like your solution it depended on configs being written in a scripting language. I think it was ten lines of code.

The whole reason for doing it at all was simply that MySQL doesn't support Kerberos. There's a very old ticket for that in their bug tracker.

"For everything else, there's Kerberos."

Doesn't this mean that for a sufficiently short secret, someone could run an offline attack to guess it?
Good catch. My comment was already rather long, so I didn't mention that the public key actually encrypts an AES key that encrypts the secret. A different AES key is used for each secret. Also if the secret is < 1000 bytes (I forget the exact value), it's padded with random bytes. The encrypted format is something like kz::[AES key]:[encrypted padded secret]. Both the AES key and secret bytes are base64 encoded so they don't screw up parsing or break Python string quoting/escaping.
Presumably it's padded if it's not a multiple of 16 bytes, because that's the AES blocksize, and not just some off-the-wall requirement that the data be 1000 bytes long. I'm also hoping that your encrypted format has one more field, which is an IV that changes each time the data is encrypted.
There's no IV, but the AES key changes each time you encrypt the secret. The key is random.