IoT Shaman

Application Secrets in Node.js Production Environments

Almost all applications I have developed require storage of some secure information. Whether this information is your Facebook application secret, or your database connection information, you definitely do not want this type of information becoming public knowledge. With native json support in Node.js, it is really easy to create a config.json file containing your application configuration / secrets and then load this during startup; the bad news is that you do not want to check this information into source control. Considering most modern applications are deployed using some form of 'Continuous Integration' that pulls directly from source control, these applications will need some way of generating this information for each environment, and will need to do so in a way that is secure.

The Strategy

  • Our strategy will want to observe the following requirements:
  • Allow for local / remote configuration
  • Integrate with cloud hosting environments
  • Allow for config changes without a restart
  • Store variables in key-value pairs

Create folder structure / files

The following folders and files will need to be created in order to store the configuration data:

/config
/config/secret-map.json
/config/secrets.json
/config/config.json
/config/config-loader.js

Map your secret variables

Open the file '/config/secret-map.json' and paste the below code into the file:

{
  "db_user": "Description goes here...",
  "db_password": "Description goes here...",
  "facebook_app_secret": "Description goes here..."
}

This file will be responsible for mapping your application secrets, any variable you need to store securely will need to have a corresponding key in this file.

Configure your local secrets file

Open the file '/config/secrets.json' and paste the below code into the file:

{
  "db_user": "replace-with-value",
  "db_password": "replace-with-value",
  "facebook_app_secret": "replace-with-value"
}

This file is your local copy for development, make sure that you DO NOT CHECK THIS INTO SOURCE CONTROL. If you are using git, simply add this file to your .gitignore file. Once you have excluded this from source control, take some time to replace these dummy-values with your actual application secrets. NOTE: you will want to update your '/config/secret-map.json' file to include any variable you add to this file.

Load your non-sensitive configuration variables

Open the file '/config/config.json' and paste the below code into the file:

{
  "environments": {
    "dev": {
      "some_value": "for_dev"
    },
    "prd": {
      "some_value": "for_prd"
    }
  },
  "static": {
    "facebook_id": "replace-with-actual-value"
  }
}

This file can be referenced at anytime during runtime to retrieve variables. In the next section we will write some code to utilize the files we have just created.

Write some code to load your variables

Now that we have our files populated with data, we are ready to write a controller that handles access to the secret / config variables. Open the file '/config/config-loader.js' and paste the below code into the file:

module.exports.getSecrets = function(env, props) {
    if (env == null) {
        try {
            return require('./secrets.json');
        } catch (ex) {
            console.log(ex);
            return {};
        }
    }

    var map;
    try {
        map = require('./secret-map.json');
    } catch (ex) {
        console.log(ex);
        return {};
    }

    var keys = Object.keys(map);
    var secrets = {};
    for (var i = 0; i < keys.length; i++) {
        secrets[keys[i]] = props[keys[i]];
    }
    return secrets;
}

module.exports.getConfig = function(env) {
    try {
        var config = require('./config.json');
        if (config.static == null && config.environments == null) {
            console.log('No configuation values available');
            return {};
        }
        var new_config = config.static == null ? {} : config.static;
        var base;
        if (env == null || config.environments[env] == null) {
            base = config.environments[Object.keys(config.environments)[0]];
        } else {
            base = config.environments[env];
        }
        var keys = Object.keys(base);
        for (var i = 0; i < keys.length; i++) {
            new_config[keys[i]] = base[keys[i]];
        }
        return new_config;
    } catch(ex) {
        console.log(ex);
        return {};
    }
}

Configuring environments

The last thing we need to do is configure our environments to contain the necessary 'secret' variables. There are 2 ways that we can accomplish this using our config-loader file.

Store secret variables as environmental variables

This is the way I would recommend storing your secrets, especially if you are hosting in a PaaS cloud environment (which is the case more often than not these days). In order for this to work, you will need to set each secret variable as a key-value pair. IMPORTANT: you will also want to store a variable to note which environment this is (I will call this variable 'env_name').

Store local versions of 'secrets.json' in each environment

While I would not recommend doing it this way, if you are deploying to a server (not PaaS) it may be easier to simply create this file on each server instance and replace the values.

Wrapping it all up

If you are using environmental variables to store your secrets, all you need to load secrets at runtime is something like this:

var loader = require('./config/config-loader');
var secrets = loader.getSecrets(process.env.env_name, process.env)

When you are testing locally, the 'process.env.env_name' will be null by default, which will cause the loader script to look for a 'secrets.json' file. When in a hosted environment (qa, prod) you will want to add 'env_name' in your environmental variables. When this value is not null, it will use the 'secret-map.json' to compile your secrets from the available environmental variables.

If you are creating 'secrets.json' files in each environment, simply pass 'null' as the first parameter to 'getSecrets' and it will force the loader to only load values from 'secrets.json'.

To load your non-sensitive configuration variables at runtime, all you need to do is something like this:

var loader = require('./config/config-loader');
var secrets = loader.getConfig(process.env.env_name)

Conclusion

After reading this article you should now be ready to implement a configuration strategy that meets your needs. If you have any questions or would like clarification on anything, please reach out to us on social media.