Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 004 / 100

IAM fundamentals: what every AWS developer should know

Who can do what, on which resource.

Expona Admin6 min read

AWS Identity and Access Management (IAM) is where security begins. Get this wrong, and you're either locking legitimate users out or handing attackers the keys to your infrastructure. Let me walk you through the mental model that makes IAM click: who can do what, on which resource.

Understanding IAM fundamentals

IAM fundamentals rest on three core pillars: principals (who), policies (what they can do), and resources (what they can do it to). This isn't just security theater—it's the foundation that lets you build multi-tenant applications, grant temporary access to contractors, and sleep at night knowing your production database isn't writable by every engineer in the company.

Most developers I've worked with treat IAM as an afterthought, a permissions checkbox before deployment. But here's the reality: I've seen production incidents cascade because a Lambda assumed a role with `s3:*` permissions, or a contractor's access wasn't revoked after a contract ended. IAM isn't overhead—it's architecture.

Principals — who's making the request

A principal is any entity that can make a request to AWS. Think of it as "who am I right now?" When you call an AWS API, you're representing yourself as a principal. There are three types:

  • IAM Users — Long-lived credentials (Access Key ID and Secret Access Key). Use these sparingly. I assign them to humans doing local development or third-party integrations that genuinely need persistent credentials. Even then, I rotate keys every 90 days and use temporary credentials wherever possible.
  • IAM Roles — Short-lived temporary credentials. This is what your application should use. A Lambda, EC2 instance, or ECS task assumes a role and gets back temporary credentials (access key, secret key, and session token) that expire in hours, not years.
  • Federated users — External identities (SAML, OpenID Connect, temporary credentials from AWS STS). Useful for enterprise setups where you've already got Okta or similar running.

Here's the mental model I use: if it's a machine or an application, it should use a role. If it's a human in development, use a role tied to your AWS SSO or temporary credentials. Never commit an IAM User's long-lived keys to source control or bake them into application code.

In a real example from a ride-hailing platform I built, our backend services (NestJS microservices running on ECS) each assumed their own role. The booking service could read from the user database but had no write access. The payment service could invoke Stripe but couldn't touch ride data. Each service had its own blast radius.

Policies — what actions are allowed

A policy is a JSON document that says "this principal can perform these actions on these resources, under these conditions." It's the engine of IAM.

Here's a simple example—a policy that lets a Lambda read from one S3 bucket:

bash
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-uploads",
        "arn:aws:s3:::my-app-uploads/*"
      ]
    }
  ]
}

Break this down:

  • Effect — Either "Allow" or "Deny". The policy language is permission-granting by default (Allow), but you can explicitly Deny to override inherited permissions.
  • Action — Which API calls this principal can make. `s3:GetObject` means "read a file," `s3:ListBucket` means "list files." You can use wildcards (`s3:*` for all S3 actions), but be specific when you can.
  • Resource — What the actions apply to. Here, a specific S3 bucket and everything in it. Resources are identified by ARN (Amazon Resource Name).

Policies also support conditions. For example, only allow access from a specific IP range, or only during business hours. I've used this to lock down administrative roles—DynamoDB admin access only from the office network, for instance.

The key mental shift: policies are additive until you hit a Deny. If you have three policies attached to a role, and any of them Allow an action, the action is allowed (unless another explicitly Denies it). Start restrictive, then expand as needed.

Roles over users — temporary credentials for applications

This is where most teams get it right or catastrophically wrong.

Long-lived IAM User credentials are the default in AWS, but they're also your biggest operational liability. A developer leaves, their key doesn't get rotated immediately, and six months later you discover it was being used to mine cryptocurrency in your AWS account. I've seen it happen.

Roles solve this. Instead of embedding credentials in your code, you let the application assume a role. AWS handles credential rotation transparently. Your Lambda gets fresh credentials every hour without you doing a thing.

Here's how it works in practice. You create a role with a trust policy that says "EC2 instances can assume me":

bash
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Then you attach a permission policy to the role specifying what it can do. Launch an EC2 instance with this role, and it automatically gets temporary credentials injected into the instance metadata service. Your application reads those credentials without hardcoding anything.

In the SaaS platform I built last year, every microservice got its own role. The user service assumed `UserServiceRole`, which could read/write the users table in DynamoDB. The notification service assumed `NotificationServiceRole`, which could only send SNS messages and read from an SQS queue. If one service got compromised, the attacker had a narrow scope—they couldn't reach the billing database or delete customer data.

For Lambda, it's even simpler. Attach the role when you create the function, and it works. Same for ECS tasks, EC2 instances, and most other AWS services.

Default deny — security by default

Here's the rule that makes everything click: by default, nothing is allowed unless a policy explicitly allows it.

This is called "deny by default" or "principle of least privilege." It means when you create a new IAM user or role, they can't do anything. Not even read their own IAM user details. You have to explicitly grant every permission.

This is actually a feature, not a bug. It forces you to think about what each role actually needs to do. In the trading platform I worked on, the risk team's role could read transaction history and generate reports, but couldn't execute trades or modify compliance rules. If we'd defaulted to "allow everything," it would've been an audit nightmare.

The challenge is figuring out what permissions to grant. AWS publishes managed policies (pre-built bundles like `AmazonDynamoDBReadOnlyAccess`), but I rarely use them as-is. They're too broad. Instead, I use the IAM Access Analyzer to understand what a role actually needs, then write a custom policy.

Here's a workflow I follow:

  1. Create a role with no permissions (except the trust policy so it can be assumed).
  2. Deploy the application and let it fail.
  3. Check CloudTrail to see what API calls failed due to missing permissions.
  4. Add just those permissions to the role.
  5. Repeat until the application works.

This sounds tedious, but it means you end up with least-privilege roles by default. I've also used AWS Access Analyzer Policy Validation to check for overly permissive policies before I deploy them.

Common mistakes

Mistake 1: Using a single overpowered role for everything. I've seen teams give all services admin access because "it was faster to deploy." Then a developer accidentally runs a script that deletes the entire production database. Narrow your roles. It takes 20 minutes upfront and saves you from disasters.

Mistake 2: Embedding IAM User credentials in code. Every framework has a way to read credentials from the environment. Use it. If you're hardcoding an Access Key, you've already lost.

Mistake 3: Not using conditions in policies. If you've got a sensitive operation (delete a table, modify billing settings), restrict it by IP or time of day. A policy without conditions is brittle.

Mistake 4: Forgetting to revoke access when people leave. This is organizational, not technical, but it's critical. Set up a process to disable IAM Users and assume any contractor roles assume an automated policy that expires after 90 days.

Mistake 5: Using wildcard resources in high-risk policies. If your policy allows `logs:DeleteLogGroup`, don't pair it with `Resource: "*"`. Scope it to specific log groups.

Key takeaways

  • IAM fundamentals: principals (who), policies (what they can do), resources (what they're doing it to). Master this model and everything else clicks.
  • Never use long-lived IAM User credentials for applications. Use roles, which give temporary credentials and rotate automatically.
  • Write policies that grant minimum necessary permissions. Default deny is your friend.
  • Roles are per-application or per-team. Each microservice gets its own role with its own blast radius.
  • Use AWS CloudTrail to audit who did what, and IAM Access Analyzer to validate your policies before deploying.
  • Rotating credentials and revoking access are operational processes, not just technical configurations. Build them into your team workflow.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles