Yesterday you learned the policy evaluation algorithm. Today we handle the policies themselves that feed into that algorithm. The real differentiator SCS-C03 tests in IAM isn't "can you write policies" but "among multiple controls producing the same outcome, can you pick the most precise and least-privileged one?" If you solve with Resource policy what should be Identity policy, it works but becomes a trap answer. If you split with separate roles what one Condition key could do, it's over-engineered.
Today we dive into three things. First, when Identity and Resource policies split and how to choose. Second, using Condition keys for precise control — IP, MFA, encryption, tag-based. Third, designing least privilege scalably with Permissions Boundary and ABAC.
| Aspect | Identity-based | Resource-based |
|---|---|---|
| Attached to | User/Group/Role | Resources (S3, KMS, SQS, SNS, Lambda, etc.) |
| Principal element | None | Required |
| Main use | "What can this principal do" | "Who can access this resource" |
| cross-account | Can't work alone (needs counterpart resource policy) | Can work alone, allowing other accounts |
| Typical examples | Managed/inline policies | S3 bucket policy, KMS key policy, IAM Role trust policy |
Selection is clear:
💡 Related Theory: KMS key policy is special. Every KMS key's policy is first authority (authoritative) — if the key policy doesn't permit IAM delegation (allowing
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"}), then IAM policy alone won't work. S3 and SQS work with either identity or resource policy alone, but KMS requires the key policy to open the gate first before IAM policy functions. That's why cross-account KMS use mandates explicitly stating the other account in the key policy.
🔍 Deep Dive: An IAM Role's trust policy is also a resource-based policy. It's a Principal-including policy on the Role resource defining "who can assume this role (
sts:AssumeRole)." That's why cross-account AssumeRole requires ① target account's Role trust policy (Principal naming calling account) ② calling side's identity policy withsts:AssumeRoleAllow — both (covered in depth Day 4).
Condition is a clause that adds "additional constraints" to policies. Security engineers use these most-common keys, organized by type:
| Condition Key | Meaning | Typical Use |
|---|---|---|
aws:SourceIp | Request source IP | Allow only from corporate IP |
aws:MultiFactorAuthPresent | MFA authentication presence | Force MFA for sensitive actions |
aws:SecureTransport | HTTPS or not | Reject plaintext HTTP |
aws:RequestedRegion | Target region | Block work outside certain regions |
aws:PrincipalTag / aws:ResourceTag | Principal·resource tags | ABAC (attribute-based access control) |
aws:SourceArn / aws:SourceAccount | Calling service source | Prevent Confused Deputy |
s3:x-amz-server-side-encryption | Upload encryption header | Block unencrypted uploads |
kms:ViaService | KMS call via service | Allow key use only through specific service |
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::secure-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}This bucket policy denies all uploads without a KMS encryption header. Classic data protection (Domain 5) and governance enforcement.
{
"Sid": "DenySensitiveWithoutMFA",
"Effect": "Deny",
"Action": ["iam:*", "kms:ScheduleKeyDeletion", "ec2:TerminateInstances"],
"Resource": "*",
"Condition": {
"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}
}
}⚠️ Trap: The difference between
BoolandBoolIfExistsis exam standard. Some requests (service-to-service calls, STS sessions) don't have theaws:MultiFactorAuthPresentkey at all. UsingBoolto check"false"blocks even normal requests missing the key.BoolIfExistsmeans "if key exists, check if false; if missing, pass" — works as intended. Also,aws:SourceIpdoesn't apply to traffic via VPC endpoints — useaws:VpcSourceIporaws:SourceVpcthere.
🔍 Deep Dive:
aws:SourceArnandaws:SourceAccountare core to preventing Confused Deputy (confused intermediary) attacks. For example, when S3 sends events to SNS, if the SNS topic policy doesn't useaws:SourceArnto specify "only this bucket," another person's bucket could be tricked into triggering your topic. Service principals (Serviceprincipal) should almost always pair withaws:SourceArn/aws:SourceAccountconditions (re-emerges Day 4 in STS context).
Permissions Boundary defines "the maximum permission upper limit this principal can have." Identity policy grants permissions; boundary caps that limit. Effective permission = identity-based policy ∩ Permissions Boundary (intersection).
Most powerful use: safely restricting permission delegation. Give a developer "can create IAM roles" but prevent their created roles from becoming admin.
// Attach to developer: allow role creation but force boundary attachment
{
"Effect": "Allow",
"Action": "iam:CreateRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::ACCOUNT:policy/DevBoundary"
}
}
}Now the developer can create roles but only with DevBoundary attached. That role's effective permissions can't exceed boundary — structurally blocking privilege escalation.
💡 Related Theory: The evaluation relationship of four policy types in one picture — SCP(org upper limit) ∩ Permissions Boundary(principal upper limit) ∩ Identity policy(grant) → and explicit Deny takes priority anywhere. SCP and Boundary are "cutting" filters, not "granting" steps. Session policy (passed on AssumeRole) works the same intersection filter.
RBAC (role-based) creates policy explosion as roles grow. ABAC (Attribute-Based Access Control) uses tags to express permissions, keeping policy count constant.
{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Project": "${aws:PrincipalTag/Project}"
}
}
}This single policy expresses "allow when principal's Project tag matches resource's Project tag." 100 projects → 1 policy. New team appears? No policy changes, just grant tags.
🎯 Scenario: "Dozens of teams operate EC2 separately; policy additions for each new team is hitting limits." Answer isn't multiplying team roles and policies — it's ABAC migration. Tag principals with
team, resources withteam, unify withaws:PrincipalTag/team == aws:ResourceTag/team. Identity Center session tags and SAML/OIDC attributes map to PrincipalTag, applying uniformly to federated users.
Least privilege isn't intuitive; it's derived from real usage data.
# Access Analyzer: Generate policies from actual CloudTrail log usage
aws accessanalyzer start-policy-generation \
--policy-generation-details '{"principalArn":"arn:aws:iam::111122223333:role/AppRole"}' \
--cloud-trail-details '{...}'
# Find unused permissions by last-accessed timestamp
aws iam get-service-last-accessed-details \
--job-id <job-id>IAM Access Analyzer's policy generation creates policies from only actual recorded calls in CloudTrail. Access Advisor (service-last-accessed) shows "permissions never used in last N months" to justify narrowing over-broad access.
📚 Case Study: Many organizations start with policies near
*:*, only narrowing after incidents. Best practice is the opposite: "progressive narrowing — add permissions only when denials occur." Use Access Analyzer-generated policies as a starting point and monitor CloudTrail'sAccessDenied. "Broad first, then narrow" creates security debt; "narrow first, then expand" doesn't.
Checklist to quickly judge multi-policy tangles:
💡 Related Theory: In this order, steps 2-4 (SCP, Boundary, cross-account both) are all "filters that cut upper limits," while step 5 (Identity/Resource Allow) is the "permissions granting" step. Clarifying this distinction lets you quickly pin "permissions exist but denied" as hitting one of those filters (2-4) rather than missing Allow.
Three essentials today. First, Identity policy handles "what principal can do"; Resource policy handles "who accesses this resource" — cross-account and KMS key policies are Resource policy territory. Second, Condition keys provide precise control via IP, MFA, encryption, tags, but watch for BoolIfExists traps and aws:SourceArn Confused Deputy prevention. Third, Permissions Boundary and ABAC make least privilege "scalable," and least privilege emerges from Access Analyzer and Access Advisor data, not intuition.
Tomorrow we shift to STS and temporary credentials. How AssumeRole exactly works, what federation is, role chaining, and how to block Confused Deputy with ExternalId and aws:SourceArn. Today's trust policy and Condition keys are the raw materials.
Click a choice to reveal the answer and explanation.
Question 1
A Lambda function in Account A must decrypt data encrypted with a KMS key managed in Account B. What must be configured?
Question 2
Which of the following conditions is intended to mean "deny if MFA is absent" but risks blocking legitimate requests that have no MFA key at all, such as service-to-service calls?
Question 3
An organization faces the operational burden of adding an IAM policy every time a team is added. What is the most appropriate way to implement "allow only when the principal's team tag matches the resource's team tag" without an explosion in the number of policies?
Question 4
You want to grant a developer permission to create IAM roles while enforcing that the roles they create cannot exceed the ceiling they were given. Which mechanism is most appropriate?
Question 5
You want to enforce that every object uploaded to an S3 bucket is encrypted with KMS. What is the most direct approach with no gaps?