Articles

Stop Hardcoding Secrets: AWS Secrets Management Guide

Hardcoding credentials puts applications at risk. This guide shows how AWS Secrets Manager, Systems Manager Parameter Store, and IAM roles work together to keep Node.js production workloads on ECS/Fargate secure and maintainable.

Written by:
APin

Senior Technology Analyst • Verified Expert

More from this author →
Stop Hardcoding Secrets: AWS Secrets Management Guide

Hardcoding credentials puts applications at risk. This guide shows how AWS Secrets Manager, Systems Manager Parameter Store, and IAM roles work together to keep Node.js production workloads on ECS/Fargate secure and maintainable.

Why Hardcoding Secrets Is Dangerous

Hardcoding credentials such as database passwords, JWT signing keys, and API tokens directly into source code introduces severe security risks. By embedding sensitive values—like const dbPassword = "my-super-secret-password";—within your codebase, you significantly expand the attack surface. Any entity with access to the source code, including authorized team members or automated systems, gains access to these credentials. Furthermore, pushing code containing plaintext secrets to version control systems like GitHub risks permanent exposure, as sensitive data may remain in repository history even after subsequent deletion.

The operational burden of hardcoding is equally problematic. When a credential requires rotation or an update, the current process necessitates a full development lifecycle iteration: modifying the source code, rebuilding the container image, and redeploying the application. This coupling of application logic to sensitive configuration violates the principle of separation of concerns and hinders agility.

Embedding secrets within Docker images is a dangerous, common anti-pattern. If you use the ENV instruction in a Dockerfile to set sensitive values, those secrets are persisted within the image layers. This exposes the credentials to anyone capable of inspecting the image metadata or layers in registries like Amazon ECR. Production applications must decouple code from configuration to ensure security and maintainability. Best practices include:

  • Externalize Configuration: Use dedicated secrets management services, such as AWS Secrets Manager for sensitive credentials that require lifecycle management and rotation, or AWS Systems Manager Parameter Store for non-sensitive configuration values.
  • Implement Least Privilege: Use IAM policies to restrict access to secrets, ensuring only the specific execution environment (e.g., an ECS task role) can retrieve the necessary parameters.
  • Utilize Git Ignore: Keep .env files or other local configuration files out of version control by updating your .gitignore file. Use .env.example files to document required variables without exposing real values.
  • Dynamic Injection: Inject secrets into the application environment at runtime via orchestration layers (e.g., ECS task definitions) rather than baking them into the container image itself.

The Role of .env Files in Development

In local development, the .env file serves as a mechanism to externalize configuration, preventing the hardcoding of sensitive credentials such as database URIs or API keys directly into the source code. By utilizing libraries like dotenv to load these values into the application process at runtime, engineers maintain a clear separation between executable code and environment-specific configuration.

To ensure security, these files must be explicitly excluded from version control systems. The following practices are essential for local environment hygiene:

  • Version Control Exclusion: Always add .env to your .gitignore file to prevent sensitive data from being committed to repositories.
  • Template Usage: Create a .env.example file that acts as a template. This file contains the required variable names without the actual secret values, providing documentation for other developers on which environment variables the application requires.
  • Strict Pattern Matching: Use patterns in .gitignore (e.g., .env.*) to account for variations in local configurations while maintaining the exclusion of the primary .env file.

While .env files are effective for local workflows, they are insufficient for production secret management. Storing secrets in flat files within a production environment introduces risks, including potential exposure through server logs, process inspection, or unauthorized access to the filesystem. Unlike production-grade secret management services, .env files lack critical features such as:

  • Automated Secret Rotation: The ability to programmatically update and rotate credentials without manual intervention or redeployment.
  • Centralized Access Control: Integration with identity providers to enforce the principle of least privilege, ensuring only specific authorized entities can retrieve sensitive data.
  • Audit Logging: Detailed tracking of which entities accessed which secrets and when.
  • Encryption at Rest: Managed services typically provide transparent encryption using hardware security modules, whereas .env files usually exist as plaintext.

For production, credentials should be retrieved from dedicated services like AWS Secrets Manager or Systems Manager Parameter Store, ensuring that sensitive values are never persisted in the codebase or baked into container images.

AWS Secrets Manager for Sensitive Information

AWS Secrets Manager is a dedicated service for storing and retrieving sensitive data such as database credentials, JWT signing keys, and third‑party API keys. Each secret is stored as a JSON‑structured key‑value pair, for example:

{
  "MONGO_URI": "mongodb+srv://user:password@cluster.example.com",
  "JWT_SECRET": "my‑jwt‑secret",
  "THIRD_PARTY_API_KEY": "abcd1234"
}

When an application needs a value, it calls the GetSecretValue API. The call succeeds only if the caller’s IAM identity has an explicit secretsmanager:GetSecretValue permission for that secret ARN, enforcing the principle of least privilege.

Secret rotation workflow

Secrets Manager can automate rotation for supported targets (e.g., Amazon RDS). Rotation is configured by attaching a Lambda function that performs the following steps:

  • Generate a new credential.
  • Test the new credential against the target service.
  • Update the secret value in Secrets Manager.
  • Mark the rotation as successful.

Rotation must be enabled per secret; storing a secret does not trigger automatic rotation.

IAM‑authorized retrieval in container environments

In Amazon ECS/Fargate, two IAM roles are relevant:

  • Task execution role – used by the ECS agent for actions such as pulling images.
  • Task role – assumed by the application code inside the container.

To let the application read a secret, attach a policy like the following to the task role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:region:account-id:secret:production/my-backend*"
    }
  ]
}

If the task definition references the secret, ECS can inject the value as an environment variable at container start. Note that the variable is static for the lifetime of the task; a new deployment is required to pick up rotated values.

Practical example

Consider a Node.js backend that connects to Amazon RDS. The code retrieves the database URI at runtime:

const AWS = require('aws-sdk');
const client = new AWS.SecretsManager();
async function getDbUri() {
  const data = await client.getSecretValue({ SecretId: 'production/my-backend' }).promise();
  const secret = JSON.parse(data.SecretString);
  return secret.MONGO_URI;
}

This pattern keeps credentials out of source control, Docker images, and CI/CD pipelines, while IAM controls ensure only the intended task can read the secret.

Systems Manager Parameter Store for Configuration

Systems Manager Parameter Store (PS) provides a hierarchical key‑value store that can be accessed via the AWS SDK, CLI, or EC2/ECS integration. It defines three native data types:

  • String – plain‑text values suitable for static configuration such as APP_ENV=production or LOG_LEVEL=info.
  • StringList – a comma‑separated list that the SDK returns as an array, useful for enumerations like ALLOWED_ORIGINS=example.com,api.example.com.
  • SecureString – values encrypted at rest with an AWS KMS customer master key (CMK). The plaintext is never stored; it is decrypted only for authorized callers.

For non‑secret configuration, String and StringList are sufficient. They incur no per‑parameter charge and can be versioned, enabling roll‑back without exposing sensitive data. A typical pattern is to group parameters by application and environment:

/my-app/dev/LOG_LEVEL          → info
/my-app/dev/API_ENDPOINTS      → https://dev.api.example.com,https://dev.auth.example.com
/my-app/prod/MAX_RETRIES       → 5

When a value must be protected—e.g., a database password, an API token, or a private TLS key—SecureString should be used. The parameter is stored encrypted with the specified CMK, and IAM policies control which principals can call ssm:GetParameter with WithDecryption=true. This satisfies compliance frameworks such as SOC 2, ISO 27001, and NIST 800‑53, which require encryption of credential data at rest and strict access control.

Choosing between SecureString and AWS Secrets Manager depends on lifecycle requirements:

  • Use SecureString for low‑frequency rotation or when the secret is part of a broader configuration set that does not need automated rotation.
  • Prefer Secrets Manager when you need built‑in rotation, cross‑service integration (e.g., RDS credential rotation), or fine‑grained secret versioning.

In practice, many architectures combine both services: non‑secret settings live in Parameter Store as String/StringList, while highly sensitive credentials are stored as SecureString or in Secrets Manager, with the application retrieving each via the same SDK call pattern. This separation keeps the parameter hierarchy simple, reduces cost, and aligns with the principle of least privilege.

IAM Policies and Least‑Privilege Access

