secure-config – easy and safe NodeJS configuration management

A convenient npm package to handle multi-environment NodeJS configurations with encrypted secrets.

secure-config-picture

Certainly you will have faced the situation that you need a lean and secure configuration management in your NodeJS app. “Lean” in the sense that you neither want to spend much time on it nor that you want to oversize your code for that. “Secure” in the sense of being able to carry confidential information like password and credentials without exposing them.

To meet that requirements we created the npm package secure-config. It is an easy to use and very basic configuration-management package. According to KISS principle it offers nearly no options and is focused on its main purpose – easy and secure provision of configurations in every environment. Just follow some basic guidelines like a common naming convention and you are in the game!

Benefits

  • No need to “hide” your configuration files from code repos etc.
  • Strong AES-256-CBC secured configuration is still a valid, legible JSON.
  • Configuration management for different environments (dev, test, prod…)
  • No need to use 3rd party secret stores like GCP KMS, Vault or something.
  • Works perfectly on-premise, with Docker, cloud platforms like Google AppEngine etc.

Usage

Suppose you have the following JSON configuration file config.json with secret information about your database connection…

{ 
  "database": {
    "host": "127.0.0.1", 
    "user": "MySecretDbUser",
    "pass": "MySecretDbPass" 
  } 
} 

Step 1 – Install secure-config-tool [optional]

[tsmx@localhost ] npm i -g @tsmx/secure-config-tool

Note: In the explanation I will use the provided tool secure-config-tool. You can do all that without the tool as it is NodeJS crypto standard, but it’s a lot more easier so…

Step 2 – Get a secret key and export it.

[tsmx@localhost ]$ secure-config-tool genkey
df9ed9002b...
[tsmx@localhost ]$ export CONFIG_ENCRYPTION_KEY=df9ed9002b...

Step 3 – Encrypt your configuration JSON values and generate a new, secure configuration file.

[tsmx@localhost ]$ secure-config-tool create-file config.json > conf/config.json
[tsmx@localhost ]$ cat conf/config.json
{ 
  "database": {
    "host": "127.0.0.1", 
    "user": "ENCRYPTED|50ceed2f97223100fbdf842ecbd4541f|df9ed9002bfc956eb14b1d2f8d960a11",
    "pass": "ENCRYPTED|8fbf6ded36bcb15bd4734b3dc78f2890|7463b2ea8ed2c8d71272ac2e41761a35" 
  } 
}

The generated file should be in the conf/ subfolder of your app. For details see naming conventions.

Step 4 – Use your configuration in the code

const conf = require('@tsmx/secure-config');

function MyFunc() {
  let dbHost = conf.database.host; // = '127.0.0.1'
  let dbUser = conf.database.user; // = 'MySecretDbUser'
  let dbPass = conf.database.pass; // = 'MySecretDbPass'
  //...
}

Step 5 – Run your app using the new encrypted configuration.

[tsmx@localhost ]$ export CONFIG_ENCRYPTION_KEY=df9ed9002b...
[tsmx@localhost ]$ node app.js

File name and directory conventions

You can have multiple configuration files for different environments or stages. They are distinguished by the environment variable NODE_ENV. The basic configuration file name is config.json if this environment variable is not present. If it is present, a configuration file with the name config-[NODE_ENV].json is used. An exception will be thrown if no configuration file is found.

All configuration files must be located in a conf/ directory of the current running app, meaning a direct subdirectory of the current working directory (CWD/conf/).

Example project structure

Development stage

  • NODE_ENV: not set
  • Configuration file: conf/config.json

Production stage

  • NODE_ENV: production
  • Configuration file: conf/config-production.json

Test stage, e.g. Jest

  • NODE_ENV: test
  • Configuration file: conf/config-test.json
path-to-your-app/
├── conf/
│       ├── config.json
│       ├── config-production.json
│       └── config-test.json
├── app.js
└── package.json

Injecting the decryption key

The key for decrypting the encrypted values is derived from an environment variable named CONFIG_ENCRYPTION_KEY. You can set this variable whatever way is most suitable. Here are some examples for common use-cases.

Set/export directly in the command line.

export CONFIG_ENCRYPTION_KEY=0123456789qwertzuiopasdfghjkly

Set the key in your launch.json configuration for developing/debugging.

... 
    "env": { "CONFIG_ENCRYPTION_KEY": "0123456789qwertzuiopasdfghjklyxc" },
