IAM: Identity and Access Management Is Everywhere
IntroductionDev EnvironmentClientTransportServerAppDataMore

IAM: Identity and Access Management Is Everywhere

Layer 0 in the HTTP Request Journey

When developers hear "IAM," they usually picture the AWS console — a dense wall of JSON policies, trust relationships, and role ARNs. It's intimidating, and it feels like something you deal with when deploying to cloud infrastructure, not when writing application code.

Here's the thing: IAM is a pattern, not a product. The pattern is straightforward — who gets access to what, how that access is granted explicitly, and how you track its use. That pattern appears at every level of your stack. Your GitHub organization. Your cloud provider. Your database. Your internal services. The AWS console is just one place where you configure it.

Getting this right matters for a mundane reason: access control failures are consistently among the most common causes of data breaches — not dramatic zero-days, but quiet misconfigurations. An ex-employee's API key that was never rotated. A service account with admin access that only needed to read a single bucket. A database user that could drop tables because the developer copy-pasted a connection string from a setup guide and nobody questioned it.

The good news is that the principles are simple and the same everywhere.


The One Principle That Runs Through Everything

Least privilege — every identity (a person, a service, an automated process) should have access to exactly what it needs to do its job, and nothing more.

Not "probably won't need this but it's easier to grant it now." Exactly what's needed.

This sounds obvious and yet it's consistently violated, for understandable reasons. Broad access is faster to configure. When you're setting up a project, you want things to work, not to spend an hour determining the minimal IAM policy for your CI pipeline. The problem is that access tends to accumulate and stay. Nobody revokes it. And one day you have an environment where the credential used to run tests can also delete your production database.

The complementary practices: explicit grants (access is denied by default, explicitly approved when needed) and regular review (access that made sense six months ago may not make sense today).


Team-Level IAM: Repository and Organization Permissions

Let's start close to home, with the system most developers use every day: version control.

On GitHub — and the same model applies to GitLab and Bitbucket — access is layered:

Organization roles give a baseline. Members can read all repositories in the org by default. Owners have full admin across everything. Most developers should be members, not owners. Owners should be a small, deliberate group.

Teams let you grant specific permissions to groups of people for specific repositories. A backend team gets write access to the API repo. A data team gets read access to the schema migrations. Teams are much easier to maintain than individual collaborator grants — when someone joins the backend team, they automatically get the right access; when they leave the team, it's gone.

Individual collaborators are useful for external contributors or contractors. Use them sparingly, with explicit end dates in mind.

Two things that routinely get missed:

Offboarding — when someone leaves the organization, their access should be revoked immediately. Not "when someone gets around to it." Immediately. A departed employee's GitHub account with write access to your codebase is an active risk that costs nothing to fix. Most version control platforms let you automate this through SSO (Single Sign-On) — when the account is deprovisioned in your identity provider, access to all connected services disappears automatically. Worth setting up if you haven't.

External collaborators — contractors, vendors, freelancers. Easy to grant access to, easy to forget about. Quarterly: pull the list of external collaborators on your repositories and remove anyone who's no longer active.


Cloud Provider IAM: Where the Stakes Are Highest

Cloud provider IAM is where the most consequential mistakes happen, and where the default temptation is strongest: just use the root account or create one admin user for everything.

The root account (AWS) or super admin (GCP, Azure) has unrestricted access to everything — including deleting all your infrastructure, viewing all your data, and generating credentials that grant those same privileges to others. This account should be used exactly once: to create the first admin user. After that, it stays locked — 2FA enabled, no access keys created, signed out.

Everything else uses roles (AWS, GCP) or service principals (Azure) — identities with explicitly defined permissions attached to them. Not personal user accounts with access keys that never expire.

For a typical small team deploying a web app, you probably need:

Identity What it needs What it doesn't need
Developer Read logs, view resources, access test environments Deploy to production, modify IAM policies
CI (test jobs) Pull container images, read test secrets Any write access to production resources
CI (deploy jobs) Push images, deploy to specific environments Manage IAM, access other environments
Application at runtime Read its own secrets, write to its own storage Access other services' data, admin anything

The goal is that if any single identity is compromised, the blast radius is limited to what that identity could actually do. A compromised CI test token shouldn't be able to push to production. A compromised application service account shouldn't be able to modify IAM policies.

AWS's IAM Access Analyzer has a feature called unused access findings — it shows you which permissions a role was actually used in the past 90 days. It's a practical tool for trimming policies that accumulated more permissions than they need.


Secrets Managers: The Next Step After Environment Variables

Environment variables are better than hardcoding credentials in source code. They're not the end state.

The problems with environment variables at scale:

  • No audit trail. There's no record of which service accessed which credential, or when.
  • No rotation mechanism built in. Changing a credential means redeploying everything that uses it.
  • Visibility. Environment variables are visible to anyone with server access, in process listings, in certain crash dump formats.
  • No fine-grained access control. Any service that shares the same deployment environment can read any env var.

A secrets manager — AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault — addresses all of these:

Access control: only specific roles or service accounts can read specific secrets. Your payment service can read the Stripe key; your email service cannot.

Audit trail: every read of a secret is logged. You can see exactly which service accessed which credential at what time.

Rotation: some secrets managers support automatic rotation — AWS Secrets Manager can rotate database passwords on a schedule, coordinating the change across the database and the application without manual intervention.

