It’s generally known that, when we develop with a NodeJS framework, we can work with environment variables and define sensitive content within the “.env” file.
You can think of credentials, connection ports, API keys, and such. This blog post will dive into how exactly this works, the benefits, and best practices for NodeJS secret management.
What are the main benefits of working with ecosystem environment variables?
Working with ecosystem environment variables allows us to centralize and secure the sensitive information that our project needs to use, such as:
- the host and internal port of the HTTP server
- the API keys for connection to other services
- the Security tokens for request calls to Rest APIs
- keys to Cloud services such as AWS, GCP or Azure
- connection data to databases, and much more.
This way we will always know where our project gets this necessary information from.
Best Practices for Working with Environment Variables in NodeJS
Environment variables, in general, are included as soon as the NodeJS processes start. At this moment, we must carry out the necessary tasks to obtain the content of the .env file, whether local or remote. This can be achieved in a couple ways – either through your existing CI/CD service, by executing this task using your own process, or through an external security service, such as Jenkins, Github Actions, etc.
With the first option, our project has to obtain this information from an .env file. There are libraries like dotenv that are responsible for quickly and easily obtaining the content of our .env file, and injecting it into the NodeJS process (process.env).
How do we protect this important information?
As a first fundamental practice, and while this may seem very obvious (and I’ve seen this easily forgotten or overlooked), it is important that these files should never be shared through insecure means or channels. Examples of such channels are by sending them in emails, through chat channels, in a simple shared folder with all the company, and the like. Remember access to such secrets can compromise your entire production environment.
A second good practice is that absolutely no one on the team should be allowed to host these files in any public or private repository. Especially not in an uncontrolled environment.
And a third best practice , is that only essential people should have access to this content. Particularly content related to more critical environments, such as a pre-production or production environments.
In order to have greater control over these files and their content, they are usually stored in a secure and encrypted way in specific services. These services can be provided, for example, by the repositories themselves (Encrypted secrets from Github) or the cloud service itself (GCP’s Secret Manager), where we plan to deploy our project.
How do we control the variables and patterns of values that we put in the .env file?
Let’s take a practical case which I have encountered regularly over the course of the years as a NodeJS developer. Imagine that your software development company discovers, after several cycles of development for clients, a common and recurring pattern. The team starts thinking about code reuse and eventually this evolves into a product For sure, in this phase an implementation team will be defined that will be in charge of using this “product”, adapting it to a greater or lesser extent to the final needs, and implementing it for the end customer. Your company no longer only makes software, now it also develops, implements and sells a product.
In my opinion, as a first basic measure, these projects should start with the definition of a variable scheme, the what is known in the NodeJS world as .env.schema. In this way, all members will know the basic and generic variables, although they can be adapted to each project according to needs. This file is for informational purposes only, and solely contains a reference to the keys of the environment variables that the project requires. For each deployment environment, for example: development, QA, staging and production, these environment variables have different values.
In the table below, you can see the comparison with a schema file versus one for each environment, in this case development and production. As you can see, they all continue the guide marked by the .env.schema file, essentially our schema.
As a second basic measure, we have to invest in the training and awareness of our team of developers, to ensure that they comply with the schema. The team must always know what kind of data can or should be stored in the environment files, and which ones cannot. It is very common to find content related to HTTPS error codes, translation texts, and much more. Environment files have a specific purpose, and are not a disaster drawer in which to store all kinds of unrelated content.
As a third basic measure, we can think of implementing a small testing module to review the content of .env files locally just before each commit. In this way, we can have a first layer of security that is able to validate if the code complies with the schema and rules for that environmentA common example is the use of an unknown variable. While we have already mentioned that these environment files should not be uploaded to the repositories, it may be important to check if they are within the framework of possible environment variables as defined in the .env.schema.
A fourth measure would be to think of an added security layer just before the deployment of each environment. Developean in-house service that is integrated with your repositories and CI/CD processes that is responsible for executing the same test described above only this time outside the developer’s local environment. It’s recommended that this test is executed, just before deploying it to a web environment. This way, we can control what is published in a public web environment.
The following image is a visualization of a suggested flow that could be defined so that the team can work with the development and QA environments, without compromising the schema. As you can see, there is a small service at the end called “Environment Security Check Service” that carries out the business logic defined above.
This sounds familiar to us, doesn’t it? Yes, in part it is the basic idea of how security scanning tools, such as Snyk work. It generates an integration, for example with Github, and is able to perform security tests on each commit, merge, pull request. Imagine this flowchart, since the developer uploads the code, the QA team reviews and accepts it and begins to pass code quality tools, unit tests, security tests, and why not, rule patterns for the previously deployed .env.
Security is one of the most important and critical points when developing software. We work with sensitive data regarding infrastructure, links to external services, and even end-user data that, under no circumstance, may be compromised. For this reason, we must apply all possible efforts to ensure that these layers of software and content are properly secured.