In AWS, Identity and Access Management (IAM) enforces the principle of least‑privilege by binding identities (users, services, or containers) to policies that explicitly list allowed actions on specific resources. When a container running on Amazon Elastic Container Service (ECS) needs a secret, the IAM policy attached to the container’s role must grant secretsmanager:GetSecretValue for the exact secret ARN; any broader permission would expose unrelated credentials.

Roles used by ECS

  • Task execution role – assumed by the ECS agent itself. It is required for operations that occur outside the container, such as pulling a private image from Amazon ECR, writing logs to CloudWatch, or retrieving secrets that the agent injects as environment variables.
  • Task role – assumed inside the container process. The application code uses this role when it directly calls AWS APIs (e.g., AWS SDK to read a secret from Secrets Manager or Parameter Store).

Choosing the correct role prevents unnecessary privilege escalation. If the secret is injected by the ECS service, only the execution role needs secretsmanager:GetSecretValue. If the application fetches the secret at runtime, the task role must hold that permission.

Granting minimal secret access

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:production/my-backend-ABC123"
        }
    ]
}

This policy limits the container to a single secret (production/my-backend). Adding additional statements for other secrets must be done deliberately, preserving the least‑privilege posture required by standards such as NIST SP 800‑53 and ISO 27001.

Practical implementation steps

  1. Create a secret in AWS Secrets Manager containing only the values the service needs (e.g., DB_PASSWORD).
  2. Define a task role (or execution role) with a policy like the example above.
  3. Reference the secret ARN in the ECS task definition under secrets if using injection, or let the application call GetSecretValue using the SDK.
  4. Deploy the task; the container will receive the secret only if the attached role matches the policy.

By separating the execution and task roles and scoping IAM policies to the exact secret ARN, engineers enforce least‑privilege access, reduce the attack surface, and remain compliant with widely adopted security frameworks.

Integrating Secrets with Docker and ECS/Fargate

When a Docker image is built, the image layers become immutable artifacts that can be inspected by anyone with read access to the registry. Embedding secrets (e.g., ENV JWT_SECRET=…) therefore violates the principle of separating code from configuration and creates a permanent exposure surface. Instead, the image should contain only the application binaries and any non‑sensitive defaults.

At runtime, AWS provides two mechanisms to supply secrets to an Amazon ECS or Fargate task without baking them into the image:

  • AWS Secrets Manager – stores confidential values (database passwords, API keys) and can rotate them automatically. The secret is retrieved via the secrets block of a task definition, which maps a secret ARN to an environment variable.
  • AWS Systems Manager Parameter Store – holds configuration data; SecureString parameters are encrypted with KMS and can be referenced the same way as Secrets Manager values.

To inject these values, define a task role with the least‑privilege IAM policy that permits secretsmanager:GetSecretValue (or ssm:GetParameter) only for the specific ARNs required by the service. The task execution role needs permission to pull the image from ECR and to read the secret if ECS performs the injection on behalf of the container.

Example task definition fragment (JSON):

{
  "containerDefinitions": [
    {
      "name": "backend",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/backend:latest",
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/backend-db-ABC123"
        },
        {
          "name": "API_KEY",
          "valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/backend/api-key"
        }
      ]
    }
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/backend-task-role",
  "executionRoleArn": "arn:aws:iam::123456789012:role/backend-exec-role"
}

Because the secret value is fetched only when the container starts, a running task will not see updates automatically. To apply a rotation, trigger a new deployment (e.g., update the service or use a rolling restart). This aligns with NIST SP 800‑53 guidance on secret lifecycle management and satisfies ISO 27001 controls that require separation of duties and timely revocation of credentials.

Best‑practice checklist:

  • Never use ENV or ARG in the Dockerfile for production secrets.
  • Store secrets in Secrets Manager; use Parameter Store for non‑secret configuration.
  • Assign a dedicated task role with scoped permissions to each service.
  • Reference secrets in the task definition’s secrets array, not in the image.
  • Plan for secret rotation by redeploying tasks after a change.
APPWORKS ENGINEERING

Looking for Custom Software or AI Solutions?

Appworks Technologies designs, builds, and scales production enterprise platforms, microservices, and AI agent workflows tailored to your business goals.

Editorial Policy & Research Methodology

Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.

Have an Idea? we offer services in Lucknow, Bangalore, Delhi NCR and other locations