Definition
- Instance profile – wraps an IAM role for EC2; the instance receives temporary credentials from the Instance Metadata Service, picked up by the SDK's default credentials provider chain.
- Task role – an IAM role attached to an ECS task definition that grants the application's own AWS calls temporary credentials.
- Task execution role – a separate ECS role that the ECS agent uses to pull images from ECR and write logs to CloudWatch.
- Execution role (Lambda) – the IAM role a Lambda function assumes; the runtime provides temporary credentials automatically.
- Secrets Manager – stores, encrypts (KMS), and automatically rotates application secrets like database passwords on a schedule.
- SSM Parameter Store SecureString – a cheaper KMS-encrypted parameter store for configuration and simpler secrets.
Imagine an employee at National Bank of India needs to open certain doors. The wrong way is to photocopy a permanent master key and tape it inside their notebook (hardcoded keys) -- if the notebook is lost, anyone can open the doors forever. The right way is a smart badge (an IAM role) tied to their role in the building: it grants exactly the doors they need, it is checked automatically at each door, and the access code rotates constantly, so a copied badge stops working quickly. Secrets Manager is the sealed, time-locked safe where sensitive combinations are kept and automatically changed on a schedule.
Understanding the Concepts
What is the problem with hardcoded access keys?
Static access keys are long-lived credentials that, if leaked through source control, logs, or a compromised instance, give an attacker unlimited access for as long as the keys exist. At National Bank of India, embedding AWS access keys in application code is a critical security violation because a single leaked key pair can reach every service the key is authorized for.
// WRONG: hardcoded keys -- if this file leaks, the attacker has full access
package com.bank.service;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.s3.S3Client;
public class InsecureClient {
// Never do this in production
private final S3Client s3 = S3Client.builder()
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG")
)).build();
}
How does an instance profile work for EC2?
You create an IAM role with the permissions the application needs, wrap it in an instance profile, and attach it to the EC2 instance. The application running on the instance reads temporary credentials from the Instance Metadata Service, and the AWS SDK does this transparently via the default credentials provider chain -- you write no credential code at all.
// CORRECT: no keys in code. The SDK picks up role credentials automatically.
package com.bank.service;
import software.amazon.awssdk.services.s3.S3Client;
public class SecureClient {
// Default provider chain: instance profile -> task role -> env vars -> ...
private final S3Client s3 = S3Client.create();
public void uploadStatement(String accountId) {
// Uses temporary, rotating credentials from the attached role
System.out.println("Uploading with role credentials for: " + accountId);
}
}
How does a task role work for ECS?
For ECS, you attach a task role to the task definition, and each task receives its own temporary credentials scoped to exactly what that service needs. There is also a separate task execution role that ECS itself uses to pull the container image from ECR and write logs to CloudWatch. It is important not to confuse the two: the task role is for your application's calls, the execution role is for the ECS agent's housekeeping.
// ECS task definition with both roles (conceptual)
package com.bank.infrastructure;
public class EcsTaskConfig {
// Task role: what the application can do (S3, DynamoDB, SQS)
private final String taskRoleArn = "arn:aws:iam::111122223333:role/transfer-task-role";
// Execution role: what ECS agent can do (ECR pull, CloudWatch logs)
private final String executionRoleArn = "arn:aws:iam::111122223333:role/ecs-exec-role";
public void printRoleExplanation() {
System.out.println("Task role: authorizes YOUR application's AWS calls");
System.out.println("Execution role: authorizes the ECS AGENT's housekeeping");
}
}
How does an execution role work for Lambda?
For Lambda, you attach an execution role, and the function's runtime environment provides temporary credentials automatically. The role grants both the permissions the function logic needs (such as reading from S3 or writing to DynamoDB) and the basic permissions to write logs to CloudWatch Logs.
// Lambda handler using role-based credentials
package com.bank.lambda;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.util.Map;
public class FeeCalculatorHandler {
// SDK picks up Lambda execution role credentials automatically
private final DynamoDbClient dynamo = DynamoDbClient.create();
public String handleRequest(Map<String, Object> event, Object context) {
dynamo.putItem(PutItemRequest.builder()
.tableName("nbi-fee-records")
.item(Map.of("txnId", AttributeValue.fromS(event.get("txnId").toString())))
.build());
return "Fee recorded";
}
}
How does Secrets Manager work?
AWS Secrets Manager encrypts secrets with KMS, controls access with IAM, and supports automatic rotation on a schedule using a rotation Lambda. The rotation Lambda changes the database password and updates both the secret and the database without downtime. Your application fetches the secret at runtime using the role's permissions.
// Fetching a rotated secret at runtime
package com.bank.service;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
public class SecretFetcher {
private final SecretsManagerClient sm = SecretsManagerClient.create();
public String getDatabasePassword() {
String secretJson = sm.getSecretValue(
GetSecretValueRequest.builder()
.secretId("prod/nbi-ledger/credentials")
.build()
).secretString();
// Parse JSON to extract username and password
System.out.println("Fetched rotated credentials from Secrets Manager");
return secretJson;
}
}
How does SSM Parameter Store compare to Secrets Manager?
SSM Parameter Store offers SecureString parameters, also KMS-encrypted and IAM-controlled, which is cheaper and suitable for configuration values and simpler secrets. It does not have built-in automatic rotation like Secrets Manager. Use Parameter Store for configuration like feature flags and non-rotating secrets, and Secrets Manager for credentials that must rotate automatically.
| Feature | Secrets Manager | Parameter Store SecureString |
|---|---|---|
| Encryption | KMS | KMS |
| Automatic rotation | Built-in (Lambda) | Manual (no built-in) |
| Cost | $0.40/secret/month + API calls | Free tier + lower API cost |
| Best for | Database passwords, API keys | Config values, feature flags |
// Fetching a config value from Parameter Store
package com.bank.service;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
public class ConfigFetcher {
private final SsmClient ssm = SsmClient.create();
public String getConfig(String paramName) {
return ssm.getParameter(
GetParameterRequest.builder()
.name(paramName)
.withDecryption(true)
.build()
).parameter().value();
}
}
What are common mistakes?
The first mistake is baking secrets into container images, environment variables committed to source, or AMIs. These are precisely where leaks happen. The second mistake is giving one overprivileged role to all services instead of a distinct, least-privilege role per service. The third mistake is never refreshing a cached secret, so the application breaks silently when Secrets Manager rotates the password.
// Wrong: secret baked into environment variable in source control
// docker-compose.yml:
// environment:
// DB_PASSWORD: "MyS3cretPa$$word" <-- committed to git!
// Correct: fetch at runtime, cache briefly, refresh on failure
package com.bank.service;
public class SecretCacheExample {
private String cachedPassword;
private long cacheExpiry;
public String getPassword() {
if (cachedPassword == null || System.currentTimeMillis() > cacheExpiry) {
cachedPassword = new SecretFetcher().getDatabasePassword();
cacheExpiry = System.currentTimeMillis() + 300_000; // 5 min cache
}
return cachedPassword;
}
public void onAuthFailure() {
// Force refresh on auth failure (password may have rotated)
cachedPassword = null;
}
}
What should a senior engineer know?
Scope each role to least privilege and prefer a distinct role per service so a compromise is contained and auditable. Cache fetched secrets briefly in memory to avoid hitting the secrets store on every request, but refresh on an authentication failure so rotation is picked up immediately. Use the AWS SDK default credentials provider chain everywhere and never instantiate static credentials providers. For ECS, understand the distinction between task role and execution role and never over-privilege the execution role.
Visual Overview
How Compute Gets Credentials (No Hardcoded Keys)
┌───────────┐ ┌──────────────────┐ ┌───────────────────┐
│ EC2 │ │ ECS Fargate │ │ Lambda │
│ Instance │ │ Task │ │ Function │
└─────┬─────┘ └────────┬─────────┘ └─────────┬─────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────────────┐ ┌───────────────────┐
│ Instance │ │ Task Role │ │ Execution Role │
│ Profile │ │ (app calls) │ │ (function logic) │
│ (IAM Role)│ │ Exec Role │ │ │
│ │ │ (agent calls) │ │ │
└─────┬─────┘ └────────┬─────────┘ └─────────┬─────────┘
│ │ │
└───────────────────┼────────────────────────┘
│
▼
┌──────────────────────────┐
│ AWS STS │
│ Issues temporary, │
│ auto-rotating │
│ credentials │
│ (~1 hour expiry) │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ AWS SDK Default │
│ Credentials Provider │
│ Chain picks them up │
│ transparently │
└──────────────────────────┘
Secret Management Flow
┌───────────┐ fetch at ┌──────────────────┐
│Application│───runtime──────→│ Secrets Manager │
│(ECS task) │ (role auth) │ or Parameter Store│
└───────────┘ └────────┬─────────┘
▲ │
│ refresh on │ encrypted with
│ auth failure ▼
│ ┌──────────────────┐
└───────────────────────│ AWS KMS │
│ (key management)│
└──────────────────┘
Key Takeaway
The fundamental rule is that compute should assume an IAM role, never carry static access keys. For an EC2 instance I attach an instance profile, for an ECS task I attach a task role, and for a Lambda function I attach an execution role. In every case AWS delivers short-lived credentials automatically -- through the instance metadata service or the runtime -- and the AWS SDK picks them up transparently through its default credentials provider chain, so I write no credential code and there is nothing long-lived to leak.
For ECS specifically, I distinguish the task role, which authorizes the application's own AWS calls, from the task execution role, which the ECS agent uses to pull images and write logs. I scope every role to least privilege and prefer a distinct role per service so a compromise is contained and every action is attributable.
For application secrets like database passwords and third-party API keys, I store them in AWS Secrets Manager or SSM Parameter Store as SecureString, both encrypted with KMS and access-controlled by IAM, and I fetch them at runtime using the role's permissions rather than baking them into images, environment variables, or AMIs. Secrets Manager's key advantage is automatic rotation: a rotation Lambda changes the password and updates the database on a schedule with no downtime.
At National Bank of India the transfer service ran as an ECS Fargate task with a least-privilege task role, so it reached S3 and other services with temporary rotating credentials and had no access keys anywhere. Its database password lived in Secrets Manager with automatic rotation, fetched at startup and refreshed on auth failure. The senior habit I always mention is caching the fetched secret briefly in memory while refreshing on failure, and never committing a secret or access key into source control or a container image.
Remember one sentence: attach IAM roles to compute so the SDK gets temporary, rotating credentials automatically, store secrets in Secrets Manager with automatic rotation, and never embed long-lived access keys anywhere in code or config.
---