IAM Roles, Policies, and VPC Endpoints

Overview

AWS authorization uses several policy types that work together. An identity-based policy says what a user or role can do. A resource-based policy says who can access this resource. A VPC endpoint policy adds a boundary for traffic that goes through a private endpoint.

Policy typeAttached toAnswers
Identity-basedUser, group, roleWhat can this principal do?
Resource-basedS3 bucket, Lambda function, SNS topic, …Who can access this resource?
Trust policyIAM roleWho can assume this role?
VPC endpointGateway / Interface endpointWhat can pass through this endpoint?

See also: IAM Policy Examples and Triage Guide for SCPs, boundaries, and troubleshooting.


Identity-based policies

JSON documents that control which actions an identity (user, group, or role) can perform on which resources, under optional conditions.

Managed vs inline

TypeDescription
AWS managedAWS-maintained; attach to users, groups, roles
Customer managedYour reusable policy; attach to multiple identities
InlineEmbedded on one user, group, or role; deleted with that identity

Grant access to assume a role

To let a user switch into another role, the originating account attaches an identity policy that allows sts:AssumeRole on the target role ARN.

Developers can assume UpdateData

Attach to role Developer (or group Developers):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::111122223333:role/UpdateData"
    }
  ]
}

Analysts cannot assume UpdateData

Attach to role Analyst:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::111122223333:role/UpdateData"
    }
  ]
}

Result: Developers can switch to UpdateData; Analyst cannot. Explicit Deny wins over Allow.

The target role also needs a trust policy allowing the originating account or principal - identity policy alone is not enough.

Switch role (CLI)

aws sts assume-role \
  --role-arn "arn:aws:iam::999999999999:role/UpdateData" \
  --role-session-name "David-ProdUpdate"

Export the temporary credentials from the response:

{
  "Credentials": {
    "AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "SessionToken": "AQoDYXdzEGcaEXAMPLE...",
    "Expiration": "2014-12-11T23:08:07Z"
  }
}
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...

Resource-based policies

JSON documents attached to a resource (not to a user). They grant a specified principal permission to perform actions on that resource.

Evaluation: both the identity policy and the resource policy must allow the action (unless the principal is in the same account and the resource policy alone is sufficient for some services).

S3 bucket - allow an application role to read objects

Use case: A Lambda execution role needs read access to a specific bucket. You can grant it in the bucket policy (resource-based) instead of (or in addition to) the role’s identity policy.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAppRoleRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/MyAppLambdaRole"
      },
      "Action": [
        "s3:ListBucket",
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    }
  ]
}

Lambda - allow API Gateway to invoke a function