Versioning: when a secret is rotated, old versions are retained briefly. Services that fetched the old version continue working until they refresh.

The migration doesn't have to be a big-bang rewrite. Start with the secrets that matter most: database credentials, payment processor API keys, internal service tokens. Environment variables for non-sensitive config — log levels, region names, feature flags — are fine to leave alone.

The Authentication Tokens and API Keys post covers managing credentials at the application level. A secrets manager is the natural infrastructure-level complement to that.


Database IAM: One Service, One User

Most applications connect to their database with a single user that has full access to everything — select, insert, update, delete, create table, drop table, all of it. It's the default because it's simple, and it works.

It also means that a SQL injection vulnerability, a compromised server, or a misconfigured ORM can do maximum damage. If your application user can drop tables, so can the attacker who exploits a flaw in your application.

The practice is straightforward: one database user per role, each with only the permissions that role actually needs.

For a typical web application:

  • app_user — INSERT, SELECT, UPDATE, DELETE on the tables the application works with. No DDL access (no CREATE TABLE, ALTER TABLE, DROP TABLE). This is the user in your production application's connection string.
  • migration_user — Full DDL access for running schema migrations. Only used during deploys, never held by the running application.
  • analytics_user — SELECT-only on a read replica (if you have one), used for reporting queries and data exports. Can't touch the primary.
  • backup_user — Permission to create database snapshots. Nothing else.

If app_user's credentials are compromised, the attacker can read and modify data but can't drop your tables or modify the schema. Still serious — but containable. Combined with the backups practices, you have a path to recovery.

Connection strings are secrets. A connection string contains a username, a password, a hostname, and a database name. Treat it like any other credential: store it in your secrets manager, not in a .env file committed to version control.


Service-to-Service Authentication: Network Location Isn't Authorization

As applications grow, they split into services. A main API that calls an email service, a PDF renderer, an internal search API. Each of those calls is an authentication decision: is this caller authorized to make this request?

A common shortcut: skip authentication for internal services because they're "on the same network" or "inside the VPC." The reasoning is that if traffic can only come from inside the trusted network, the caller is implicitly trusted.

The problem is that "inside the VPC" is not a strong guarantee. If an attacker gets a foothold anywhere in your infrastructure — through a vulnerable dependency, a misconfigured container, a compromised CI runner — they can make requests to your internal services from inside the network just as easily as your legitimate services can.

The options for service-to-service authentication:

Per-service API keys — each service has its own key. Easy to set up, provides auditability (you know which service made which call), and keys can be rotated independently. Store them in your secrets manager, not in environment variables.

OIDC / short-lived tokens — the same mechanism described in the CI/CD security post. Services request short-lived tokens from an identity provider and present them to other services for validation. Nothing long-lived to rotate or leak. More infrastructure to set up, but significantly stronger.

Mutual TLS (mTLS) — each service has a certificate, and both sides of every connection verify each other's identity. Strong guarantees, but operationally complex. Worth considering if you're running a service mesh (Istio, Linkerd). Overkill for most early-stage applications.

For most small teams: per-service API keys stored in a secrets manager is a reasonable middle ground. Better than no authentication, less complex than mTLS. The key point is that authentication is happening at all.


Access Review and Audit Logs

IAM isn't a one-time setup. Access that was correct six months ago may not be correct today. The pattern that keeps it accurate:

Regular access review — once a quarter, go through: repository permissions (especially external collaborators), cloud IAM roles and the identities attached to them, database users and their privileges, service account credentials. Remove anything that's no longer needed. This is tedious, but it takes an hour and closes gaps that accumulate quietly.

Access key ages — cloud providers show when an access key was created and when it was last used. A key that's more than 90 days old and hasn't been rotated is a signal. AWS IAM and GCP both surface this. Rotate credentials on a schedule, not just after an incident.

Dormant credentials — a service account with an access key that hasn't been used in 60 days is likely orphaned. Either it belongs to an inactive service or a user who left. Disabled until proven otherwise.

Audit logs — AWS CloudTrail, GCP Cloud Audit Logs, and Azure Monitor capture every API call made with your credentials: what was accessed, when, by which identity, from which IP. These logs are invaluable during an incident. They're also useful for routine review — seeing unexpected access to a secret at 3am is exactly the kind of signal that these logs exist to surface.


Quick Checklist

  • Root / super admin cloud accounts have no access keys, MFA enabled, not used for day-to-day operations
  • Each cloud identity (CI, app, developer) has its own role with minimum required permissions
  • Sensitive credentials stored in a secrets manager, not environment variables
  • Database has separate users per role — app user has no DDL access
  • Connection strings treated as secrets
  • Internal services authenticate to each other; network location alone isn't authorization
  • Repository permissions reviewed after team changes; external collaborators audited quarterly
  • Cloud access keys rotated on a schedule; dormant credentials disabled
  • Audit logs enabled for cloud provider API calls

The same pattern — least privilege, explicit grants, regular review — shows up at every layer described in this series. For your users and application permissions, it's RBAC covered in Role-Based Access Control for Beginners and Implementing Basic Permission Systems. For your deployment pipeline, it's the token scoping and OIDC setup from the CI/CD security post. The implementations differ; the principle is the same.


Sources: AWS IAM Best Practices, OWASP Access Control Cheat Sheet, GCP IAM Best Practices