Securing Node.js APIs with AWS Secrets Manager: A Step-by-Step Guide

Simon MuchemiSimon Muchemi
14 min read

If you're a web developer with experience building Node.js APIs — whether using Express or another backend framework — you've probably stored your secrets in a .env file. While this approach may seem safe at first, it actually introduces serious security risks, especially in production environments.

In this article, I’ll explain why a dedicated secrets management service like AWS Secrets Manager is a more secure and scalable solution. I’ll also walk you through how I integrated it into an application I’m currently building, called NodeSocial.

What is AWS Secrets Manager?

Just as the name suggests, AWS Secrets Manager is a service that enables you as the developer to securely store, manage, and retrieve secrets. These secrets are the same ones you would typically store in a .env file - database credentials, JWT secrets, API keys and more.

Using this service is better than relying on .env files for several important reasons:

  1. Security.

    The service stores and manages your sensitive data, ensuring that secrets aren’t exposed to unauthorized access. In contrast, environment variables stored in a .env file can be accessed by any code running in your application.

  2. Fine-grained access control.

    With AWS Secrets Manager, you have precious control over who and what can access your secrets. That is, by using AWS Identity and Access Management (IAM) roles and policies, you can limit this access with ease.

  3. Automatic secret rotation

    This means you can update secrets like database credentials on a schedule without requiring code changes, making your applications more secure and resilient.

  4. Encryption at rest

    All secrets managed by AWS Secrets Manager are encrypted using AWS Key Management Service (KMS). This ensures that even if storage is compromised, your secrets remain protected and unreadable without proper permissions.

.env FileAWS Secrets Manager
Environment variables are visible all over the application.Secrets are only visible to authorized parts of the application
No access control.Access to secrets is controlled using IAM roles and policies.
Manual secret rotation.Automated secret rotation.
No data encryption.Data encrypted using AWS KMS.

.env Files vs AWSSecrets Manager: Why Upgrade?

Storing secrets in .env files is a common practice - especially when getting started with Node.js applications. It’s simple, requires no external service, and works well in local development.

However, this approach quickly becomes risky and limited in production environments. Here’s why:

  1. Secrets are stored on disk

    Environment variables defined in .env are stored in plain text on the server’s file system. If someone gains unauthorized access to you server (physically or remotely), they can easily read those secrets. .env files are not encrypted by default.

  2. Easy to leak via version control

    It’s alarmingly common for .env files to be accidentally committed to version control systems like Git. Even if .gitignore is used, developers sometimes make mistakes, and a leaked secret in a public repository can be catastrophic.

  3. No access control

    Once your app loads the .env variables, any part of your code — or any dependency — has access to all secrets. There’s no way to restrict access to specific secrets for specific modules or processes.

  4. Secret rotation is manual and risky

    If you need to change a password, API key, or token, you must:

    1. Manually edit the .env file,

    2. Restart the application to reload the new variables,

    3. Hope nothing breaks during the process.

This makes secrets harder to manage at scale and increases the chances of human error.

Why AWS Secrets Manager is better

AWS Secret Manager address all these issues:

  • Secrets are encrypted at rest and in transit; on disk and in network.

  • You get centralized, cloud-native management.

  • Access can be restricted using IAM policies.

  • Automatic rotation can keep secrets fresh without code changes.

How I Integrated AWS Secrets Manager into my Node.js Application (Step-by-Step)

I followed the following steps in order to integrate Secrets Manager in my social media feed application; NodeSocial:

  1. Stores secrets in a new Secret Manager store

  2. Created an IAM user for local development to access the keys in my local environment.

  3. Install and setup AWS secret manager client SDK to retrieve keys.

  4. Configured an EC2 server to access the secrets in a production environment.

Creating an AWS Secrets Manager Store

Firstly, you have to create an AWS Secrets Manager store on AWS to store the various secrets that the application will need. Below I demonstrate how to create a secret store with a sample MongoDB connection URL.

  1. On your console, search ‘Secrets Manager’ and click on ‘Secrets’ in the sidebar on the left.

  2. Select the ‘Other types of secrets’ options since you might want to store more than one secret value. You can also select the database specific option.

  3. Create a key/value pair to hold MongoDB URL value as shown in the example below.

  4. Create a new secret key to secure the secrets or select an already existing on like in my case.

  5. For my case, I didn’t configure rotation but you can do so if you are handling a larger project.

  6. Review the changes and save.