...

Set the key in an environment block in app.yml.

... 
env_variables:
  CONFIG_ENCRYPTION_KEY: "0123456789qwertzuiopasdfghjklyxc"
...

Note: Make sure to not include the productive app.yml deployment descriptor in your code repository to not expose the production key. In general only your deployment managers should have access to this file as it also contains other sensitive and cost-relevant information like instance sizes and scaling options.

Pass the key to the docker run command.

docker run --env CONFIG_ENCRYPTION_KEY=0123456789qwertzuiopasdfghjklyxc MYIMAGE

There’s a complete docker test example available on GitHub.

For testing with Jest I recommend to create a test key and set it globally for all tests in the jest.config.js.

process.env['CONFIG_ENCRYPTION_KEY'] = '0123456789qwertzuiopasdfghjklyxc';
module.exports = { testEnvironment: 'node' };

If your NodeJS app using secure-config should run as a systemd service, set the key in the [Service] section of your service file.

...
[Service]

Environment=CONFIG_ENCRYPTION_KEY=0123456789qwertzuiopasdfghjklyxc
WorkingDirectory=/path/to/your/app
...

Note: You should also set the WorkingDirectory in the service configuration so that secure-config can find the configuration files in the conf/ subfolder correctly.

The key length must be 32 bytes! The value set in CONFIG_ENCRYPTION_KEY has to be:

  • a string of 32 characters length, or
  • a hexadecimal value of 64 characters length (= 32 bytes)

Otherwise an error will be thrown.

Examples of valid key strings:

  • 32 byte string: MySecretConfigurationKey-123$%&/
  • 32 byte hex value: 9af7d400be4705147dc724db25bfd2513aa11d6013d7bf7bdb2bfe050593bd0f

Different keys for each configuration environment are strongly recommended.

Generating encrypted entries

Option 1: secure-config-tool

For better convenience I provided a very basic secure-config-tool to easily generate the encrypted entries, process entire JSON files and getting keys.

Option 2: NodeJS crypto functions

You can simply use crypto functions from NodeJS with the following snippet to create the encrypted entries for your configuration files:

const crypto = require('crypto');
const algorithm = 'aes-256-cbc';

function encrypt(value) { 
  let iv = crypto.randomBytes(16); 
  let key = Buffer.from('YOUR_KEY_HERE'); 
  let cipher = crypto.createCipheriv(algorithm, key, iv); 
  let encrypted = cipher.update(value); 
  encrypted = Buffer.concat([encrypted, cipher.final()]); 
  return 'ENCRYPTED|' + iv.toString('hex') + '|' + encrypted.toString('hex');
}

The generated encrypted entry must always have the form: ENCRYPTED | IV | DATA.

PartDescription
ENCRYPTEDThe prefix ENCRYPTED used to identify configuration values that must be decrypted.
IVThe ciphers initialization vector (IV) that was used for encryption. Hexadecimal value.
DATAThe AES-256-CBC encrypted value. Hexadecimal value.

Under the hood

This is how secure-config works: when importing the package via require('@tsmx/secure-config') it…

  1. Retrieves the decryption key out of the environment variable CONFIG_ENCRYPTION _KEY
  2. Loads the applicable JSON configuration file.
  3. Recursively iterates the loaded JSON and decrypts all encrypted entries that were found.
  4. Returns a simple JSON object with all secrets decrypted.

Important notes / good to know:

  • Decryption is only done in-memory. Decrypted values are never persisted anywhere.
  • The CONFIG_ENCRYPTION _KEY environment variable is always required, even if the config file doesn’t contain encrypted secrets.
  • Recursion depth is not limited, encrypted entries can be used at every object level in the JSON config file.
  • At the time of writing only decryption of simple values is supported (no arrays or complete sub-objects. but properties of sub-objects). This covers most of the use-cases as sensitive data like passwords are normally all plain, nonstructured values.

Sample project

To get familiar with the use of secure-config I provided a secure-config-test project on Github. For trying out with Docker and Kubernetes a public docker image is also available on Docker-Hub.

Test

The package contains a set of unit tests with a nearly 100% coverage of the code. To run them, install or clone the package and run the tests via npm:

npm run test

To output the code coverage run:

npm run test-coverage

Also check out the current coverage stats at Coveralls.

Useful Links