I am using KMS, which is an aws service. I have a file in my local system called secrets.json
which contains the raw secrets and is not committed into github. There is a corresponding file called secrets.encrypted
which is committed into github and simply contains the encrypted contents of secrets.json
.
I then created some gulp scripts which I can run using my aws credentials like this:
$ AWS_PROFILE=staging npm run encrypt
This then encrypts the contents of secrets.json
and puts it into a file called secrets.encrypted
I have the inverse as well:
$ AWS_PROFILE=staging npm run decrypt
Which decrypts the contents of secrets.encrypted
and puts the decrypted contents into secrets.json
.
I also made a gulp script which I run before a publish which diffs the decrypted contents of both files and makes sure that they are the same, because since I’m not committing it into git the two files can become unsynchronized or overwritten by a merge.
When publishing I publish the encrypted secrets.encrypted
file. During runtime in my lambda it finds secrets.encrypted
and then uses the KMS api to decrypt its contents:
import AWS from 'aws-sdk'
export default function getSecrets(callback) {
let kms = new AWS.KMS()
fs.readFile(path.join(__dirname, 'secrets.encrypted'), 'utf8', (err, encrypted) => {
if (err) return callback(err)
kms.decrypt({ CiphertextBlob: new Buffer(encrypted, 'base64') }, (err, data) => {
if (err) return callback(err)
try {
let decrypted = data.Plaintext.toString('utf8')
let secrets = JSON.parse(decrypted)
callback(null, secrets)
} catch (ex) {
callback(secrets)
}
})
})
}
When looking to decrypt you don’t have to specify a key the KMS api will use the current users access rights to find and try a key until decryption is successful. In my case I have a single key in my account that I use and grant access rights to users or roles that need it and it has a well known name. You then use that key to do the encryption.
KMS keys can be found under the IAM > Encryption Keys
section in the AWS console.
From there just create a key and give it access, here is an example access policy which would let any user or role in your account use the key to encrypt / decrypt files:
{
"Version": "2012-10-17",
"Id": "example-key",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::abc123:user/jchase",
"arn:aws:iam::abc123:root"
]
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow use of the key",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
When you create a key this way it gets a Key ID and an ARN, you need to use that ARN for encryption (not decryption).
Here is my entire encrypt / decrypt set of commands for Gulp4:
// secrets.js
import fs from 'fs'
import gulp from 'gulp'
import { KMS } from 'aws-sdk'
import { account, force } from '../helpers/env'
import diff from 'variable-diff'
const arns = {
prod: 'arn:aws:kms:us-east-1:abc123:key/c6f433fa-ec75-4214-866e-fbc225df5295',
stag: 'arn:aws:kms:us-east-1:xyz987:key/abdf54ba-5f62-40f1-8e64-c82f5dfec4a4'
}
export function checkSecrets (callback) {
getDecryptedContent((err, secrets) => {
if (err) return callback(err)
if (!secrets.decrypted) return callback() // no locally decrypted secrets, fine
let result = diff(secrets.decrypted, secrets.encrypted)
if (!result.changed) return callback() // Secrets are sync'd no problem
console.log('You have un-syncrhonzied secrets in your decrypted secrets file.')
console.log('Please manually merge and then run `npm run encrypt`')
console.log(result.text)
callback()
})
}
function getDecryptedContent (callback) {
let encryptedPath = `apps/secrets.${account}.encrypted`
let decryptedPath = `apps/secrets.${account}.json`
fs.readFile(encryptedPath, 'utf8', (err, encrypted) => {
if (err && err.code !== 'ENOENT') return callback(err)
if (err) encrypted = ''
let kms = new KMS({ region: 'us-east-1' })
kms.decrypt({ CiphertextBlob: new Buffer(encrypted, 'base64') }, (err, data) => {
if (err && (err.code !== 'ValidationException' || encrypted)) return callback(err)
let encryptedPlaintext = err ? 'null' : data.Plaintext.toString('utf8')
fs.readFile(decryptedPath, 'utf8', (err, decryptedPlaintext) => {
let encryptedObj = JSON.parse(encryptedPlaintext)
let decryptedObj = err
? null
: JSON.parse(decryptedPlaintext)
callback(null, {
encryptedPath,
decryptedPath,
encrypted: encryptedObj,
decrypted: decryptedObj
})
})
})
})
}
function decrypt (callback) {
getDecryptedContent((err, secrets) => {
if (err) return callback(err)
let result = diff(secrets.decrypted, secrets.encrypted)
if (secrets.decrypted && result.changed && !force) {
console.log('Encrypted secrets differ from Unencrypted secrets, you must manually merge them or --force:')
console.log(result.text)
callback()
} else if (!secrets.decrypted || !result.changed || force) {
console.log('Writing Unencrypted secrets file...')
if (result.changed) console.log(result.text)
// Write the encrypted file contents
fs.writeFile(secrets.decryptedPath, JSON.stringify(secrets.encrypted, null, 2), callback)
}
})
}
export function encrypt (callback) {
getDecryptedContent((err, secrets) => {
if (err) return callback(err)
if (!secrets.decrypted) return callback(new Error('No decrypted secrets to write.'))
let result = diff(secrets.encrypted, secrets.decrypted)
if (result.changed && !force) {
console.log('Encrypted secrets differ from Unencrypted secrets, you must manually merge them or --force:')
console.log(result.text)
callback()
} else if (!result.changed || force) {
console.log('Writing Encrypted secrets file...')
if (result.changed) console.log(result.text)
let kms = new KMS({ region: 'us-east-1' })
let key = arns[account]
let params = {
KeyId: key,
Plaintext: new Buffer(JSON.stringify(secrets.decrypted), 'utf8')
}
kms.encrypt(params, (err, data) => {
if (err) return callback(err)
let decryptedCiphertext = data.CiphertextBlob.toString('base64')
fs.writeFile(secrets.encryptedPath, decryptedCiphertext, callback)
})
}
})
}
gulp.task('encrypt', encrypt)
gulp.task('decrypt', decrypt)