-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy path412-aws-iam.mdc
More file actions
407 lines (313 loc) · 14.7 KB
/
Copy path412-aws-iam.mdc
File metadata and controls
407 lines (313 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
---
title: AWS IAM (Principals, Policy Evaluation, STS, SCPs, KMS)
description: AWS-specific IAM guidance covering principal types, policy evaluation, cross-account assume role patterns, SCPs, KMS key policies/grants, and AccessDenied debugging.
priority: 412
alwaysApply: false
files:
include:
- "**/*iam*.*"
- "**/*sts*.*"
- "**/*assume*role*.*"
- "**/*trust*policy*.*"
- "**/*scp*.*"
- "**/*organizations*.*"
- "**/*kms*.*"
- "**/*.tf"
- "**/*.yaml"
- "**/*.yml"
---
# AWS IAM (Principals, Policy Evaluation, STS, SCPs, KMS)
## Scope
This rule is AWS-specific and complements:
- `315-iam.mdc` (identity and auth concepts across IAM/OIDC/SAML/PKI/PAM)
- `410-aws.mdc` (AWS engineering patterns)
- `310-security.mdc` (OWASP and general security)
## Mental model: "Who am I?" vs "What can I do?"
- **Authentication** answers: **who am I?**
- Credentials and identity: IAM user access keys, federated user, or (most commonly) an **assumed role session** via STS
- **Authorization** answers: **what am I permitted to do?**
- Policy evaluation over: identity-based policies, resource-based policies, boundaries, session policy, and org guardrails (SCP)
## IAM request evaluation (what AWS actually decides)
Think in four parts:
- **Principal**: the caller identity (often an STS assumed-role session)
- **Action**: the API operation (`s3:GetObject`, `kms:Decrypt`, `iam:PassRole`, ...)
- **Resource**: the ARN(s) the action applies to
- **Context**: condition keys (source VPC, principal tags, org ID, MFA, request region, ...)
### Policy layers (common sources of confusion)
AWS authorization can include all of the below at once:
- **Identity-based policies** (attached to users/roles)
- **Resource-based policies** (S3 bucket policy, KMS key policy, Lambda permission, API Gateway resource policy)
- **Permission boundary** (limits the maximum permissions a role can exercise)
- **Session policy** (optional policy passed at `AssumeRole` time, further restricting the session)
- **SCP (Organizations)** (limits maximum permissions in an account/OU; does not grant)
> [!IMPORTANT]
> **Explicit deny wins.** A single applicable `Deny` in any policy layer blocks the request.
## Principal types (be precise)
Use the right terms in docs and debugging:
- **IAM user**: long-lived identity (avoid for automation)
- **IAM role**: identity intended to be assumed (workloads + automation)
- **Assumed role session**: the *runtime principal* after STS (`arn:aws:sts::ACCOUNT:assumed-role/RoleName/SessionName`)
- **Federated user**: temporary identity via federation mechanisms
- **AWS account root principal**: `arn:aws:iam::ACCOUNT:root` (represents the account, not a human)
- **Service principal**: `service.amazonaws.com` (used in trust policies; not a KMS grant grantee)
## Roles, trust policies, and STS (assume role)
### Trust policy is the "who can assume me?"
Common anti-patterns:
- Trusting `Principal: "*"` or broad external accounts without conditions
- Missing `sts:ExternalId` for third-party cross-account integrations where the confused deputy risk applies
### EKS Pod Identity: namespace scoping via session tags (ABAC)
EKS Pod Identity assumes roles using the service principal `pods.eks.amazonaws.com` and uses `sts:TagSession` to attach **session tags** like:
- `kubernetes-namespace`
- `kubernetes-service-account`
Those session tags are available as **principal tags** (`aws:PrincipalTag/...`) for authorization decisions.
This enables a clean pattern for multi-tenant controls:
- **Trust policy** restricts *which* namespace/serviceaccount can assume the role
- **Downstream resource policies** (for example, VPC Lattice auth policies) restrict access based on `aws:PrincipalTag/kubernetes-namespace`
Docs:
- EKS Pod Identity trust policy and request tags: `https://docs.aws.amazon.com/eks/latest/userguide/pod-id-role.html`
- EKS Pod Identity session tags (ABAC): `https://docs.aws.amazon.com/eks/latest/userguide/pod-id-abac.html`
- VPC Lattice auth policies + principal tags: `https://docs.aws.amazon.com/vpc-lattice/latest/ug/auth-policies.html`
#### Example: trust policy (restrict by namespace + service account)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEksAuthToAssumeRoleForPodIdentity",
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"],
"Condition": {
"StringEquals": {
"aws:RequestTag/kubernetes-namespace": ["my-namespace"],
"aws:RequestTag/kubernetes-service-account": ["my-service-account"]
}
}
}
]
}
```
#### Example: VPC Lattice auth policy (restrict invoke by namespace tag)
> [!IMPORTANT]
> VPC Lattice invoke uses the action prefix `vpc-lattice-svcs` (not `vpc-lattice`).
> See AWS managed policy `VPCLatticeServicesInvokeAccess`: `https://docs.aws.amazon.com/aws-managed-policy/latest/reference/VPCLatticeServicesInvokeAccess.html`
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowNamespace",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT:role/multi-tenant-lattice-role" },
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "arn:aws:vpc-lattice:REGION:ACCOUNT:service/svc-0123456789abcdef0/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/kubernetes-namespace": "my-namespace"
}
}
}
]
}
```
#### Good baseline trust policy (workload identity / OIDC)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE:aud": "sts.amazonaws.com",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE:sub": "system:serviceaccount:default:my-service"
}
}
}
]
}
```
#### Cross-account access (third party) - ExternalId
If a vendor assumes a role in your account, require an `ExternalId` and lock to the vendor's principal.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/vendor-access" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "REPLACE_WITH_CUSTOMER_GENERATED_ID" }
}
}
]
}
```
> [!NOTE]
> Use `ExternalId` when you are protecting against the **confused deputy** problem (common in SaaS integrations).
#### Cross-service confused deputy prevention (resource policies): `aws:SourceAccount` / `aws:SourceArn`
When a **resource-based policy** grants access to an **AWS service principal** (for example,
`cloudtrail.amazonaws.com`, `events.amazonaws.com`, `sns.amazonaws.com`), the policy is trusting the
service principal - not the human/role that configured the calling service.
To reduce confused-deputy risk, restrict the service principal with global condition keys:
- `aws:SourceAccount` (allow only when acting on behalf of a specific AWS account)
- `aws:SourceArn` (allow only when acting on behalf of a specific AWS resource ARN)
AWS guidance: `https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html`
Example (CloudTrail writing to S3; restrict by source account):
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudTrailAclCheck",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket1",
"Condition": {
"StringEquals": { "aws:SourceAccount": "111122223333" }
}
}
]
}
```
> [!IMPORTANT]
> Prefer **both** `aws:SourceAccount` and `aws:SourceArn` where supported by the calling service.
> Check the specific service’s docs for supported keys.
## VPC Lattice + `AWS_IAM` + SigV4 - the AWS-native workload-identity pattern
For AWS workload-to-workload HTTPS calls, this is the Zero Trust default before reaching for SPIRE/mTLS. See `318-workload-identity.mdc` for the cross-cloud comparison; this section is the AWS-IAM-side mechanics.
### Four enforcement layers (all must pass)
```mermaid
flowchart LR
src["Source workload<br/>(Lambda / ECS / EKS / EC2)"]
src -->|"1. IAM identity policy<br/>vpc-lattice-svcs:Invoke"| iam[IAM evaluation]
iam -->|"allowed"| sig["2. SigV4 signature<br/>service: vpc-lattice-svcs"]
sig -->|"3. Network path<br/>via VPC association"| lat["VPC Lattice service"]
lat -->|"4. Lattice auth policy<br/>resource-based"| origin["Origin<br/>(ALB / NLB / Lambda / Fargate)"]
```
1. **IAM identity policy** on the caller principal grants `vpc-lattice-svcs:Invoke` on the target service ARN.
2. **SigV4 signature** with signing name `vpc-lattice-svcs`, region of the service, payload `UNSIGNED-PAYLOAD` (Lattice does not currently support payload signing).
3. **Network path** - caller reaches the service via its VPC association. No internet exposure required.
4. **Lattice auth policy** (resource-based, attached to the service or service network) explicitly permits the caller principal.
### Why this is legitimately Zero Trust
- Short-lived credentials via STS (mitigates NHI7 - long-lived secrets).
- No static keys to leak (mitigates NHI2).
- IAM principal is attested by the cloud control plane - the workload can't forge its identity any more than a SPIRE-attested pod can.
- Authorization happens before reaching the origin.
- Traffic stays on the AWS private network; no internet hairpin.
### Caller IAM identity policy
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "arn:aws:vpc-lattice:us-east-1:111122223333:service/svc-abc/*"
}]
}
```
Attach to the workload's IAM role (Lambda execution role / ECS task role / IRSA / Pod Identity / EC2 instance role).
### Lattice auth policy (resource-based)
Default-deny by absence of allow statements. Allow specific caller principals:
```json
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowPaymentsService",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/payments-service" },
"Action": "vpc-lattice-svcs:Invoke",
"Resource": "arn:aws:vpc-lattice:us-east-1:111122223333:service/svc-abc/*"
}]
}
```
Layered conditions (recommended):
- `aws:SourceVpc` - require traffic from a specific VPC.
- `aws:PrincipalTag/...` - ABAC for multi-tenant Lattice (see existing example earlier in this rule).
- `aws:VpcSourceIp` - belt-and-suspenders source range.
### SigV4 from the workload
Most SDKs do this transparently - just point the HTTP client at the Lattice service URL with credentials from the default chain.
```python
# Python (boto3 + botocore SigV4 signer)
import boto3
import botocore.auth
import botocore.awsrequest
import requests
session = boto3.Session()
creds = session.get_credentials().get_frozen_credentials()
req = botocore.awsrequest.AWSRequest(
method="POST",
url="https://my-svc-abc.7d67968.vpc-lattice-svcs.us-east-1.on.aws/orders",
data='{"customer_id": "c_123"}',
headers={"content-type": "application/json"},
)
botocore.auth.SigV4Auth(creds, "vpc-lattice-svcs", "us-east-1").add_auth(req)
resp = requests.request(method=req.method, url=req.url, headers=dict(req.headers), data=req.body)
```
Go, Node, and Bash equivalents follow the same shape: load creds from the default chain, sign with `vpc-lattice-svcs` + region, send.
### When to choose Lattice vs SPIFFE
- **Lattice** for AWS-native workloads inside one account or organization. Fully managed; integrates with existing IAM.
- **SPIFFE** when the platform spans clouds, runs a non-trivial Kubernetes fleet, needs attestation down to image digest, or is an ISV.
- **Both** at the platform level - segmented by workload class. Document the boundary in an ADR. See `318-workload-identity.mdc`.
### Internet machine callers
VPC Lattice does not solve the case where the caller is *outside* AWS (SaaS partners, vendor automation). For that, terminate at the edge with Cloudflare Access service auth tokens (or equivalent) and forward authenticated traffic to a Lattice-fronted origin. See `400-cloudflare.mdc`.
### Anti-patterns
- Wildcard `Principal: *` in a Lattice auth policy "to keep moving" - never.
- IAM identity policy without a corresponding Lattice auth policy entry, or vice versa - both layers must allow.
- Mixing IAM auth (SigV4) callers with `AWS_IAM`-disabled (`NONE` auth) on the same Lattice service - go all-in on auth or none.
- SigV4 with a payload-signed digest - Lattice expects `UNSIGNED-PAYLOAD`; signing the body breaks the request.
---
## Identity-based vs resource-based policies (quick rules)
- **Identity-based** policies generally do **not** specify `Principal` (it's implicit: the attached user/role)
- **Resource-based** policies **must** specify `Principal` (who is allowed to access the resource)
- Cross-account access is frequently easiest with:
- a target resource policy + caller role, or
- assume-role into the target account + identity-based permissions
## Organizations + SCPs (guardrails)
- SCPs define **maximum available permissions** in an account/OU
- SCPs **do not grant** permissions
- Use SCPs for invariants like:
- deny disabling logging
- deny leaving approved regions (`aws:RequestedRegion`)
- deny IAM privilege escalation paths in non-admin OUs
## KMS: key policies, IAM policies, and grants
KMS often surprises people because **the key policy matters**.
- **Key policy**: primary policy for the CMK
- **IAM policy**: may grant KMS actions, but is not sufficient if the key policy does not allow it
- **Grant**: delegated permissions (often for AWS services) with explicit constraints; useful for temporary or scoped access
### Grant grantee principals (gotchas)
A KMS grant grantee can be:
- AWS account root principal
- IAM user
- IAM role
- assumed role user (STS session)
- federated user
But typically **not**:
- IAM group
- AWS Organizations / OU
- service principal (`service.amazonaws.com`)
## Debugging `AccessDenied` (fast path)
When you see `AccessDenied`:
1. **Identify the runtime principal** (often STS assumed-role ARN)
2. Confirm the **action** and **resource ARN** in the error
3. Check for an **explicit deny** in:
- identity policy
- resource policy
- permission boundary
- session policy
- SCP
4. For cross-account: verify both sides:
- trust policy allows assume
- permissions allow the API call
- resource policy (if any) allows the caller principal
5. For KMS: verify key policy + IAM + grant constraints
Useful commands:
```bash
aws sts get-caller-identity
aws iam simulate-principal-policy --policy-source-arn "$ROLE_ARN" --action-names "s3:GetObject" --resource-arns "$RESOURCE_ARN"
```