{"id":"l-aws-2-1","courseId":"aws-cloud-foundations","lang":"en","kind":"lesson","summary":"Tell IAM users, groups and roles apart, know where identity-based and resource-based policies attach, and predict whether AWS allows a request using the deny-allow-deny rule.","html":"<p>At the end of section one you ran <code dir=\"ltr\">aws sts get-caller-identity</code> and AWS answered. Behind that answer, and behind every other call, sits <strong>AWS Identity and Access Management (IAM)</strong>. It answers one question billions of times a day: is <em>this</em> principal allowed to do <em>this</em> action on <em>this</em> resource, right now? Lantern’s old server had one answer for everything: whoever had the SSH key could do anything. This lesson gives you the vocabulary to do better.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Anatomy of a request</h2>\n<p>Every request to AWS, from the console, the CLI or code, carries four things:</p>\n<ul>\n<li><strong>Principal</strong>: who is asking. A user, a role session, or an AWS service.</li>\n<li><strong>Action</strong>: what they want, written <code dir=\"ltr\">service:Operation</code>, such as <code dir=\"ltr\">s3:GetObject</code> or <code dir=\"ltr\">dynamodb:PutItem</code>.</li>\n<li><strong>Resource</strong>: what they want it on, identified by an <strong>ARN</strong> (Amazon Resource Name).</li>\n<li><strong>Context</strong>: everything else IAM can see, such as the source IP address, the time, whether MFA was used, or whether the request used TLS.</li>\n</ul>\n<p>ARNs follow the pattern <code dir=\"ltr\">arn:partition:service:region:account-id:resource</code>. Some parts are empty when they don’t apply:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Text</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code>arn:aws:iam::111122223333:user/amara\narn:aws:s3:::lantern-posters/2026/street-food-festival.jpg\narn:aws:dynamodb:eu-west-1:111122223333:table/lantern-rsvps</code></pre></div>\n<p>IAM is global, so the user ARN has no Region. S3 bucket names are unique across all of AWS, so an S3 ARN has neither Region nor account. DynamoDB tables are regional and belong to an account, so their ARN has both. You will write ARNs constantly in the next lesson.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The identities</h2>\n<p><strong>Users</strong> are long-term identities for one person or, in older setups, one application. A user can have a console password and up to two access keys. Users are the oldest part of IAM, and in section one you created exactly one, your admin.</p>\n<p><strong>Groups</strong> are collections of users. You attach policies to a group, and every member gets them. Lantern creates <code dir=\"ltr\">Admins</code>, <code dir=\"ltr\">Developers</code> and <code dir=\"ltr\">Billing</code>, so that when a volunteer joins or leaves, you change one membership instead of hunting through individual permissions. Groups are not principals: you can’t sign in as a group or name one in a bucket policy.</p>\n<p><strong>Roles</strong> are identities with permissions but <strong>no long-term credentials</strong>. A trusted principal assumes a role, and AWS Security Token Service (STS) hands back temporary credentials that expire. A role’s <strong>trust policy</strong> says who may assume it: a Lambda function, an EC2 instance, a user in another account, or people signed in through IAM Identity Center. Roles are how code gets permissions without secrets, and lesson three of this section builds on them.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> iam</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> create-group</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --group-name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> Developers</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> iam</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> attach-group-policy</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --group-name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> Developers</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --policy-arn</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> arn:aws:iam::aws:policy/ReadOnlyAccess</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> iam</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> add-user-to-group</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --group-name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> Developers</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --user-name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> sam</span></span></code></pre></div>\n<h2 id=\"sec-3\" tabindex=\"-1\">Where policies live</h2>\n<p>A <strong>policy</strong> is a JSON document of statements that allow or deny actions on resources. Where it is attached changes how it reads:</p>\n<ul>\n<li><strong>Identity-based policies</strong> attach to a user, group or role and describe what <em>that identity</em> may do. They come as AWS managed policies (like <code dir=\"ltr\">ReadOnlyAccess</code>), customer managed policies you write and reuse, or inline policies embedded in a single identity.</li>\n<li><strong>Resource-based policies</strong> attach to a resource and name the <strong>principal</strong> they apply to. An S3 bucket policy, a Lambda function’s resource policy and a role’s trust policy are all resource-based.</li>\n</ul>\n<p>Within one account, a request is allowed if either kind of policy allows it and nothing denies it. Across accounts, both sides must allow it.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">How IAM decides</h2>\n<p>The evaluation rule fits on an index card, and it explains nearly every AccessDenied error you will ever see.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 660 250\" role=\"img\" aria-labelledby=\"l-aws-2-1-t1\"><defs><marker id=\"arrow-l-aws-2-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-aws-2-1-t1\">Flowchart. A request first checks for any explicit deny; if found the result is deny. Otherwise it checks for any allow; if found the result is allow, otherwise the result is an implicit deny.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"96\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"80\" y=\"129\" text-anchor=\"middle\">Request</text>\n  <path class=\"d-arrow\" d=\"M140 124 L186 124\" marker-end=\"url(#arrow-l-aws-2-1-1)\"/>\n  <rect class=\"d-box-warn\" x=\"190\" y=\"90\" width=\"150\" height=\"68\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"265\" y=\"119\" text-anchor=\"middle\">Explicit Deny</text>\n  <text class=\"d-label\" x=\"265\" y=\"139\" text-anchor=\"middle\">anywhere?</text>\n  <path class=\"d-arrow\" d=\"M265 90 L265 50\" marker-end=\"url(#arrow-l-aws-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"278\" y=\"74\">yes</text>\n  <rect class=\"d-box-warn\" x=\"200\" y=\"10\" width=\"130\" height=\"38\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"265\" y=\"35\" text-anchor=\"middle\">Denied</text>\n  <path class=\"d-arrow\" d=\"M340 124 L386 124\" marker-end=\"url(#arrow-l-aws-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"352\" y=\"114\">no</text>\n  <rect class=\"d-box-primary\" x=\"390\" y=\"90\" width=\"150\" height=\"68\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"465\" y=\"119\" text-anchor=\"middle\">Any Allow</text>\n  <text class=\"d-label\" x=\"465\" y=\"139\" text-anchor=\"middle\">matches?</text>\n  <path class=\"d-arrow\" d=\"M540 124 L580 124\" marker-end=\"url(#arrow-l-aws-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"546\" y=\"114\">yes</text>\n  <rect class=\"d-box-success\" x=\"584\" y=\"105\" width=\"70\" height=\"38\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"619\" y=\"130\" text-anchor=\"middle\">Allowed</text>\n  <path class=\"d-arrow\" d=\"M465 158 L465 196\" marker-end=\"url(#arrow-l-aws-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"476\" y=\"182\">no</text>\n  <rect class=\"d-box\" x=\"380\" y=\"200\" width=\"170\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"465\" y=\"225\" text-anchor=\"middle\">Implicitly denied</text>\n</svg><figcaption>Explicit deny wins, then any allow, otherwise the default deny</figcaption></figure>\n<ol>\n<li><strong>Default deny.</strong> Every request starts denied.</li>\n<li><strong>Explicit deny wins.</strong> If any applicable policy has a matching <code dir=\"ltr\">Deny</code>, the answer is no. Nothing can override it.</li>\n<li><strong>Explicit allow permits.</strong> Otherwise, if a policy has a matching <code dir=\"ltr\">Allow</code>, the answer is yes.</li>\n<li><strong>No match means no.</strong> If nothing allowed it, the default deny stands. This is called an implicit deny.</li>\n</ol>\n<p>Larger setups add further layers that can only narrow what is allowed, such as service control policies from AWS Organizations and permissions boundaries on individual users and roles. The rule above still holds; those layers add more places a deny can come from.</p>\n<p>You can ask IAM to evaluate a request without making it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> iam</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> simulate-principal-policy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --policy-source-arn</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> arn:aws:iam::111122223333:user/sam</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --action-names</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3:DeleteBucket</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --resource-arns</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> arn:aws:s3:::lantern-posters</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --query</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"EvaluationResults[].EvalDecision\"</span></span></code></pre></div>\n<p>The answer is <code dir=\"ltr\">allowed</code>, <code dir=\"ltr\">explicitDeny</code> or <code dir=\"ltr\">implicitDeny</code>, which tells you not only <em>whether</em> but <em>why</em>.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>One shared IAM user for the whole team</span></p>\n<p>Lantern’s old habit was one SSH key passed around. The cloud version is one IAM user whose password everyone knows. You lose any record of who did what, you can’t remove one person’s access without rotating everyone’s, and MFA ends up on one person’s phone. One identity per human, permissions through groups, always.</p>\n</aside>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Cost of IAM</span></p>\n<p>IAM itself has no charge. The cost of getting it wrong is what shows up: a leaked credential running someone else’s crypto miner on your bill. Tight permissions are a cost control as much as a security one.</p>\n</aside>\n<p>You can now read who is asking and how the decision is made. Next you write the policies themselves, line by line.</p>\n","toc":[{"id":"sec-1","text":"Anatomy of a request","level":2},{"id":"sec-2","text":"The identities","level":2},{"id":"sec-3","text":"Where policies live","level":2},{"id":"sec-4","text":"How IAM decides","level":2}],"takeaways":["Every AWS request carries a principal, an action, a resource ARN and a context, and IAM evaluates all four before anything happens.","Users are long-term identities, groups are a way to give users the same policies, and roles are identities with no long-term credentials that trusted principals assume for temporary credentials.","Identity-based policies attach to users, groups and roles; resource-based policies, such as S3 bucket policies, attach to the resource and name a principal.","Everything is denied by default, an explicit allow is needed to permit a request, and an explicit deny anywhere always wins."],"further":[{"title":"IAM identities (users, user groups, and roles)","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/id.html"},{"title":"Policy evaluation logic","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html"},{"title":"IAM identifiers (ARNs)","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-identifiers.html"}],"quiz":[{"q":"<p>Lantern’s developer Sam has no policies that mention DynamoDB. Sam runs <code dir=\"ltr\">aws dynamodb scan --table-name lantern-rsvps</code>. What happens?</p>","options":[{"text":"It succeeds, because nothing denies it.","why":"IAM is deny by default. A request needs an explicit allow from some policy, and nothing allows this one."},{"text":"It is denied, because no policy allows the action (an implicit deny).","why":"Correct. With no matching allow, the default deny stands and the CLI returns an AccessDenied error."},{"text":"It succeeds only if Sam signed in with MFA.","why":"MFA matters only when a policy condition checks for it. It cannot create permissions that no policy grants."}],"answer":1},{"q":"<p>A policy allows <code dir=\"ltr\">s3:*</code> on Lantern’s posters bucket for the Developers group, and a second policy explicitly denies <code dir=\"ltr\">s3:DeleteBucket</code> for the same group. A developer tries to delete the bucket. What happens?</p>","options":[{"text":"It is denied, because an explicit deny overrides any allow.","why":"Correct. Deny statements are checked first and always win, which makes them a reliable guardrail."},{"text":"It succeeds, because the more general allow was attached first.","why":"Order of attachment and statement order do not matter. IAM evaluates all applicable policies together."},{"text":"It succeeds, because <code dir=\"ltr\">s3:*</code> is more powerful than a single action.","why":"Wildcards only widen what an allow matches. They never outrank a deny."}],"answer":0},{"q":"<p>Which statement about IAM roles is accurate?</p>","options":[{"text":"A role has its own password, which you share with the team that uses it.","why":"Roles have no password and no long-term access keys. That is their main security advantage."},{"text":"A role is a type of group, so you add users to it.","why":"Groups contain users; roles do not. A principal assumes a role and gets that role’s permissions for a session."},{"text":"A role is assumed by a trusted principal, which receives temporary credentials that expire.","why":"Correct. The role’s trust policy says who may assume it, and AWS STS issues credentials that expire after the session duration."},{"text":"A role can only be used by AWS services, never by people.","why":"People assume roles all the time, for example through IAM Identity Center or when switching roles in the console."}],"answer":2}],"exercise":{"kind":"choice","multiple":true,"options":["sam-get","sam-delete","sam-scan","role-get","group-principal"],"correct":["sam-get","role-get"],"title":"Allowed or denied?","prompt":"<p>All of this happens in Lantern’s single AWS account.</p>\n<ul>\n<li>The <strong>Developers</strong> group (Sam is a member) has an identity-based policy that allows <code dir=\"ltr\">s3:GetObject</code> and <code dir=\"ltr\">s3:DeleteObject</code> on <code dir=\"ltr\">arn:aws:s3:::lantern-posters/*</code>.</li>\n<li>The <strong>lantern-posters</strong> bucket policy explicitly denies <code dir=\"ltr\">s3:DeleteObject</code> to every principal, and allows <code dir=\"ltr\">s3:GetObject</code> to the role <code dir=\"ltr\">lantern-thumbnailer</code>.</li>\n<li>The role <strong>lantern-thumbnailer</strong> has no identity-based policies at all.</li>\n</ul>\n<p>Select every request that AWS <strong>allows</strong>.</p>\n","hints":["Run each request through the order: explicit deny anywhere? then any allow? then default deny.","Inside one account, an allow in either an identity-based or a resource-based policy is enough, as long as nothing denies.","Two requests are allowed. One is allowed by the bucket policy alone."],"explanation":"<p>Sam’s <code dir=\"ltr\">GetObject</code> matches the group’s allow and nothing denies it, so it succeeds. Sam’s <code dir=\"ltr\">DeleteObject</code> is also allowed by the group policy, but the bucket policy’s explicit deny wins. Sam’s DynamoDB scan matches no allow anywhere, so the default deny stands. The thumbnailer role has no identity policies, but the bucket policy names it as a principal and allows <code dir=\"ltr\">GetObject</code>; within one account that resource-based allow is enough. Finally, groups are not principals, so a bucket policy cannot name the Developers group at all; you would grant the group through its identity-based policy instead.</p>\n","checkLabels":{},"optionText":{"sam-get":{"text":"Sam downloads <code dir=\"ltr\">s3://lantern-posters/2026/street-food-festival.jpg</code> with <code dir=\"ltr\">aws s3 cp</code>.","why":"Allowed. The group policy allows <code dir=\"ltr\">s3:GetObject</code> on objects in the bucket and no policy denies it."},"sam-delete":{"text":"Sam deletes an old poster with <code dir=\"ltr\">aws s3 rm</code>.","why":"Denied. The group allows it, but the bucket policy’s explicit deny applies to every principal and an explicit deny always wins."},"sam-scan":{"text":"Sam scans the <code dir=\"ltr\">lantern-rsvps</code> DynamoDB table.","why":"Denied. Nothing allows any DynamoDB action for Sam, so this is an implicit deny."},"role-get":{"text":"A Lambda function running as <code dir=\"ltr\">lantern-thumbnailer</code> reads a poster.","why":"Allowed. The bucket policy is a resource-based policy that names the role and allows the read; within one account that is enough."},"group-principal":{"text":"The bucket policy is edited to allow <code dir=\"ltr\">s3:PutObject</code> with the Developers group as the principal, and Sam then uploads a poster.","why":"Denied, and the policy edit itself fails validation. Groups are not principals, so a resource-based policy cannot name one."}}},"words":806,"readMinutes":4}