Reputation: 83
I'm relatively new to Node.js and Express and I was following a guide on adding authentication using JWT to my site found here: Medium. I'm confused by one point of this guide, about using secrets, that says:
This secret will be read by JWT library while creating and validating tokens. In production, we need to store this secret in environment variable instead of a file.
In the guide, they placed the secret inside a config file. I wish the guide described why exactly the JWT secret should be an environment variable instead of in a config file, but it's pretty vague. So why should secrets be environment variables, and what are other best practices for using secrets? I've decided to use Passport & Express Sessions for authentication instead of JWT, but I'd think it still applies to session secrets.
My next question is how would I exactly set a secret in an environment variable? I'm not using the guide anymore, but how would somebody that is using it convert that config file into an environment variable? And lastly, what do you usually use environment variables for in a typical Node.js app?
Upvotes: 6
Views: 27760
Reputation: 3230
Maybe the original question was more about why you should not hard-code secrets into your codebase? Hopefully that is fairly obvious to most people. So ideally your secret and non secret "app config" should live separate from your codebase making your app "configurable". The standard way to do this:
config.js
or .env
) that is added to .gitignore
so it is never checked in.config.sample.js
or sample.env
) for new developers to use as a template for their own development configs (this is checked in and does not contain real secrets).(Yes, I know this is a controversial topic for some people. Sorry.)
You can't really prevent unprivileged users from getting access to ENV information. So if you put secrets in env, the bar is lowered for an attacker because they don't necessarily need a privileged account to see your app's secrets running under a different user.
If you are provisioning a production server, you can just as easily put your secrets in a .env
file as a systemd EnvironmentFile
using the familiar environment variable format.
However the trick is you want to treat this as secret app-loaded config rather than loading it into process environment (via systemd, bash, npm scripts, etc) or process.env
within your app.
dotenv
by default (when you call .config()
) populates process.env
. so if you want to use dotenv
securely, you can parse your .env
config using dotenv.parse
so it doesn't put the secrets from your .env
into process.env
.
This would allow you to provision a .env
file with production secrets and use a development .env
file that you add to .gitignore
so you don't commit your development file. However application developers need to remember to use .parse
instead of .config
or else it is a security risk.
Alternatively if you don't want to use dotenv
since its primary design is focused on loading up ENV... just use a json or js config file that exports a config object (e.g. config.js
). Same as .env
you can have a dev version that is not committed and a production one that is auto-provisioned. This can be required within the app where needed and avoids the risk (you have with dotenv) of accidentally populating secrets into the environment. Of course bad app code can always still leak secrets, but at least they won't be leaked by default due to how the secrets are exposed to the app.
dotenvx
supports encryption of values in on-disk .env
files (ostensibly to prevent leaks if you are checking in config/secrets and shipping the files around...just don't!)...but dotenvx
still populates process.env
with the decrypted secrets...which is not secure. Don't put production secrets in your CI, don't put any config/secrets in your codebase. Let devops manage production config/secrets.
The downside of all this is that ENV gave us a very standard way to pass config to any type of app/process/script (ie not just nodejs) in production environments. ENV being insecure means we have to come up with a (ideally standard) mechanism for passing secure config to a variety of apps and scripts in production.
In bash (as an example) you can source a .env
file and as long as those vars aren't export
ed they are just "local" variables to the current bash process (ie not inherited by sub-processes). Not using export
is akin to not populating process.env
. Parsing json config in simple bash scripts...not so fun without helper apps.
You would like application developers to not have to think about production secrets/config or security and give them a standard config retrieval mechanism that works for local development, testing and production.
You would like this standard mechanism to be straight forward and relatively low-risk of them inadvertently opening up security issues on the app side (exposing production secrets to sub-processes). Of course nothing protects you 100% as if the app has access to the secret there are many ways it could leak it (writing to file, making http calls, etc)...but at least it wouldn't be due to the config loading/passing mechanism.
We are currently looking at options for addressing this for a variety of apps in production...so very happy if someone has well thought out suggestions or experience solving this in a clean, general purpose way.
I would be inclined to stick with the .env
because we already support EnvironmentFile provisioning and every app can deal with that file format (e.g. can source
a .env
with bash)...the problem is how to prevent apps/developers from accidentally using dotenv.config instead of dotenv.parse to keep this out of the app environment.
So we are probably just sticking with config.sample.{env,json,js}
so the devlops team has a template to populate with production secrets. Config is loaded by the node app via require so it doesn't populate process.env
. We also are adding command-line flag (or ENV) to our apps to allow the location of this config file to be specified so devops has the option to place the production config file outside of the app deploy directory (which can make continuous deployment a bit easier). The downside is that a config.js won't work easily for other languages, but devops can fairly easily script converting an ENV style variable format into other targets.
As I said, keeping secrets out of your ENV is for people that are concerned about security. Many blogs/guides/tutorials online show secret passing through ENV. The popularity of dotenv
which (by default) loads secrets into the process environment also might lead you to believe this is the right way. Security is always a tradeoff between risk, effort/cost and inconvenience. Personally I think if you auto-provision production it isn't a big deal to fix this...the issue is more about coming up with a semi-standard mechanism that can work across a variety of apps/code-languages.
Upvotes: 18
Reputation: 664
Why should a secret be an environment variable, instead of being stored in a config file?
While working on projects, you will end up uploading your code on github, which is accessible to everyone. If you store your secrets in a config file, anybody with a github account will be able to read it, and hence it is a security risk. Storing the secrets as environment variables ensure their safe-keeping. Your config file should read these values from the process.env
object. Something like this:
const jwtSecretKey = process.env.JWT_SECRET_KEY;
const googleApiKey = process.env.GOOGLE_API_KEY;
const serverPort = process.env.PORT || 8000; // 8000 is the default value in case if the env variable has not been set
module.exports = {
jwtSecretKey: jwtSectetKey,
googleApiKey: googleApiKey,
serverPort: serverPort
}
And any other piece of code using these secrets should require the config file.
const config = require('./config');
...
jwt.verify(token, config.jwtSecretKey);
Along with storing secrets as environment variables, you should also store any environment specific values as environment variables. For example, say your NodeJS server (thats is connected to a hosted Database) is run on three environments - Development, QA and PROD. Each of these environments will have different information regarding the DB HOST and PORT, the environment should connect to. Environment Variables are a good way to store the DB Host and Port on each individual environment, and to use the same code across all environments to connect to different databases by reading the environment variable.
How to store the values as environment variables - One way of doing it is manually, though I wouldn't recommend it. One way is use of shell scripts. It actually depends upon your infrastructure.
You should also add your *.env to the .gitignore file So pushing it to the public will ignore the file and you have only one file with all the environment variables.
Upvotes: 8