Imagine you're building a Node.js backend using Express and MongoDB.
During development, you might have a .env file containing:
PORT=5000
MONGO_URI=mongodb+srv://...
JWT_SECRET=...
API_KEY=...
Your application works perfectly.
But now imagine pushing the project to GitHub, deploying it through a CI/CD pipeline, and running it on AWS.
Where should those secrets live?
Should we put them directly inside the source code? Add them to the Docker image? Store them in GitHub? Or let every developer manage their own copies?
These questions become important when an application moves from a local development environment to production.
In this article, we'll explore how AWS Secrets Manager, Systems Manager Parameter Store, and IAM help us manage application configuration and credentials more securely.
1. Why Hardcoding Secrets Is Dangerous
Let's start with a simple example.
const dbPassword = "my-super-secret-password";
const jwtSecret = "my-jwt-secret";
This may look convenient, but it creates a serious security problem.
Anyone who can access the source code may also be able to access the credentials.
If we push these values to a public repository, the secrets could be exposed. Even a private repository can have too many people or automated systems with access.
There is another problem: what happens when a password changes?
We may need to modify the code, rebuild the Docker image, and deploy the application again just to update a credential.
Application code and sensitive configuration should be managed separately.
That is the principle behind secure secrets management.
2. What About the .env File?
A .env file is useful during local development.
For example:
PORT=5000
MONGO_URI=your_database_connection_string
JWT_SECRET=your_jwt_secret
Your Node.js application can load these values using a package such as dotenv.
require("dotenv").config();
const port = process.env.PORT;
const mongoUri = process.env.MONGO_URI;
This keeps configuration out of the source code itself.
However, a .env file is not automatically a secure secrets management system.
For local development, we should keep it out of Git using .gitignore.
.env
.env.*
!.env.example
An .env.example file can document the required variable names without containing real credentials.
For production, we need a controlled way to provide those values to our running application.
That's where AWS services come in.
3. AWS Secrets Manager — Store Sensitive Information
AWS Secrets Manager is designed to store, retrieve, and manage sensitive information such as database credentials, API keys, and other application secrets.
For example, we could store a secret containing:
Secret name: production/my-backend
MONGO_URI: ...
JWT_SECRET: ...
THIRD_PARTY_API_KEY: ...
The application retrieves the required secret when it needs it, provided it has the appropriate IAM permissions.
A simplified architecture looks like this:
Developer
|
v
AWS Secrets Manager
|
| Authorized access
v
Application on ECS / Fargate
|
v
Connect to Database / External API
The important point is that the sensitive values do not need to be hardcoded into our application source code or baked into the Docker image.
Secrets Manager also supports secret rotation workflows, including integrations and rotation configurations for supported services.
Rotation still needs to be configured correctly; storing a secret in Secrets Manager does not automatically mean every secret will rotate itself.
4. Systems Manager Parameter Store — Manage Configuration
Not every configuration value is a secret.
Consider these examples:
APP_ENV=production
LOG_LEVEL=info
MAX_RETRIES=3
API_BASE_URL=https://api.example.com
These values may need to change between development, testing, and production, but they are not necessarily confidential.
AWS Systems Manager Parameter Store provides a central place to store application configuration and parameters.
It supports different parameter types, including:
-
String— ordinary configuration values. -
StringList— comma-separated string values. -
SecureString— sensitive values encrypted using AWS KMS.
For example:
/my-app/production/LOG_LEVEL
/my-app/production/API_BASE_URL
/my-app/production/DB_PASSWORD
Parameter Store can help organize configuration by application and environment.
A SecureString can be useful for encrypted parameters, while Secrets Manager is often a better fit when managed secret lifecycle and rotation features are important.
The choice depends on the requirements rather than one service being universally better.
5. Secrets Manager vs Parameter Store
This is where many developers get confused.
Both services can store sensitive configuration, but their strengths differ.
| Feature | Secrets Manager | Parameter Store |
|---|---|---|
| Main purpose | Secret lifecycle management | Configuration and parameter management |
| Database credentials | Yes | Can store encrypted values |
| Encrypted values | Yes | Yes, using SecureString
|
| Secret rotation | Built-in rotation capabilities and integrations | Usually requires a separate rotation workflow |
| Ordinary configuration | Possible | A natural fit |
| Cost considerations | Generally charged for secrets and API usage | Standard parameters have a free tier; advanced features can incur charges |
For example, I might choose Secrets Manager for database credentials that require a managed rotation workflow and Parameter Store for ordinary application settings.
Both can be useful in the same application.
6. IAM — Who Is Allowed to Read the Secrets?
Storing secrets securely is only half of the problem.
We also need to decide who or what is allowed to access them.
This is where AWS Identity and Access Management (IAM) comes in.
IAM controls access to AWS resources through identities, policies, and roles.
Imagine our Node.js backend is running as an ECS task on Fargate.
We want it to retrieve one database secret from Secrets Manager.
We should not give the application unrestricted access to every secret in the AWS account.
Instead, we can grant the task permission to retrieve only the required secret.
ECS Task
|
v
Task Role
|
v
IAM Policy
|
v
Allowed Secret Only
This follows the principle of least privilege: give each component only the permissions it needs.
For ECS, there are two roles worth distinguishing:
- Task execution role: Used by ECS/Fargate for operations such as pulling private images from ECR and sending logs to supported destinations.
- Task role: Provides AWS permissions to the application code running inside the container.
If the application itself calls Secrets Manager, its task role generally needs the appropriate permission.
If ECS retrieves secrets and injects them into a container as environment variables, the task execution role needs the relevant permissions instead.
Getting this distinction right helps avoid unnecessary permissions and deployment failures.
7. How Does This Work with Docker and ECS?
Let's connect secrets management to the previous article about Docker, ECR, ECS, and Fargate.
A common mistake is putting production credentials directly into the Dockerfile.
For example, this is a bad idea:
ENV JWT_SECRET=my-production-secret
Why?
Because the value can become part of the image configuration and may be exposed to anyone who can inspect the image.
Instead, the image should contain the application, while production configuration is supplied separately.
Docker Build
|
v
Application Image
|
v
Amazon ECR
|
v
ECS / Fargate
|
v
Runtime Configuration
|
+---------+----------+
| |
v v
Secrets Manager Parameter Store
For example, an ECS task definition can reference a secret stored in Secrets Manager and make it available to the container at startup.
The application can then read the value through its environment.
One important detail: when ECS injects a secret into an environment variable, the value is retrieved when the container starts. If the secret changes later, an already-running container does not automatically receive the updated value. A new task deployment or restart may be needed.
Also, environment variables are convenient, but they can be exposed through debugging tools, process inspection, or logs if handled carelessly. They should never be printed unnecessarily.
8. Example: A Node.js Backend on AWS
Imagine we deploy a Node.js application using:
- GitHub Actions for CI/CD.
- Docker for packaging.
- Amazon ECR for storing the image.
- Amazon ECS with Fargate for running the backend.
- Amazon RDS for the database.
- AWS Secrets Manager for database credentials.
- IAM for controlling access.
The application flow looks like this:
Users
|
v
Application Load Balancer
|
v
ECS / Fargate
|
+-------> Secrets Manager
| Database Credentials
|
+-------> Amazon RDS
Application Data
The backend needs permission to retrieve the secret and network access to connect to the database.
The database should normally be placed in private subnets, with security group rules that allow connections only from the appropriate application tasks.
Secrets Manager handles secret storage, IAM controls authorization, and the VPC and security groups control network access.
These are separate security layers working together.
9. What About GitHub Actions and CI/CD?
Our previous article explained how GitHub Actions can build Docker images, push them to ECR, and deploy them to ECS.
The pipeline also needs secure access to AWS.
A recommended approach is to configure OpenID Connect (OIDC) so GitHub Actions can assume an IAM role and obtain temporary AWS credentials.
GitHub Actions
|
v
OIDC
|
v
AWS IAM Role
|
v
Amazon ECR
|
v
Amazon ECS
The workflow role should have only the permissions required for its deployment responsibilities.
This is different from the IAM task role used by the application after deployment.
We should also avoid printing secret values in CI logs or passing credentials through command-line arguments unnecessarily.
The goal is to secure both the deployment process and the application runtime.
10. What Should We Choose?
Here is a practical way to decide.
Use Secrets Manager when:
- You need centralized storage for sensitive credentials.
- You need secret rotation capabilities.
- Your application needs controlled access to database passwords or API keys.
Use Parameter Store when:
- You need centralized application configuration.
- You want to organize settings by environment.
- You need encrypted parameters and don't require the full Secrets Manager lifecycle features.
Use IAM when:
- You need to control which users, services, or workloads can access AWS resources.
- You want applications to use roles instead of hardcoded AWS access keys.
- You want to enforce least-privilege permissions.
These services are complementary, not direct replacements for one another.
Quick Cheat Sheet
| Tool | Main Responsibility |
|---|---|
.env |
Local application configuration |
.gitignore |
Prevents local environment files from being committed |
| AWS Secrets Manager | Stores and manages sensitive secrets |
| Systems Manager Parameter Store | Stores configuration and parameters |
SecureString |
Encrypted Parameter Store value |
| IAM Role | Grants AWS permissions to an identity or workload |
| ECS Task Role | Permissions used by application code |
| ECS Task Execution Role | Permissions used by ECS/Fargate for task startup and related operations |
| GitHub OIDC | Provides a way for GitHub Actions to obtain temporary AWS credentials |
| AWS KMS | Manages encryption keys used by supported AWS services |
Final Thoughts
Moving an application to AWS is not just about getting it to run.
We also need to think about where its credentials live, who can access them, how they are updated, and what happens when those credentials change.
A local .env file can be useful during development, but production applications need a more controlled approach.
Secrets Manager, Parameter Store, and IAM help separate application code from sensitive configuration and enforce controlled access.
The key lesson is simple:
Don't hardcode secrets. Don't give every component unrestricted access. Keep configuration, permissions, and application code separate.
As I continue learning AWS, I'm finding that production engineering is not only about deploying applications. It's also about making sure they remain secure, maintainable, and reliable after deployment.

Top comments (1)
tr.ee/dev-to