Use case: REST API integration invokes a Lambda. The function resource policy trusts API Gateway as principal.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAPIGatewayInvoke",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:111122223333:function:MyFunction",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:us-east-1:111122223333:abc123/*"
        }
      }
    }
  ]
}

Console and CLI (aws lambda add-permission) create the same resource-based statement on the function.


VPC endpoint policies

A resource-based policy on the endpoint controls which principals can use the endpoint and which destination actions/resources are reachable through that endpoint.

Endpoint policies do not replace identity or destination resource policies - they add an extra gate for traffic entering via the endpoint.

S3 Gateway endpoint

Allow access to one bucket

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow-access-to-specific-bucket",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:ListBucket",
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::bucket_name",
        "arn:aws:s3:::bucket_name/*"
      ]
    }
  ]
}

Restrict to a specific IAM role

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow-access-to-specific-IAM-role",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "ArnEquals": {
          "aws:PrincipalArn": "arn:aws:iam::111122223333:role/role_name"
        }
      }
    }
  ]
}

Restrict to callers from one account

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow-callers-from-specific-account",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalAccount": "111122223333"
        }
      }
    }
  ]
}

DynamoDB Gateway endpoint

Deny unless traffic comes from a specific endpoint

Use on the DynamoDB table resource policy (or identity policy) with aws:sourceVpce:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow-access-from-specific-endpoint",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "dynamodb:*",
      "Resource": "arn:aws:dynamodb:us-east-1:111111111111:table/*",
      "Condition": {
        "StringNotEquals": {
          "aws:sourceVpce": "vpce-11aa22bb"
        }
      }
    }
  ]
}

Requests not routed through vpce-11aa22bb are denied, even if other policies allow access.

Restrict endpoint policy to one role or account

Use the same ArnEquals / PrincipalAccount condition patterns as the S3 endpoint examples above.

Allow access to one table via endpoint policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow-access-to-specific-table",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "dynamodb:Batch*",
        "dynamodb:Delete*",
        "dynamodb:DescribeTable",
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Update*"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/table_name"
    }
  ]
}

Common Condition blocks (copy-paste)

Drop these into any statement’s "Condition" key. Pair with your own Effect, Action, Resource, and Principal.

See also: IAM Policy Conditions for full statements and operator reference.

Principal and account

Specific IAM role only

"Condition": {
  "ArnEquals": {
    "aws:PrincipalArn": "arn:aws:iam::111122223333:role/MyAppRole"
  }
}

Any principal in one account

"Condition": {
  "StringEquals": {
    "aws:PrincipalAccount": "111122223333"
  }
}

Principal has a specific IAM tag

"Condition": {
  "StringEquals": {
    "aws:PrincipalTag/Department": "Engineering"
  }
}

Caller is in an AWS Organization

"Condition": {
  "StringEquals": {
    "aws:PrincipalOrgID": "o-abc1234567"
  }
}

Network and transport

HTTPS / TLS only

"Condition": {
  "Bool": {
    "aws:SecureTransport": "true"
  }
}

Allow from specific IP range

"Condition": {
  "IpAddress": {
    "aws:SourceIp": ["203.0.113.0/24", "198.51.100.10/32"]
  }
}

Deny if not from corporate CIDR

"Condition": {
  "NotIpAddress": {
    "aws:SourceIp": "203.0.113.0/24"
  }
}

Traffic must use a specific VPC endpoint

"Condition": {
  "StringEquals": {
    "aws:sourceVpce": "vpce-11aa22bb"
  }
}

Deny if request did not come through the endpoint

"Condition": {
  "StringNotEquals": {
    "aws:sourceVpce": "vpce-11aa22bb"
  }
}

Region, time, and MFA

Restrict API calls to one Region

"Condition": {
  "StringEquals": {
    "aws:RequestedRegion": "us-east-1"
  }
}

Allow only during a time window (UTC)

"Condition": {
  "DateGreaterThan": {
    "aws:CurrentTime": "2026-01-01T08:00:00Z"
  },
  "DateLessThan": {
    "aws:CurrentTime": "2026-01-01T18:00:00Z"
  }
}

Require MFA for this session

"Condition": {
  "Bool": {
    "aws:MultiFactorAuthPresent": "true"
  }
}

MFA must be fresh (within 1 hour = 3600 seconds)

"Condition": {
  "NumericLessThan": {
    "aws:MultiFactorAuthAge": "3600"
  }
}

Source ARN (cross-service)

Lambda invoke only from one API Gateway API

"Condition": {
  "ArnLike": {
    "AWS:SourceArn": "arn:aws:execute-api:us-east-1:111122223333:abc123/*"
  }
}

Event only from one S3 bucket

"Condition": {
  "StringEquals": {
    "AWS:SourceArn": "arn:aws:s3:::my-app-bucket"
  }
}

Source account that owns the calling resource

"Condition": {
  "StringEquals": {
    "aws:SourceAccount": "111122223333"
  }
}

S3-specific

List only objects under a prefix

"Condition": {
  "StringLike": {
    "s3:prefix": ["logs/*", "app/*"]
  }
}

Require SSE-S3 on upload

"Condition": {
  "StringEquals": {
    "s3:x-amz-server-side-encryption": "AES256"
  }
}

Deny upload when encryption header is missing

"Condition": {
  "Null": {
    "s3:x-amz-server-side-encryption": "true"
  }
}

Tags on resources and requests

Resource must have tag

"Condition": {
  "StringEquals": {
    "aws:ResourceTag/Environment": "Production"
  }
}

Deny delete unless resource is tagged Backup=true

"Condition": {
  "StringNotEquals": {
    "aws:ResourceTag/Backup": "true"
  }
}

Create request must include tag

"Condition": {
  "StringEquals": {
    "aws:RequestTag/CostCenter": "12345"
  }
}

Operator cheat sheet

OperatorExample use
StringEqualsExact match - Region, account ID, tag value
StringNotEqualsExclude a value - deny outside endpoint
StringLikeWildcards - ARN prefix, S3 prefix
ArnEquals / ArnLikePrincipal or source ARN
Boolaws:SecureTransport, MFA present
IpAddress / NotIpAddressSource IP CIDR
DateGreaterThan / DateLessThanTime windows
NumericLessThanMFA age, numeric limits
NullKey must be absent ("true")