serverless-kms-secrets
Version:
Serverless plugin for encrypting secrets using KMS
250 lines (225 loc) • 8.14 kB
JavaScript
;
/**
* serverless-kms-secrets
* - a plugin for for encrypting secrets using KMS with Serverless Framework
*/
const fse = require('fs-extra');
const BbPromise = require('bluebird');
const yaml = require('yamljs');
function readFile(filePath) {
return new BbPromise((resolve, reject) => {
if (!fse.existsSync(filePath)) {
reject(`No file ${filePath}`);
}
const kmsSecrets = yaml.load(filePath);
return resolve(kmsSecrets.secrets);
});
}
class kmsSecretsPlugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
encrypt: {
usage: 'Encrypt variables to file',
lifecycleEvents: [
'kmsEncrypt',
],
options: {
name: {
usage: 'Name of variable',
shortcut: 'n',
required: true,
},
value: {
usage: 'Value of variable',
shortcut: 'v',
required: true,
},
keyid: {
usage: 'KMS key Id',
shortcut: 'k',
required: false,
},
},
},
decrypt: {
usage: 'Decrypt variables from file',
lifecycleEvents: [
'kmsDecrypt',
],
options: {
name: {
usage: 'Name of variable',
shortcut: 'n',
required: false,
},
},
},
};
this.hooks = {
'encrypt:kmsEncrypt': this.encryptVariable.bind(this),
'decrypt:kmsDecrypt': this.decryptVariable.bind(this),
};
}
decrypt(secret, region, stage) {
const myModule = this;
return new Promise((success, failure) => {
myModule.serverless.getProvider('aws')
.request('KMS',
'decrypt',
{
CiphertextBlob: Buffer.from(secret, 'base64'), // eslint-disable-line new-cap
}, region, stage)
.then((data) => {
success(String(data.Plaintext));
}, failure);
});
}
encryptVariable() {
return new Promise((resolve, reject) => {
const myModule = this;
let stage = this.options.stage;
let region = this.options.region;
const parts = this.options.name.split(':');
const varname = parts[0];
const subvarname = parts[1];
let value = this.options.value;
this.serverless.service.load({
stage,
region,
})
.then((inited) => {
myModule.serverless.environment = inited.environment;
const vars = new myModule.serverless.classes.Variables(myModule.serverless);
return vars.populateService(this.options).then(() => inited);
})
.then((inited) => {
const moduleConfig = inited.custom['serverless-kms-secrets'] || {};
region = this.options.region || inited.provider.region;
stage = this.options.stage || inited.provider.stage;
const configFile =
moduleConfig.secretsFile
|| `kms-secrets.${stage}.${region}.yml`;
let kmsSecrets = {
secrets: {},
};
let keyId = this.options.keyid;
if (fse.existsSync(configFile)) {
kmsSecrets = yaml.load(configFile);
if (!keyId) {
keyId = kmsSecrets.keyArn.replace(/.*\//, '');
myModule.serverless.cli.log(`Encrypting using key ${keyId} found in ${configFile}`);
}
} else if (!this.options.keyid) {
myModule.serverless.cli.log(`No config file ${configFile} and no keyid specified`);
reject('No keyId in serverless.yml');
}
function preEncrypt() {
return new Promise((succeed) => {
if (subvarname) {
if (kmsSecrets &&
kmsSecrets.secrets &&
kmsSecrets.secrets[varname]) {
myModule.decrypt(kmsSecrets.secrets[varname], region, stage)
.then((valtext) => {
succeed(JSON.parse(valtext));
});
} else {
succeed({});
}
} else {
succeed({});
}
});
}
return preEncrypt()
.then((valstruct) => {
if (subvarname) {
const newStruct = valstruct;
newStruct[subvarname] = value;
value = JSON.stringify(newStruct);
}
myModule.serverless.getProvider('aws')
.request('KMS',
'encrypt',
{
KeyId: keyId, // The identifier of the CMK to use for encryption.
// You can use the key ID or Amazon Resource Name (ARN) of the CMK,
// or the name or ARN of an alias that refers to the CMK.
Plaintext: Buffer.from(String(value)), // eslint-disable-line new-cap
}, region, stage)
.then((data) => {
kmsSecrets.secrets[varname] = data.CiphertextBlob.toString('base64');
kmsSecrets.keyArn = data.KeyId;
fse.writeFileSync(configFile, yaml.stringify(kmsSecrets, 2));
myModule.serverless.cli.log(`Updated ${varname} to ${configFile}`);
resolve();
}, (error) => {
myModule.serverless.cli.log(error);
reject(error);
});
});
}, (error) => {
myModule.serverless.cli.log(error);
reject(error);
});
});
}
// Decrypt a variable defined in the options
decryptVariable() {
return new Promise((resolve, reject) => {
const myModule = this;
let stage = this.options.stage;
let region = this.options.region;
this.serverless.service.load({
stage,
region,
})
.then((inited) => {
myModule.serverless.environment = inited.environment;
const vars = new myModule.serverless.classes.Variables(myModule.serverless);
return vars.populateService(this.options).then(() => inited);
})
.then((inited) => {
const moduleConfig = inited.custom['serverless-kms-secrets'] || {};
stage = this.options.stage || inited.provider.stage;
region = this.options.region || inited.provider.region;
const configFile =
moduleConfig.secretsFile
|| `kms-secrets.${stage}.${region}.yml`;
myModule.serverless.cli.log(`Decrypting secrets from ${configFile}`);
readFile(configFile)
.then((secrets) => {
const names = this.options.name ? [this.options.name] : Object.keys(secrets);
names.forEach((varName) => {
const parts = varName.split(':');
const mainVarName = parts[0];
const subVarName = parts[1];
if (secrets[mainVarName]) {
myModule.decrypt(secrets[mainVarName], region, stage)
.then((secret) => {
if (subVarName) {
let varStruct = {};
if (secret) {
varStruct = JSON.parse(secret);
}
myModule.serverless.cli.log(`${varName} = ${varStruct[subVarName] || ''}`);
} else {
myModule.serverless.cli.log(`${varName} = ${secret}`);
}
}, (error) => {
myModule.serverless.cli.log(`KMS error ${error}`);
reject(error);
});
} else {
myModule.serverless.cli.log(`No secret with name ${varName}`);
resolve();
}
});
}, error => myModule.serverless.cli.log(error));
}, error => myModule.serverless.cli.log(error));
});
}
}
module.exports = kmsSecretsPlugin;