{"id":"l-aws-2-2","courseId":"aws-cloud-foundations","lang":"en","kind":"lesson","summary":"Read any IAM policy statement by statement, write least-privilege policies with exact ARNs, and add conditions such as MFA and TLS that tighten when an allow applies.","html":"<p>In the last lesson you learned how IAM decides. Now you write what it decides <em>from</em>. Lantern needs a policy for the function that receives poster uploads, a policy for the developers, and a bucket policy that protects the posters. All three are the same small JSON language.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Anatomy of a statement</h2>\n<p>Here is the policy for Lantern’s upload function, which saves posters under <code dir=\"ltr\">uploads/</code> and occasionally lists what is there:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">lantern-upload-policy.json</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:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2012-10-17\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Statement\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Sid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"WriteUploads\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Allow\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s3:PutObject\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-posters/uploads/*\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Sid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"ListUploadsOnly\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Allow\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s3:ListBucket\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-posters\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Condition\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        \"StringLike\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"s3:prefix\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"uploads/*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Read it top to bottom:</p>\n<ul>\n<li><code dir=\"ltr\">Version</code> is always <code dir=\"ltr\">&quot;2012-10-17&quot;</code>, the current policy language version. It is a fixed string, not today’s date.</li>\n<li><code dir=\"ltr\">Statement</code> is a list. Each statement is evaluated on its own.</li>\n<li><code dir=\"ltr\">Sid</code> is an optional label. Name what the statement is for; future you will thank you.</li>\n<li><code dir=\"ltr\">Effect</code> is <code dir=\"ltr\">Allow</code> or <code dir=\"ltr\">Deny</code>.</li>\n<li><code dir=\"ltr\">Action</code> is one action or a list, written <code dir=\"ltr\">service:Operation</code>. Wildcards work (<code dir=\"ltr\">s3:Get*</code>) but each one widens the door.</li>\n<li><code dir=\"ltr\">Resource</code> is one ARN or a list. Wildcards work here too.</li>\n<li><code dir=\"ltr\">Condition</code> is optional and narrows when the statement applies.</li>\n</ul>\n<p>Resource-based policies, such as bucket policies, add a <code dir=\"ltr\">Principal</code> element naming who the statement is about. Identity-based policies don’t have one, because the identity they are attached to <em>is</em> the principal.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Bucket ARN or object ARN?</h2>\n<p>The policy above has two different resources on purpose. <code dir=\"ltr\">s3:ListBucket</code> is an operation on the <strong>bucket</strong>, so its resource is <code dir=\"ltr\">arn:aws:s3:::lantern-posters</code>. <code dir=\"ltr\">s3:PutObject</code> is an operation on <strong>objects</strong>, so its resource is the bucket ARN followed by a path: <code dir=\"ltr\">lantern-posters/uploads/*</code>. Swap them and neither statement ever matches, and you get AccessDenied with a policy that looks right.</p>\n<p>The same distinction exists elsewhere. DynamoDB’s <code dir=\"ltr\">dynamodb:Query</code> on a table index needs the index ARN (<code dir=\"ltr\">table/lantern-rsvps/index/by-event</code>), not only the table ARN. When a policy “obviously” allows something and AWS says no, compare the request’s ARN with the policy’s character by character.</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>The convenient wildcard</span></p>\n<p><code dir=\"ltr\">&quot;Action&quot;: &quot;s3:*&quot;, &quot;Resource&quot;: &quot;*&quot;</code> makes every AccessDenied go away, including for the call a bug or an attacker makes to delete another bucket. Wildcards in <strong>both</strong> Action and Resource are almost never right outside an admin role. If you need a wildcard, put it in one place and pin the other.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Least privilege, in practice</h2>\n<p><strong>Least privilege</strong> means granting only the actions and resources a job needs. It sounds slow, but the workflow is quick:</p>\n<ol>\n<li>Write down what the job does in plain words: “write posters to <code dir=\"ltr\">uploads/</code>, list that prefix”.</li>\n<li>Translate each verb into an action, and each noun into an exact ARN.</li>\n<li>Deploy and test. An AccessDenied message names the missing action and resource; add exactly that.</li>\n</ol>\n<p>For existing roles, IAM shows <strong>last accessed</strong> information per service, and IAM Access Analyzer can generate a policy from the actions a role actually used according to AWS CloudTrail. Both are good ways to shrink a policy that grew too wide.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Conditions: when an allow applies</h2>\n<p>A condition block maps <strong>operators</strong> to <strong>keys</strong> and <strong>values</strong>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JSON</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:#0A3069;--shiki-dark:#A5D6FF\">\"Condition\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"StringEquals\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:RequestedRegion\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"eu-west-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"eu-south-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Bool\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:MultiFactorAuthPresent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"true\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Two rules make conditions predictable. Separate keys are <strong>ANDed</strong>: the request must be in one of those Regions <em>and</em> made with MFA. Multiple values for one key are <strong>ORed</strong>: either Region will do.</p>\n<p>Useful keys for Lantern:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Key</th>\n<th>Use it to</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">aws:SecureTransport</code></td>\n<td>refuse plain-HTTP requests</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">aws:MultiFactorAuthPresent</code></td>\n<td>require MFA for risky actions such as deletes</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">aws:SourceIp</code></td>\n<td>limit a human to the office or VPN range</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">aws:RequestedRegion</code></td>\n<td>keep work inside Lantern’s Regions</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">s3:prefix</code></td>\n<td>limit ListBucket to a folder</td>\n</tr>\n</tbody>\n</table></div><p>Conditions also make great <strong>deny</strong> guardrails. Lantern’s bucket policy refuses any request without TLS, from anyone:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">lantern-posters-bucket-policy.json</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:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2012-10-17\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Statement\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Sid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"DenyInsecureTransport\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Deny\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Principal\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s3:*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"arn:aws:s3:::lantern-posters\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"arn:aws:s3:::lantern-posters/*\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Condition\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"Bool\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:SecureTransport\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Because an explicit deny always wins, no allow anywhere can undo it.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Validate before you attach</h2>\n<p>The IAM console’s policy editor runs <strong>IAM Access Analyzer</strong> checks as you type: syntax errors, invalid actions, and security warnings such as a pass-role wildcard. From the terminal:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><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\"> accessanalyzer</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> validate-policy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --policy-type</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> IDENTITY_POLICY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --policy-document</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> file://lantern-upload-policy.json</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\"> \"findings[].[findingType,issueCode]\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --output</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> table</span></span></code></pre></div>\n<p>An empty result means no findings. Fix every <code dir=\"ltr\">ERROR</code> and <code dir=\"ltr\">SECURITY_WARNING</code>, and read each <code dir=\"ltr\">WARNING</code> and <code dir=\"ltr\">SUGGESTION</code>.</p>\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>Policies are code</span></p>\n<p>Keep Lantern’s policies in the repository next to the app, review them in pull requests, and validate them in CI. A policy change can do more damage than a code change, so it deserves at least the same review.</p>\n</aside>\n<p>You can now write tight permissions. Next: how people get them without IAM users, and how code gets them without access keys.</p>\n","toc":[{"id":"sec-1","text":"Anatomy of a statement","level":2},{"id":"sec-2","text":"Bucket ARN or object ARN?","level":2},{"id":"sec-3","text":"Least privilege, in practice","level":2},{"id":"sec-4","text":"Conditions: when an allow applies","level":2},{"id":"sec-5","text":"Validate before you attach","level":2}],"takeaways":["A statement is Effect plus Action plus Resource, optionally narrowed by a Condition; resource-based policies also name a Principal.","Bucket-level actions such as s3:ListBucket take the bucket ARN, while object-level actions such as s3:GetObject need the bucket ARN followed by /* or a narrower path.","Least privilege means listing the specific actions and resources a job needs, then widening only when a real AccessDenied tells you to.","Condition keys are ANDed with each other, and multiple values for one key are ORed.","Validate every policy before attaching it with IAM Access Analyzer, in the console editor or with aws accessanalyzer validate-policy."],"further":[{"title":"IAM JSON policy element reference","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html"},{"title":"AWS global condition context keys","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html"},{"title":"Security best practices in IAM","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html"},{"title":"Validate policies with IAM Access Analyzer","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-validation.html"}],"quiz":[{"q":"<p>A function with this policy calls <code dir=\"ltr\">GetObject</code> on <code dir=\"ltr\">posters/2026/festival.jpg</code> in the <code dir=\"ltr\">lantern-posters</code> bucket and gets AccessDenied. Why?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JSON</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:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Allow\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s3:GetObject\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-posters\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"The Action should be <code dir=\"ltr\">s3:Get*</code> because <code dir=\"ltr\">GetObject</code> is not a real action name.","why":"<code dir=\"ltr\">s3:GetObject</code> is the real action. A wildcard would only broaden the policy without fixing the actual problem."},{"text":"The Resource is the bucket, but GetObject acts on objects, so it needs <code dir=\"ltr\">arn:aws:s3:::lantern-posters/*</code>.","why":"Correct. Object actions match object ARNs. Without <code dir=\"ltr\">/*</code> (or a narrower path) the statement never matches the request."},{"text":"S3 policies must include a Principal element, so the statement is ignored.","why":"Identity-based policies have no Principal; the identity they are attached to is the principal. Only resource-based policies need one."},{"text":"The Version line is missing, so IAM rejects the whole policy.","why":"Without a Version, IAM falls back to the old 2008 language and policy variables stop working, but a simple statement like this still matches."}],"answer":1},{"q":"<p>Which requests does this condition allow?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JSON</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:#0A3069;--shiki-dark:#A5D6FF\">\"Condition\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"StringEquals\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:RequestedRegion\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"eu-west-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"eu-south-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Bool\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:SecureTransport\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"true\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"Requests to either Region, whether or not they use TLS.","why":"The two keys are ANDed, so the TLS check still applies to every request."},{"text":"Requests that use TLS, to any Region.","why":"Different keys are ANDed. The Region key still has to match one of its listed values."},{"text":"Requests to eu-west-1 or eu-south-2 that also use TLS.","why":"Correct. Values inside one key are ORed (either Region) and separate keys are ANDed (and TLS)."}],"answer":2},{"q":"<p>You need a policy for Lantern’s poster-resize function but you are not sure which S3 actions it calls. What is the best starting point?</p>","options":[{"text":"Grant <code dir=\"ltr\">s3:*</code> on <code dir=\"ltr\">*</code> and narrow it later when there is time.","why":"“Later” rarely comes, and in the meantime any bug or compromise in the function can touch every bucket in the account."},{"text":"Grant the actions you know it needs on the exact bucket path, then add actions only when a real AccessDenied shows they are missing.","why":"Correct. AccessDenied errors name the missing action, so widening on evidence is fast and keeps the policy tight."},{"text":"Attach the AWS managed <code dir=\"ltr\">AmazonS3FullAccess</code> policy because AWS maintains it.","why":"AWS maintains the policy text, not its fit for your job. Full access to every bucket is far broader than one function needs."}],"answer":1},{"q":"<p>What does this bucket policy statement do?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JSON</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:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Deny\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Principal\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s3:*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-posters\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-posters/*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Condition\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"Bool\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:SecureTransport\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"Blocks every request to the bucket, including from admins.","why":"The condition limits it to requests where SecureTransport is false, meaning plain HTTP. HTTPS requests are unaffected."},{"text":"Blocks any request made over plain HTTP, from anyone.","why":"Correct. It is a standard guardrail. Combined with the explicit-deny rule, nobody can read or write the bucket without TLS."},{"text":"Allows anonymous users to read the bucket over HTTPS.","why":"A Deny statement never grants anything. Public access would need an Allow and Block Public Access turned off."}],"answer":1}],"exercise":{"kind":"spot-bug","language":"json","code":"{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"ReadWriteRsvps\",\n      \"Effect\": \"Allow\",\n      \"Action\": \"dynamodb:*\",\n      \"Resource\": \"arn:aws:dynamodb:eu-west-1:111122223333:table/lantern-rsvps\"\n    },\n    {\n      \"Sid\": \"ReadPosters\",\n      \"Effect\": \"Allow\",\n      \"Action\": \"s3:GetObject\",\n      \"Resource\": \"arn:aws:s3:::lantern-posters\"\n    },\n    {\n      \"Sid\": \"DeletePostersOnlyWithMfa\",\n      \"Effect\": \"Allow\",\n      \"Action\": \"s3:DeleteObject\",\n      \"Resource\": \"arn:aws:s3:::lantern-posters/*\",\n      \"Condition\": {\n        \"Bool\": { \"aws:MultiFactorAuthPresent\": \"false\" }\n      }\n    }\n  ]\n}","bugLines":[7,14,22],"title":"Review the Developers policy","prompt":"<p>A colleague wrote this identity-based policy for Lantern’s <strong>Developers</strong> group. The intent, from the ticket: developers can <strong>read and write RSVPs</strong> (get, put and query items) in the <code dir=\"ltr\">lantern-rsvps</code> table, <strong>read any poster</strong> in the <code dir=\"ltr\">lantern-posters</code> bucket, and <strong>delete posters only when signed in with MFA</strong>. Select every line that breaks that intent.</p>\n","hints":["Compare each Action with the verbs in the ticket. Is anything broader than asked?","Is <code dir=\"ltr\">s3:GetObject</code> a bucket-level or an object-level action? Check its Resource.","Read the MFA condition out loud: the statement applies when MFA present is … what?"],"explanation":"<p>Line 7 grants <code dir=\"ltr\">dynamodb:*</code>, which includes <code dir=\"ltr\">DeleteTable</code> and <code dir=\"ltr\">UpdateTable</code>; the ticket needs only <code dir=\"ltr\">dynamodb:GetItem</code>, <code dir=\"ltr\">dynamodb:PutItem</code> and <code dir=\"ltr\">dynamodb:Query</code>, so list those. Line 14 uses the bucket ARN for <code dir=\"ltr\">s3:GetObject</code>, an object-level action, so the statement never matches a real download; it needs <code dir=\"ltr\">arn:aws:s3:::lantern-posters/*</code>. Line 22 inverts the condition: it allows deletes only when MFA is <strong>absent</strong>. It should be <code dir=\"ltr\">&quot;aws:MultiFactorAuthPresent&quot;: &quot;true&quot;</code>. Fixed lines:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JSON</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:#0A3069;--shiki-dark:#A5D6FF\">\"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dynamodb:GetItem\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dynamodb:PutItem\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dynamodb:Query\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-posters/*\"</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Bool\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"aws:MultiFactorAuthPresent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"true\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span></code></pre></div>\n<p>Policy validation can flag some of these, but the inverted MFA condition is perfectly valid JSON and a perfectly valid policy. Only reading the policy against the intent catches it.</p>\n","checkLabels":{}},"words":700,"readMinutes":3}