Now you’ve created a Secrets Manager store and stored your first secret.

Creating and configuring an IAM user for Local Development.

To access the keys in a secure manner, you have to create an AWS user and gave it permissions to access the secrets stored.

To achieve this, I followed the following steps:

  1. On our console search for ‘IAM’ and click on ‘Users’ link on the sidebar.

  2. Click on the ‘Create User’ button to create a new user.

  3. Give the user a new name, for example I use ‘nodesocial_user’.

  4. Click on next to arrive at the permissions page. On this page, select the ‘Attach policies directly’ to create a policy that permits our user to access the secrets stored in the Secrets Manager.

  5. Click on the ‘Create policy’ button. This will take you to a new page.

  6. On the new page, the create policy page, find the search box in the services section and type ‘Secrets Manager’ to the permission allowable to the policy.

  7. In the ‘Actions Allowed’ section, toggle the ‘Read’ menu and select ‘GetSecretValue’.

  8. In a new tab, go to the Secrets Manager page and copy the ARN of the store you created earlier. In my case I named mine ‘nodesocial_secrets’.

  9. In the policy creation tab, under the ‘Resources’ menu, click on the ‘Add ARNs’ link. A popup window will be displayed. Select the ‘Text’ to display a text area. Paste the ARN copied earlier hear and click the ‘Add ARNs’ button.

  10. Click on ‘next’ and give your policy a new name. You might want to note down the name as you will need to remember it.

  11. Click on ‘Create policy’ to create the policy.

  12. Go back to the user creation tab, use the reload icon to refresh the policy list the search for the policy you just created. In my case, I named the policy ‘nodesocial_sm_demo_policy’. Select it the click on ‘next’.

  13. On the review page, click ‘Create user’ to create the new user. This will take you back to the IAM users page.

  14. Search for the user you just created. For my case, it was named ‘demo_nodesocial_user’. At this point there is no way we can access our secrets with it from our code since we don’t have an access key yet.

  15. Click on the ‘create access key’ link to create an access key.

  16. Select the ‘Local code’ use case option, tick the confirmation box and click ‘next’.

  17. You can provided a short description for the key, then click on ‘Create access key’.

  18. Be sure to download the .csv file as it won’t be accessible once you click on ‘Done’.

Setting up AWS Secrets Manager SDK

After configuring the IAM user and attaching policy to get secret values via, you can head back to you node.js codebase and set up the Secrets Manager SDK. You can follow the following steps:

  1. Copy the secrets from the .csv file downloaded earlier in a .env file for local use. The file should be at the root of your application directory. This is necessary because the SDK will check for these values before fetching secrets from AWS. For deployment using an EC2 server, you don’t need to copy these value to your server.

     AWS_ACCESS_KEY_ID=YOURSECRETACCESSKEYID
     AWS_SECRET_ACCESS_KEY=YOURSECRETACCESSKEYGOSHERE
    
  2. Install the AWS Secrets Manager Client using npm or any node.js package manager you use:

     npm install @aws-sdk/client-secrets-manager
    
  3. To access the secret values, you need to create a function to fetch the secrets from your AWS account. The code below is a working implementation simplified with a simple caching strategy to reduce call to AWS minimizing costs:

     // src/utils/getSecrets.js
     const {
       SecretsManagerClient,
       GetSecretValueCommand,
     } = require('@aws-sdk/client-secrets-manager');
    
     const secret_name = 'nodesocial_secrets'; // replace with the name you used
    
     const client = new SecretsManagerClient({
       region: 'eu-north-1', // replace with your AWS Secret Manager region
     });
    
     let cachedSecrets = null;
    
     /**
      * Retrieves a specific secret value from AWS Secrets Manager.
      *
      * @async
      * @function getSecret
      * @param {string} value - The key of the secret to retrieve from the secrets JSON.
      * @returns {Promise<string>} The value of the specified secret key.
      * @throws {Error} If the secret string is empty or the specified key is not present in the secrets.
      */
     exports.getSecret = async (key) => {
       if (key === '') throw new Error('Secret string must not be empty!');
    
       // If cached, return from memory
       if (cachedSecrets && cachedSecrets[key]) {
         return cachedSecrets[key];
       }
    
       // If cache not available, fetch from AWS
       if (!cachedSecrets) {
         const command = new GetSecretValueCommand({
           SecretId: secret_name,
           VersionStage: 'AWSCURRENT',
         });
    
         const response = await client.send(command);
    
         if (!response.SecretString) {
           throw new Error('Secret string is EMPTY!!');
         }
    
         cachedSecrets = JSON.parse(response.SecretString);
       }
    
       if (!Object.prototype.hasOwnProperty.call(cachedSecrets, key)) {
         throw new Error(`Key "${key}" not present in secrets`);
       }
    
       return cachedSecrets[key];
     };
    
     /**
      * Resets the cached secrets (for testing purposes).
      */
     exports.resetCache = () => {
       cachedSecrets = null;
     };
    
  4. To use the getSecrets function, simple import it and call it with the name of the value to retrieve as an argument. For instance, to fetch the MongoDB URL we stored to connect to your database, you could use the code below:

     // src/config/db.js
     const mongoose = require('mongoose');
     const { getSecret } = require('../utils/getSecrets.js');
    
     exports.connectDB = async () => {
       try {
         const mongoURL = await getSecret('MONGO_DB_URL'); // replace with the name of the key used
         const conn = await mongoose.connect(mongoURL);
         console.log(`MongoDB connected: ${conn.connection.host}`);
       } catch (error) {
         console.error(error.message);
         throw error;
       }
     };
    
  5. The success db connection message should be printed on your terminal as shown below:

Configuring EC2 to access Secrets Manager Value Securely with Docker

Now that you’ve configure AWS Secrets Manager to fetch values on your local environment, you might want to deploy you application like I did mine. However, this involves various steps:

  1. At the root of your node.js application you can create a simple dockerfile that will be used for deployment. Below is the one I used for my application:

     FROM node:slim
    
     # set the working directory in the container
     WORKDIR /app
    
     # copy package.json and package-lock.json
     COPY package*.json ./
    
     # Install dependencies
     RUN npm install --only=production
    
     # Copy the rest of the application
     COPY . .
    
     # Expose the port the application will be running on
     EXPOSE 3001
    
     # Set environment variables
     ENV NODE_ENV=production
    
     # Start the application
     CMD [ "node", "src/index.js" ]
    
  2. You can use the docker compose yaml file as well to use run the application on the server:

     version: '3.8'
    
     services:
       app:
         build: .
         container_name: nodesocial
         ports:
           - '3001:3001'
    
     volumes:
       redis_data:
         driver: local
    
  3. You can also automate deployment using GitHub Actions with the sample workflow below:

     name: Deploy to EC2
    
     on:
       push:
         branches:
           - main
    
     jobs:
       deploy:
         runs-on: self-hosted
    
         env:
           DEPLOY_PATH: '/home/ubuntu/NodeSocial-API'
    
         steps:
           - name: Checkout Repository
             uses: actions/checkout@v3
    
           - name: Ensure Repository Exists
             run: |
               if [ ! -d "$DEPLOY_PATH" ]; then
                   git clone https://github.com/SymonMuchemi/NodeSocial-API.git $DEPLOY_PATH
               fi
    
           - name: Stop and remove existing containers
             run: |
               cd $DEPLOY_PATH
               sudo docker-compose down || true
    
           - name: Pull latest changes
             run: |
               cd $DEPLOY_PATH
               git pull origin main
    
           - name: Build and start containers
             run: |
               cd $DEPLOY_PATH
               sudo docker compose up --build -d
    
  4. In my case, I use a self-hosted runner which I installed on an already created EC2 server. Learn more about self-hosted runners here.

  5. Create an EC2 instance to deploy your code on. You can learn about instantiating EC2 servers here.

  6. Ssh into your EC2 instance and install docker and docker compose, you can use the following script:

     #!/bin/bash
    
     # Exit on error
     set -e
    
     echo "🚀 Updating system packages..."
     sudo apt update && sudo apt upgrade -y
    
     echo "🐳 Installing prerequisites..."
     sudo apt install -y ca-certificates curl gnupg lsb-release
    
     echo "🔐 Adding Docker GPG key..."
     sudo install -m 0755 -d /etc/apt/keyrings
     curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
       | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    
     echo "🔧 Setting up Docker repository..."
     echo \
       "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
       https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
       | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
     echo "📦 Installing Docker Engine and Docker Compose CLI..."
     sudo apt update
     sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    
     echo "✅ Verifying Docker installation..."
     sudo docker --version
     sudo docker compose version
    
     echo "👤 Adding user '$USER' to the docker group..."
     sudo usermod -aG docker $USER
    
     echo "📦 Applying docker group changes..."
     newgrp docker
    
     echo "✅ Done! Docker group changees applied."
    
  7. Deploy you application by pushing to the GitHub repository.

  8. On you AWS EC2 console select your EC2 instance, click on the actions button, then ‘Security’ followed by ‘Modify IAM’.

  9. For my case, I had already created an IAM role but to create one, click on ‘Create new IAM role’ link.

  10. Click on ‘Create Role’:

  11. Select ‘AWS Service’ as the trusted entity type.

  12. For the use case, type and select ‘EC2’ and leave everything else as it is then click ‘next’.

  13. For step two, search for Secrets Management policy created earlier. For demo purposes I created and named one as nodesocial_demo_policy'.

  14. Provide a name for the IAM role and click ‘Create Role’ to create the IAM role.

    In addition to a Secret Manager access policy on the IAM role, you also need to attach a KMS policy to IAM role to allow the server to decrypt the secret values.

  15. Head on to the IAM console and click on the roles link on the sidebar then search for the newly created role. In this case ‘demo_nodesocial_ec2_role:

  16. Click on ‘Add permission’ then ‘Create inline policy’.

  17. For the allow actions in the ‘Write’ sections, allow Decrypt to decrypt and GenerateDataKey.

  18. By now, you might not have a KMS key configure in your account.

  19. Create a new KMS key by heading to the KMS console and click on the ‘Create a key’ button. If you already have a KMS key for Encryption and Decryption purposes, skip to step 27.

  20. For the configuration step, you make leave everything as it is to use a symmetric key for encryption and decryption. The key is meant for encrypting and decrypting secret values, so make sure it is the selected usage.

  21. Give the key a descriptive name and note it for you.

  22. In step 3, you can search for the EC2 IAM role created in step 12.

  23. In step 4, search for the EC2 IAM role and select it to allow encryption and decryption usage:

  24. Click on ‘next’ to move to step step 5 and again to move to step 6 unless you wish to edit the key policy.

  25. Click on ‘Finish’ to complete the configuration.

  26. We’re almost done. Go to the KMS console and click on ‘Customer Managed Keys’. You will see the newly created key; ‘demo_kms_key’ in our case.

  27. Click on it and copy its ARN ID.

  28. Head back to the IAM policy creation tab and in the resources section, click on the ‘add ARNs’ link then click on ‘text’ and copy the ARN ID in the text area.

  29. Click on next, name the policy and click ‘Create policy’ to create it.

  30. Now your EC2 server can access the secrets in you Secrets Manager securely. You don’t need to copy the access keys to the server. The docker image will also access the secrets securely.

Conclusion

Integrating AWS Secrets Manager into your Node.js application significantly enhances the security and management of sensitive information. By moving away from .env files, you mitigate risks associated with unauthorized access, accidental leaks, and manual secret rotation. AWS Secrets Manager offers robust features such as fine-grained access control, automatic secret rotation, and encryption, ensuring that your application remains secure and scalable. By following the steps outlined in this guide, you can seamlessly integrate Secrets Manager into your development and production environments, providing a more secure and efficient way to handle secrets in your Node.js applications.

7
Subscribe to my newsletter

Read articles from Simon Muchemi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Simon Muchemi
Simon Muchemi

I’m a software engineer with over two years of hands-on experience specializing in building scalable, high-performance backend systems. I have strong expertise in TypeScript, Node.js, Python, Flask, PostgreSQL, MongoDB, and cloud technologies. I am passionate about designing and developing robust, efficient, and reliable applications that solve real-world problems. My work consistently focuses on backend architecture, API development, database optimization, and cloud deployment, ensuring that systems are both scalable and secure.