Managing Secrets in Node.js With HashiCorp Vault
As the number of services grows in an organization, the problem of secret management only gets worse. Between Zero Trust and the emergence of microservices, handling secrets such as tokens, credentials, and keys has become an increasingly challenging task. That’s where a solution like HashiCorp’s Vault can help organizations solve their secret management woes.
Although there are secret management tools native to each cloud provider, using these solutions locks you in with a specific cloud provider. Vault, on the other hand, is open source and portable.
In this article, we’ll look at how HashiCorp’s Vault can help organizations manage their secrets and in turn enhance their cybersecurity posture. We’ll then set up Vault in dev mode on our machines and interact with it via its web UI and CLI. Finally, we’ll programmatically interact with Vault using Node.js.
Vault Top Features
Vault is HashiCorp’s open-source product for managing secrets and sensitive data. Here’s a list of Vault’s top features that make it a popular choice for secret management:
- Built-in concept of low trust and enforcement of security by identity
- Encryption at rest
- Several ways to authenticate against Vault, e.g., tokens, LDAP, AppRole, etc.
- Policies to govern the level of access of each identity
- Lots of secret backends, each catering to specific needs, including key-value store, Active Directory, etc.
- Support for multiple storage backends for high availability, e.g., databases (MySQL, Postgres), object stores (GCS, S3), HashiCorp’s Consul, etc.
- Ability to generate dynamic secrets, such as database credentials, cloud service account keys (Google, AWS, Azure), PKI certificates, etc.
- Built-in TTL and lease for provided credentials
- Built-in audit trail which logs every interaction with Vault
- Several ways to interact with the Vault service, including Web UI, CLI, Rest API, and programmatic access via language libraries
These features make Vault a compelling choice for cloud-based microservices architecture, where each microservice will authenticate with Vault in a distributed manner and access the secrets. The access to secrets can be managed for each individual microservice using policies following the principle of least privilege.
In the next section, we’ll set up Vault in dev mode and discuss ways to set it up in production. We’ll then configure the dev Vault instance for our hands-on demo, learning different configuration options along the way.
Setup for Hands-On Demo
We’ll use Docker to set up Vault on our local machine. Note that this setup is not production-ready. We’ll start Vault in dev mode, which uses all the insecure default configurations.
Running Vault in production isn’t easy. To do so, you can either choose HashiCorp Cloud Platform, the fully managed Vault in the cloud, or leave it to your organization’s infrastructure team to set up a secure and highly available Vault cluster.
Let’s get started.
Start Vault in Dev Mode
We’ll start the Vault service by using the official Docker image vault:1.7.3
.
If you run the container without any argument, it will start the Vault server in Dev mode by default.
docker run --name vault -p 8200:8200 vault:1.7.3
As Vault is starting, you’ll see a stream of logs. The most prominent log is a warning telling you that Vault is running in development mode:
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory and starts unsealed with a single unseal key. The root token is already authenticated to the CLI, so you can immediately begin using Vault.
If you read the message closely, you’ll notice a few things. First, it says the Vault is unsealed with a single unseal key, and second, it mentions a root token. What does this mean?
By default, when you start Vault in production mode it’s sealed, meaning you can’t interact with it yet. To get started, you’ll need to unseal it and get the unseal keys and root token to authenticate against Vault.
In case a breach is detected, the Vault server can be sealed again to protect against malicious access.
The other information that gets printed in logs is a root token, which can be used to authenticate against Vault. The option of authentication by tokens is enabled by default and the root token can be used to initiate the first interaction with Vault.
Note that if your organization’s infrastructure team has set up the Vault, they might have enabled some other authentication backends as discussed in the previous section.
Copy the root token, as we’ll use it to log in to Vault UI.
Enable KV Secret Backend
Enter your root token (copied from the previous step) and hit “Sign In.” You’ll be greeted with the following screen.
You can see that there is already a KV backend
enabled at path secret
. This comes enabled in dev mode by default.
If it is not enabled in your Vault installation, you can do so by clicking on Enable New Engine
and then selecting KV backend
and follow through the setup.
We’ll use this backend to store our secrets and then later retrieve them in the Node.js demo.
Configure AppRole Auth Method
We’ll now configure the AppRole auth method, which our Node.js application will use to retrieve the secrets from our key-value backend.
Select Access
from the top menu. You’ll see only the token
method enabled.
Click Enable New Method
and select AppRole
. Leave the settings to default and click Enable Method
.
Create Policy for Secret Access
We’ll create a policy that allows read-only access to the KV secret backend.
Select Policies
from the top menu and click Create ACL Policy
.
Enter name as readonly-kv-backend
, and enter the following content for Policy
.
path "secret/data/mysql/webapp" {
capabilities = [ "read" ]
}
Following the principle of least privilege, this policy will only give read access to secrets at the specific path.
Hit Create Policy
to save it.
Create AppRole for Node.js Application
We’re going to switch gears and use Vault CLI to finish setting up our demo. There are two ways to access Vault CLI: you can download the Vault binary, or you can exec into Vault container and access the CLI. For this demo, we’ll use the latter.
docker exec -it vault /bin/sh
We’ll then set up the VAULT_ADDR
and VAULT_TOKEN
environment variables.
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=<ROOT TOKEN>
Now let’s create an AppRole and attach our policy to this role.
vault write auth/approle/role/node-app-role \
token_ttl=1h \
token_max_ttl=4h \
token_policies=readonly-kv-backend
You should be able to see it being created successfully.
Success! Data written to: auth/approle/role/node-app-role
Each AppRole has a RoleID
and SecretID
, much like a username and password. The application can exchange this RoleID
and SecretID
for a token, which can then be used in subsequent requests.
Get RoleID and SecretID
Now we’ll fetch the RoleID
pertaining to the node-app-role via the following command:
vault read auth/approle/role/node-app-role/role-id
Next we’ll fetch the SecretID
:
vault write -f auth/approle/role/node-app-role/secret-id
Make sure you store these values somewhere safe, as we’ll use them in our Node.js application.
Please note that it’s not safe to deliver SecretID
to our applications like this. You should use response wrapping to securely deliver SecretID
to your application. For the purpose of this demo, we’ll pass SecretID
as an environment variable to our application.
Create a Secret
As the last step of our setup process, we’ll create a secret key-value pair that we will access via our Node.js application.
vault kv put secret/mysql/webapp db_name="users" username="admin" password="passw0rd"
Now that we have our setup ready, we can proceed to our Node.js application.
Manage Secrets via Node.js
In this section, we’ll see how to interact with Vault via Node.js and use the node-vault package to interact with our Vault server.
Install the node-vault
package first, if not already installed.
npm install node-vault
Before we begin, set the ROLE_ID
and SECRET_ID
environment variables to pass these values to the application.
export ROLE_ID=<role id fetched in previous section>
export SECRET_ID=<secret id fetched in previous section>
Now let’s write the sample Node application.
const vault = require("node-vault")({
apiVersion: "v1",
endpoint: "http://127.0.0.1:8200",
});
const roleId = process.env.ROLE_ID;
const secretId = process.env.SECRET_ID;
const run = async () => {
const result = await vault.approleLogin({
role_id: roleId,
secret_id: secretId,
});
vault.token = result.auth.client_token; // Add token to vault object for subsequent requests.
const { data } = await vault.read("secret/data/mysql/webapp"); // Retrieve the secret stored in previous steps.
const databaseName = data.data.db_name;
const username = data.data.username;
const password = data.data.password;
console.log({
databaseName,
username,
password,
});
console.log("Attempt to delete the secret");
await vault.delete("secret/data/mysql/webapp"); // This attempt will fail as the AppRole node-app-role doesn't have delete permissions.
};
run();
Store this script as index.js
and run it via the node index.js
command.
If everything is set up correctly, your secrets should be printed on your screen. Apart from your secrets, you’ll also see the error in deleting the secret. This confirms that our AppRole only has access to read the secret and not delete it.
Conclusion
In this article, we saw the importance of having a secret manager in a distributed systems architecture. We also learned to access Vault via Node.js applications, to retrieve secrets, and to interface with Vault via Web UI and CLI to configure it for our sample application.
From storage backends to auth backends, Vault comes with a lot of options so you can tune it perfectly to your organization’s needs. If you’re looking for a secret management solution to your microservices architecture challenges, HashiCorp’s Vault should be at the top of your list.
For our latest insights and updates, follow us on LinkedIn