writing / devops

Stop Hardcoding AWS Keys.

For EC2 workloads, attach an IAM role via an instance profile so your app obtains temporary credentials automatically. No hardcoded keys needed.

Atharva Uday UndeAtharva Uday UndeMay 26, 20267 min read
AWSSecurityNode.jsIAMBackendInfrastructureBestPracticesCredentials

The Problem With Hardcoded Keys

When you hardcode AWS access keys and secret keys into your Node.js application, you create a management burden:

  1. Manual Key Rotation: You have to remember to rotate them. Nobody does this consistently.
  2. Secret Storage: You need to store them somewhere (env file, secrets manager, config). Each adds complexity.
  3. Audit Trail: If a key leaks, you need to know who has it and where it's being used.
  4. Risk of Exposure: Every time you export, backup, or move code, you risk exposing credentials.
  5. Deployment Friction: You have to inject secrets into every environment (dev, staging, prod).

If any of those keys get exposed (committed to git, logged, or leaked), you have to revoke them immediately and update everywhere they're used.

That's operational debt.


The Solution: Instance Profiles (for EC2)

An instance profile is a container for an IAM role. When you attach an instance profile to an EC2 instance, applications running on that instance can retrieve temporary credentials from the instance metadata service instead of using static keys.

Your Node.js application doesn't need hardcoded keys. The AWS SDK automatically retrieves temporary credentials through the instance metadata service.

How It Works

1. Create an IAM Role (e.g., "NodeAppRole")
   ↓
2. Attach IAM policies to it (e.g., S3ReadOnly, DynamoDBAccess)
   ↓
3. Create an Instance Profile containing the IAM Role
   ↓
4. Launch your EC2 instance with that Instance Profile
   ↓
5. Applications retrieve temporary credentials from instance metadata
   ↓
6. Node.js SDK automatically picks them up from the credential chain
   ↓
7. Your code makes AWS API calls. Done.

No keys in your code. No env files. No secrets management for credentials.


How Node.js Automatically Picks Up Credentials

The AWS SDK for JavaScript v3 uses a default credential provider chain. It automatically checks multiple sources for credentials, including:

  • Environment variables
  • Shared credentials file (~/.aws/credentials)
  • IAM Identity Center
  • Web identity tokens
  • ECS task role credentials
  • EC2 instance metadata service (if running on EC2)
  • And others

When you attach an instance profile to an EC2 instance, the SDK automatically discovers and uses the credentials from the instance metadata service.

Your code doesn't need configuration or changes:

// This works automatically if running on an EC2 instance with an Instance Profile
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");

const client = new S3Client({ region: "us-east-1" });
const command = new ListBucketsCommand({});

client.send(command).then(console.log).catch(console.error);

The SDK fetches credentials from the metadata service behind the scenes. You don't do anything.


Instance Profiles vs Hardcoded Keys: Side by Side

Hardcoded Keys Approach

// DON'T DO THIS
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");

const client = new S3Client({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

const command = new ListBucketsCommand({});
client.send(command);

Problems:

  • Keys in environment variables (must be injected at runtime)
  • Keys in .env file (risk of accidental commit)
  • Rotating keys means redeploying application
  • Manual tracking of which environments have which keys
  • If key is compromised, chaos ensues

Instance Profile Approach

// DO THIS
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");

const client = new S3Client({ region: "us-east-1" });

// No credentials configuration needed. SDK auto-discovers from instance profile.
const command = new ListBucketsCommand({});
client.send(command);

Benefits:

  • No keys in code, env vars, or config files
  • Temporary credentials (AWS manages them automatically for the role)
  • Fine-grained permissions (one role per application)
  • Audit trail (CloudTrail tracks which role did what)
  • Zero application changes for credential management

For EKS (Kubernetes)

For EKS workloads, use IAM Roles for Service Accounts (IRSA) instead of instance profiles.

IRSA works by associating an IAM role with a Kubernetes service account. Pods that use that service account can assume the role and obtain temporary credentials.

# Create a ServiceAccount with an IAM role associated via annotation
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service-account
  namespace: default
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT:role/AppRole

Your Node.js pod automatically gets credentials through the AWS SDK's credential chain.

Important: IRSA provides better isolation than relying on the EC2 instance metadata service directly. However, if the instance metadata service is not restricted, pods may still be able to access the node's IAM role through IMDS. AWS recommends using IMDSv2 and restricting metadata service access to prevent unintended access.


Why This Matters

Operational Burden:

  • Hardcoded keys = you manage rotation manually
  • Instance Profiles = AWS manages temporary credentials automatically for the role on the instance Security:
  • Hardcoded keys = static credentials, high blast radius if leaked
  • Instance Profiles = temporary credentials, automatically rotated, limited scope Auditability:
  • Hardcoded keys = hard to track (which deployment has which key?)
  • Instance Profiles = CloudTrail shows exactly which role did what and when Scalability:
  • Hardcoded keys = doesn't scale beyond a handful of environments
  • Instance Profiles = scales to hundreds of services and environments

Setup Example (Quick)

Step 1: Create IAM Role

Role Name: NodeAppRole
Trusted Entity: EC2 service (ec2.amazonaws.com)
Permissions: Attach policies your app needs (S3, DynamoDB, etc.)

Step 2: Create Instance Profile

Instance Profile Name: NodeAppProfile
Attach Role: NodeAppRole

Step 3: Launch EC2 Instance

When launching, specify the Instance Profile. Your app automatically has credentials.

Step 4: Code (No Changes Needed)

Your Node.js code works as-is. The SDK finds credentials automatically.


Common Mistakes

Mistake #1: Still setting AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.

  • Why it's wrong: Defeats the purpose. You're back to managing keys manually.
  • What to do: Remove them. Let the SDK auto-discover from Instance Metadata. Mistake #2: Using Instance Profile on dev machine but hardcoded keys in CI/CD.
  • Why it's wrong: Inconsistency. Some environments have keys, some don't.
  • What to do: Use Instance Profiles everywhere. For local dev, use AWS credentials file or assume a role. Mistake #3: One broad Instance Profile for all applications.
  • Why it's wrong: Violates least-privilege principle. If one app is compromised, all can access the same resources.
  • What to do: Create granular roles. One role per application, with only the permissions it needs.

TL;DR

Problem: Hardcoded AWS keys require manual rotation, create security risks, and add operational burden.

Solution: Attach an IAM Role to your EC2 instance (or use IRSA on EKS). Your Node.js app automatically gets temporary credentials.

Result: No keys in code. No env vars. No management. AWS handles credential rotation for you.

If you're still hardcoding keys, stop. Instance Profiles solve this problem completely.


Tags: AWS · Security · Node.js · IAM · Backend · Infrastructure · BestPractices · Credentials