<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[brahim serrhini]]></title><description><![CDATA[brahim serrhini]]></description><link>https://brahimserrhini.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 21 Oct 2026 19:43:57 GMT</lastBuildDate><atom:link href="https://brahimserrhini.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[ RAM, resource policy, cross-account role ou VPC peering ? L'arbre de décision pour partager entre comptes AWS
]]></title><description><![CDATA[Alice a un problème classique. Son équipe gère le compte Account A, qui héberge un Transit Gateway, quelques subnets bien configurés, et le fameux…]]></description><link>https://brahimserrhini.hashnode.dev/ram-resource-policy-cross-account-role-ou-vpc-peering-l-arbre-de-d-cision-pour-partager-entre-comptes-aws</link><guid isPermaLink="true">https://brahimserrhini.hashnode.dev/ram-resource-policy-cross-account-role-ou-vpc-peering-l-arbre-de-d-cision-pour-partager-entre-comptes-aws</guid><dc:creator><![CDATA[Brahim Serrhini]]></dc:creator><pubDate>Thu, 16 Jul 2026 15:41:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a15cfe4da253d50d4cdfe58/a4d4a02c-daa9-41dd-81a6-896997e4eb49.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Alice a un problème classique.</p>
<p>Son équipe gère le compte <strong>Account A</strong>, qui héberge un Transit Gateway, quelques subnets bien configurés, et le fameux <code>reports-bucket</code>. Une nouvelle équipe vient d'arriver avec son propre compte, <strong>Account B</strong>, et elle a besoin d'accéder à tout ça : déployer des instances dans les subnets d'Alice, lire des objets dans son bucket, router son trafic via son Transit Gateway.</p>
<p>Alice ouvre la documentation AWS et découvre qu'il existe au moins <strong>quatre mécanismes</strong> pour "partager entre comptes" : AWS RAM, les resource policies, les cross-account roles, et le VPC peering. Quatre mécanismes, quatre pages de doc différentes, et aucune qui explique clairement <strong>quand utiliser lequel</strong>.</p>
<p>C'est exactement ce qu'on va régler ici, avec un arbre de décision que vous pourrez réutiliser à chaque fois que la question se pose.</p>
<h2>L'arbre de décision</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a15cfe4da253d50d4cdfe58/8572dc93-28e7-4262-b859-74bdeedb5c7c.png" alt="" style="display:block;margin:0 auto" />

<p>La logique tient en une question d'entrée : <strong>qu'est-ce que Account B a besoin de faire, exactement ?</strong></p>
<ol>
<li><p><strong>Ses ressources doivent communiquer au niveau réseau avec celles d'Account A</strong> (une instance dans B doit atteindre une base dans A) → c'est un problème de <strong>connectivité</strong> : VPC Peering, Transit Gateway ou PrivateLink.</p>
</li>
<li><p><strong>Il doit utiliser une infrastructure qui vit dans Account A</strong> (déployer dans un subnet d'Alice, s'attacher à son Transit Gateway) → c'est un problème de <strong>partage d'infrastructure</strong> : AWS RAM.</p>
</li>
<li><p><strong>Il doit appeler l'API d'une ressource d'Account A</strong> (lire un objet S3, déchiffrer avec une clé KMS, publier dans une queue SQS) → c'est un problème d'<strong>accès API</strong> : resource policy ou cross-account role. Trois familles de besoins, trois familles de solutions. Le reste de l'arbre affine à l'intérieur de chaque branche.</p>
</li>
</ol>
<h2>Branche 1 — Connectivité réseau</h2>
<p>Si le besoin est que des ressources se parlent au niveau IP, aucun des mécanismes IAM ne vous aidera. Trois options selon la topologie :</p>
<ul>
<li><p><strong>Deux VPC qui doivent se parler, point.</strong> → VPC Peering. Simple, gratuit en intra-région (hors data transfer), mais non transitif : si A est peeré avec B et B avec C, A ne voit pas C.</p>
</li>
<li><p><strong>Plusieurs VPC, plusieurs comptes, une topologie hub-and-spoke.</strong> → Transit Gateway. C'est le routeur central. Et détail intéressant : le TGW lui-même se partage entre comptes... via RAM. Les branches de l'arbre se rejoignent.</p>
</li>
<li><p><strong>Exposer un seul service, sans ouvrir tout le réseau.</strong> → PrivateLink. Account A expose un endpoint service, Account B crée un interface endpoint. Aucune table de routage partagée, surface d'attaque minimale.</p>
</li>
</ul>
<h2>Branche 2 — Partager l'infrastructure : AWS RAM</h2>
<p>AWS Resource Access Manager permet à Account A de <strong>partager la ressource elle-même</strong> avec Account B. Pas une copie, pas un accès API : la ressource apparaît dans la console d'Account B comme si elle y vivait.</p>
<p>Le cas d'usage roi : les <strong>subnets partagés</strong>. Alice garde le contrôle du VPC (routing, NACLs, flow logs) dans Account A, et l'équipe d'Account B déploie ses instances dedans. Séparation nette entre l'équipe réseau et les équipes applicatives — le pattern de landing zone le plus propre que je connaisse.</p>
<p>Deux choses que la documentation ne met pas assez en avant :</p>
<p><strong>1. RAM ne partage pas n'importe quoi.</strong> La liste des types supportés est précise : subnets, Transit Gateways, règles Route 53 Resolver, prefix lists, configurations License Manager, et quelques autres. Un bucket S3 ou une table DynamoDB ne se partagent <strong>pas</strong> via RAM. Si votre ressource n'est pas dans la liste, retournez à la branche 3.</p>
<p><strong>2. Le partage intra-Organization doit être activé explicitement.</strong> C'est le piège dans lequel je suis tombé en montant le lab qui a inspiré cet article. Si Account A et Account B sont dans la même AWS Organization, le partage peut être automatique — pas d'invitation à accepter côté B. Mais seulement si quelqu'un a d'abord exécuté :</p>
<pre><code class="language-bash">aws ram enable-sharing-with-aws-organization
</code></pre>
<p>Sans ça, même en intra-Org, chaque partage passe par le flux d'invitation manuel, et vous allez perdre vingt minutes à chercher pourquoi "ça ne marche pas". Vécu.</p>
<h2>Branche 3 — Accès API : resource policy ou cross-account role ?</h2>
<p>C'est ici que vivent les cas les plus fréquents : S3, KMS, SQS, SNS, Lambda, Secrets Manager. Deux mécanismes, et le choix entre les deux est plus structurant qu'il n'y paraît.</p>
<h3>Option A — Resource policy</h3>
<p>Account A attache une policy directement sur la ressource, qui autorise un principal d'Account B :</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAccountBRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::222222222222:role/analytics-role"
      },
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::reports-bucket/*"
    }
  ]
}
</code></pre>
<p>Rappel de l'épisode précédent : c'est le <strong>double opt-in</strong>. Cette resource policy côté A ne suffit pas — il faut aussi que le principal côté B porte une identity policy qui autorise <code>s3:GetObject</code> sur ce bucket. Les deux comptes doivent dire oui.</p>
<p>Le point clé de la resource policy : <strong>le principal d'Account B garde sa propre identité</strong>. Il appelle l'API en son nom, avec ses propres credentials.</p>
<h3>Option B — Cross-account role</h3>
<p>Account A crée un rôle avec une trust policy qui autorise Account B à l'assumer :</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::222222222222:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "alice-reports-2026" }
      }
    }
  ]
}
</code></pre>
<p>Le principal d'Account B appelle <code>sts:AssumeRole</code>, récupère des credentials temporaires, et agit désormais <strong>en tant que rôle d'Account A</strong>. Il change de casquette.</p>
<h3>Comment choisir entre les deux</h3>
<p>La question déterminante n'est pas technique, elle est d'<strong>audit</strong> : <em>quelle identité voulez-vous voir dans CloudTrail ?</em></p>
<p>Avec une resource policy, les événements CloudTrail montrent le principal d'Account B, avec son identité d'origine. Traçabilité directe : on sait exactement <em>qui</em> dans l'autre compte a touché la ressource.</p>
<p>Avec un cross-account role, les événements montrent le rôle d'Account A. L'identité d'origine existe dans la session (le champ <code>sts:AssumeRole</code> est logué côté B), mais reconstituer "qui a fait quoi" demande de croiser les trails des deux comptes. Sur un audit de sécurité, cette différence n'est pas un détail.</p>
<p>Règle pratique :</p>
<ul>
<li><p><strong>Accès ciblé à une ressource précise, traçabilité maximale</strong> → resource policy.</p>
</li>
<li><p><strong>Accès large à plusieurs services, ou le service ne supporte pas les resource policies</strong> (EC2, la plupart des appels de management) → cross-account role.</p>
</li>
</ul>
<h2>Le tableau récapitulatif</h2>
<table>
<thead>
<tr>
<th></th>
<th><strong>VPC Peering / TGW / PrivateLink</strong></th>
<th><strong>AWS RAM</strong></th>
<th><strong>Resource policy</strong></th>
<th><strong>Cross-account role</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Ce qui est partagé</strong></td>
<td>Connectivité réseau</td>
<td>La ressource elle-même</td>
<td>L'accès API à une ressource</td>
<td>Une identité complète</td>
</tr>
<tr>
<td><strong>Ressources concernées</strong></td>
<td>Trafic IP entre VPC</td>
<td>Liste limitée (subnets, TGW, Resolver rules, prefix lists...)</td>
<td>Services avec resource policies (S3, KMS, SQS, SNS, Lambda...)</td>
<td>Tout service</td>
</tr>
<tr>
<td><strong>Prérequis</strong></td>
<td>Config réseau des deux côtés</td>
<td>Sharing Org activé, ou invitation</td>
<td>Double opt-in (policy des deux côtés)</td>
<td>Trust policy + identity policy</td>
</tr>
<tr>
<td><strong>Identité dans CloudTrail</strong></td>
<td>N/A</td>
<td>Le compte qui agit sur la ressource</td>
<td>Le principal d'Account B (identité préservée)</td>
<td>Le rôle d'Account A</td>
</tr>
<tr>
<td><strong>Qui paie</strong></td>
<td>Data transfer partagé</td>
<td>Le propriétaire paie l'infra, chacun paie ce qu'il déploie</td>
<td>Le propriétaire (sauf Requester Pays sur S3)</td>
<td>Le compte où l'action s'exécute</td>
</tr>
<tr>
<td><strong>Piège classique</strong></td>
<td>Peering non transitif</td>
<td>Types non supportés ; sharing Org non activé</td>
<td>Oublier l'identity policy côté B</td>
<td>Perdre la traçabilité de l'identité d'origine</td>
</tr>
</tbody></table>
<h2>Le takeaway</h2>
<blockquote>
<p><strong>RAM partage l'infrastructure, les policies partagent l'accès, les rôles partagent l'identité.</strong></p>
</blockquote>
<p>Si vous ne retenez qu'une ligne de cet article, c'est celle-là. La prochaine fois qu'un collègue demande "comment on donne accès au compte B ?", commencez par demander <em>à quoi</em> — la réponse tombe toute seule.</p>
<hr />
<p><em>Vous structurez une organisation multi-comptes et vous voulez vérifier que vos accès cross-account ne sont pas une passoire ? Les prochains articles de la série creuseront les patterns de gouvernance — abonnez-vous pour ne pas les rater.</em></p>
]]></content:encoded></item><item><title><![CDATA[3 Ways to Share an S3 Bucket Across Accounts (And When to Use Each)
]]></title><description><![CDATA[So you need a user in Account A to reach an S3 bucket in Account B. AWS gives you three ways to wire this up — but they're not interchangeable. Pick the…]]></description><link>https://brahimserrhini.hashnode.dev/3-ways-to-share-an-s3-bucket-across-accounts-and-when-to-use-each</link><guid isPermaLink="true">https://brahimserrhini.hashnode.dev/3-ways-to-share-an-s3-bucket-across-accounts-and-when-to-use-each</guid><dc:creator><![CDATA[Brahim Serrhini]]></dc:creator><pubDate>Fri, 26 Jun 2026 14:32:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a15cfe4da253d50d4cdfe58/3bc456e5-7c7f-4825-9bbb-a930617720c8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So you need a user in <strong>Account A</strong> to reach an S3 bucket in <strong>Account B</strong>. AWS gives you three ways to wire this up — but they're not interchangeable. Pick the wrong one and you'll either fight legacy quirks or over-engineer a one-line job.</p>
<p>Here's each method, its trade-offs, and a simple rule for choosing.</p>
<hr />
<h2>The big split first</h2>
<p>Before the details, one distinction explains everything:</p>
<ul>
<li><p><strong>ACLs and bucket policies</strong> let the outsider <em>reach in</em> while keeping their own identity. Access still requires both sides to agree (the caller's identity policy <strong>and</strong> the bucket's grant).</p>
</li>
<li><p><strong>IAM roles</strong> let the outsider <em>become an insider</em>. They assume a role that lives in Account B and act as a local B identity from then on.</p>
</li>
</ul>
<p>Keep that in mind and the rest falls into place.</p>
<hr />
<h2>1. ACL — the legacy option</h2>
<p>ACLs grant access to another account using its canonical ID, directly on the bucket or object.</p>
<p><strong>Pros</strong></p>
<ul>
<li>Simple for one narrow case: granting another account blunt access to specific objects.</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li><p>Coarse-grained — only crude permissions (READ, WRITE, FULL_CONTROL). No conditions, no fine-grained actions.</p>
</li>
<li><p>AWS now recommends turning ACLs <em>off</em> (Object Ownership → "Bucket owner enforced," the default since April 2023).</p>
</li>
</ul>
<p><strong>Verdict:</strong> Avoid for new work. You'll meet ACLs in old tutorials and inherited buckets, not in anything you'd build today.</p>
<hr />
<h2>2. Bucket policy — the everyday choice</h2>
<p>A single resource policy attached to the bucket, naming the other account's principal.</p>
<pre><code class="language-json">{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::111111111111:user/Alice" },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::accountB-bucket/*"
}
</code></pre>
<p><strong>Pros</strong></p>
<ul>
<li><p>Clean and centralized — one document on the bucket controls who gets in.</p>
</li>
<li><p>Supports conditions (IP, encryption, MFA) and fine-grained actions.</p>
</li>
<li><p>Easy to read and audit.</p>
</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li><p>S3-only. It doesn't help if the other account also needs DynamoDB, SQS, etc.</p>
</li>
<li><p>Still requires the caller's identity policy to allow the action on their side.</p>
</li>
</ul>
<p><strong>Verdict:</strong> The default for straightforward S3 sharing. If the job is "let Account A read/write this bucket," reach for this first.</p>
<hr />
<h2>3. IAM role — the heavyweight</h2>
<p>Account B creates a role with a trust policy naming Account A. Alice calls <code>sts:AssumeRole</code>, gets temporary credentials, and operates <em>as</em> that role inside B.</p>
<p><strong>Pros</strong></p>
<ul>
<li><p>The most powerful and flexible option — works for <strong>any</strong> service, not just S3.</p>
</li>
<li><p>Temporary credentials (auto-expiring), clean CloudTrail trail of who assumed what.</p>
</li>
<li><p>Great when the external party needs broad or ongoing access.</p>
</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li><p>More moving parts: a role, a trust policy, and <code>sts:AssumeRole</code> permission on the caller's side.</p>
</li>
<li><p>Overkill if all you need is access to a single bucket.</p>
</li>
</ul>
<p><strong>Verdict:</strong> Use when access spans multiple services, needs to be temporary, or when you want the external account's users to behave like local ones.</p>
<hr />
<h2>Quick decision guide</h2>
<table>
<thead>
<tr>
<th>Your situation</th>
<th>Use this</th>
</tr>
</thead>
<tbody><tr>
<td>New project, just sharing one bucket</td>
<td><strong>Bucket policy</strong></td>
</tr>
<tr>
<td>External account needs S3 + other services</td>
<td><strong>IAM role</strong></td>
</tr>
<tr>
<td>Access should be temporary / auditable</td>
<td><strong>IAM role</strong></td>
</tr>
<tr>
<td>You're reading an old tutorial that uses it</td>
<td><strong>ACL</strong> (but migrate off it)</td>
</tr>
</tbody></table>
<hr />
<h2>The one-line takeaway</h2>
<p><strong>Bucket policy</strong> for everyday S3 sharing, <strong>IAM role</strong> when the need is bigger than one bucket, and <strong>ACL</strong> only when history forces your hand. Reach for the simplest tool that fits — and that's almost never the ACL.</p>
<hr />
<p><em>Part of a series where I break down AWS concepts as I learn them. Follow along if clear, no-fluff explanations are your thing.</em></p>
]]></content:encoded></item><item><title><![CDATA[The Two Doors: How AWS Cross-Account Access Actually Works]]></title><description><![CDATA[Picture two office buildings. Building A is where Alice works. Building B is across the street, owned by a different company, full of vaults she'd love to…]]></description><link>https://brahimserrhini.hashnode.dev/the-two-doors-how-aws-cross-account-access-actually-works</link><guid isPermaLink="true">https://brahimserrhini.hashnode.dev/the-two-doors-how-aws-cross-account-access-actually-works</guid><dc:creator><![CDATA[Brahim Serrhini]]></dc:creator><pubDate>Fri, 26 Jun 2026 14:06:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a15cfe4da253d50d4cdfe58/81d1a54c-5f77-49b1-b380-5f9d0a4ebf0d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Picture two office buildings.</p>
<p><strong>Building A</strong> is where Alice works. <strong>Building B</strong> is across the street, owned by a different company, full of vaults she'd love to peek inside. Alice wants to walk over and read a file from one of B's vaults.</p>
<p>Common sense says she needs two things to pull this off:</p>
<ol>
<li><p>Her own company has to let her <strong>leave</strong> the building on this errand.</p>
</li>
<li><p>The other company has to let her <strong>in</strong>.</p>
</li>
</ol>
<p>That's it. That's the entire AWS cross-account model. Hold onto the two-buildings picture and the rest is just paperwork.</p>
<hr />
<h2>The rule, in one line</h2>
<blockquote>
<p>For a principal in Account A to touch a resource in Account B, <strong>A must allow the request out, and B must allow it in.</strong> Both. Always.</p>
</blockquote>
<p>AWS people say it as <em>"allowed from A, and allowed into B."</em> Same thing. Two doors, both must open.</p>
<hr />
<h2>Door 1: "Allowed from A"</h2>
<p>This is Alice's <strong>identity policy</strong> — the badge clipped to her, issued by her own company. It says she's permitted to make this kind of trip.</p>
<pre><code class="language-json">{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::buildingB-vault/*"
}
</code></pre>
<p>No badge, no errand. Even if Building B leaves its front door wide open, Alice never makes it out of her own lobby.</p>
<h2>Door 2: "Allowed into B"</h2>
<p>This is <strong>Building B's policy</strong> — set by B's owners, deciding who they'll buzz in. In AWS this is the <strong>resource policy</strong> on the thing being accessed (an S3 bucket, a KMS key, a queue), and the giveaway is the <code>Principal</code> field naming exactly who's welcome.</p>
<pre><code class="language-json">{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::111111111111:user/Alice" },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::buildingB-vault/*"
}
</code></pre>
<p>B is saying, by name: <em>"Alice from across the street? Yeah, let her in."</em></p>
<p>Open both doors and Alice walks straight through. Close either one and she gets the AWS equivalent of a polite security guard's hand on her chest: <code>AccessDenied</code><strong>.</strong></p>
<hr />
<h2>The twist that trips everyone up</h2>
<p>Here's where it gets interesting — and where most people's mental model quietly breaks.</p>
<p><strong>Inside one building</strong>, the rule is an <strong>OR</strong>. If Alice wants a file from a vault in <em>her own</em> Building A, she only needs <em>one</em> yes — either her badge allows it, <strong>or</strong> the vault's own policy welcomes her. Either door is enough, because it's all one company.</p>
<p><strong>Between buildings</strong>, the rule flips to an <strong>AND</strong>. Now she needs <em>both</em> yeses — her badge to leave, <strong>and</strong> B's policy to enter. One missing door is a hard stop.</p>
<p>So crossing the street raises the bar from <em>"somebody said yes"</em> to <em>"everybody said yes."</em> That single shift from OR to AND is the whole concept. If you remember nothing else, remember that.</p>
<hr />
<h2>The "wait, he only did one door!" moment</h2>
<p>You'll hit a tutorial that attaches <strong>only</strong> a bucket policy — just Door 2 — and access works anyway. For a second it looks like the two-door rule is a lie.</p>
<p>It isn't. Look at <em>who</em> the policy lets in. It's almost always a principal like <code>iamadmin</code> — an <strong>administrator</strong> who already carries <code>AdministratorAccess</code>. That admin badge already grants <code>s3:*</code> on everything, which means <strong>Door 1 was open before the tutorial even started.</strong> The instructor only had to add the second door because the first was already wide open.</p>
<p>Swap that admin for a fresh user with an empty badge, attach the same bucket policy, and watch it fail instantly. The rule was never broken — one door was just already unlocked off-screen.</p>
<hr />
<h2>The takeaway</h2>
<p>Cross-account access is a handshake, not a handout. Two independent parties, two separate policies, both saying yes:</p>
<ul>
<li><p><strong>Door 1 (from A)</strong> — the caller's identity policy lets them <em>out</em>.</p>
</li>
<li><p><strong>Door 2 (into B)</strong> — the resource policy lets them <em>in</em>.</p>
</li>
</ul>
<p>Same building? One yes will do. Across the street? It takes two.</p>
<p>Now go open both doors.</p>
<hr />
<p><em>Enjoyed this? I'm writing up AWS concepts as I work through them — clear explanations, no hand-waving. Follow along if that's your thing.</em></p>
]]></content:encoded></item><item><title><![CDATA[ The 6 AWS Policy Types Explained (With Examples You'll Actually Remember)
]]></title><description><![CDATA[If you've spent any time with AWS IAM, you've probably hit a moment of quiet confusion: "Wait, is this an identity policy or a resource policy? And how is…]]></description><link>https://brahimserrhini.hashnode.dev/the-6-aws-policy-types-explained-with-examples-you-ll-actually-remember</link><guid isPermaLink="true">https://brahimserrhini.hashnode.dev/the-6-aws-policy-types-explained-with-examples-you-ll-actually-remember</guid><dc:creator><![CDATA[Brahim Serrhini]]></dc:creator><pubDate>Fri, 26 Jun 2026 10:46:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a15cfe4da253d50d4cdfe58/b7abc0bc-d85e-4c8d-bc20-536b03fd8170.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've spent any time with AWS IAM, you've probably hit a moment of quiet confusion: <em>"Wait, is this an identity policy or a resource policy? And how is an SCP different from a permission boundary if they both seem to restrict things?"</em></p>
<p>You're not alone. AWS has <strong>six</strong> distinct policy types, and they all look like nearly identical JSON. The difference isn't in how they're written — it's in <strong>where they attach</strong> and <strong>what job they do</strong> in the evaluation pipeline.</p>
<p>In this article I'll break down all six, give you a concrete JSON example of each, and leave you with a mental model that actually sticks. To keep things tangible, every example revolves around the same character: a developer named <strong>Alice</strong>, in account <code>111122223333</code>, who wants to read from an S3 bucket called <code>reports-bucket</code>.</p>
<p>Let's go.</p>
<hr />
<h2>First, the one rule that governs everything</h2>
<p>Before the policy types, you need the evaluation logic, because it explains <em>why</em> each type behaves the way it does. For a request inside a single account, AWS asks three questions in order:</p>
<ol>
<li><p><strong>Is there an explicit</strong> <code>Deny</code><strong>?</strong> → If yes, denied. Full stop. No appeals.</p>
</li>
<li><p><strong>Is there an</strong> <code>Allow</code><strong>?</strong> → If yes (and no deny), allowed.</p>
</li>
<li><p><strong>Is there nothing at all?</strong> → Denied by default. This is the <em>implicit deny</em>.</p>
</li>
</ol>
<p>Summarized: <strong>explicit Deny → Allow → implicit Deny.</strong></p>
<p>Keep this in your head. Every policy type below is just a different place where an <code>Allow</code> or a <code>Deny</code> can live.</p>
<hr />
<h2>A quick map of the six types</h2>
<p>Here's the whole landscape in one table. If you remember nothing else, screenshot this.</p>
<table>
<thead>
<tr>
<th>Policy type</th>
<th>Attached to</th>
<th>Can it <em>grant</em>?</th>
<th>Has a <code>Principal</code>?</th>
<th>Its job</th>
</tr>
</thead>
<tbody><tr>
<td>Identity policy</td>
<td>User / group / role</td>
<td>✅ Yes</td>
<td>❌ No</td>
<td>Grant permissions to an identity</td>
</tr>
<tr>
<td>Resource policy</td>
<td>A resource (bucket, key…)</td>
<td>✅ Yes</td>
<td>✅ Yes</td>
<td>Grant access to a resource (works cross-account)</td>
</tr>
<tr>
<td>SCP</td>
<td>An Org / OU / account</td>
<td>❌ No</td>
<td>❌ No</td>
<td>Cap what a whole account can do</td>
</tr>
<tr>
<td>Permission boundary</td>
<td>A user / role</td>
<td>❌ No</td>
<td>❌ No</td>
<td>Cap what one identity can do</td>
</tr>
<tr>
<td>Session policy</td>
<td>A temporary session</td>
<td>❌ No</td>
<td>❌ No</td>
<td>Cap one <code>AssumeRole</code> session</td>
</tr>
<tr>
<td>Explicit Deny</td>
<td><em>(lives inside any of the above)</em></td>
<td>—</td>
<td>—</td>
<td>Override everything with a hard no</td>
</tr>
</tbody></table>
<p>The single most important insight in this whole table: <strong>only two types actually grant permissions</strong> — identity policies and resource policies. Everything else can only <em>restrict</em>. They are ceilings, not grants. A permission boundary that "allows" <code>s3:*</code> doesn't give you anything — it just refuses to take <code>s3:*</code> away.</p>
<p>Now let's look at each one.</p>
<hr />
<h2>1. Identity policy — "What can <em>this identity</em> do?"</h2>
<p>This is the one you'll write most often. It attaches directly to an IAM user, group, or role, and it lists what that identity is allowed to do.</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadReports",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::reports-bucket",
        "arn:aws:s3:::reports-bucket/*"
      ]
    }
  ]
}
</code></pre>
<p>Attach this to Alice and she can now list and read the bucket. Notice what's <strong>missing</strong>: there's no <code>Principal</code> field. Why? Because the principal is implicit — it's whoever this policy is attached to. The policy travels <em>with</em> the identity.</p>
<p>By default, a freshly created IAM identity can do <strong>nothing</strong>. Identity policies are what bring it to life.</p>
<blockquote>
<p>💡 Identity policies come in three flavors: <strong>AWS managed</strong> (prebuilt, like <code>ReadOnlyAccess</code>), <strong>customer managed</strong> (you write them, reuse them across identities — best practice), and <strong>inline</strong> (embedded in one identity, die with it). Prefer customer managed for anything non-trivial.</p>
</blockquote>
<hr />
<h2>2. Resource policy — "Who can access <em>this resource</em>?"</h2>
<p>A resource policy attaches to the resource itself — an S3 bucket, a KMS key, an SQS queue, a Lambda function. It looks at access from the opposite direction: instead of "what can Alice do," it says "who is allowed to touch me."</p>
<p>Here's the bucket policy on <code>reports-bucket</code>:</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAliceRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:user/Alice"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::reports-bucket/*"
    }
  ]
}
</code></pre>
<p>The dead giveaway that you're looking at a resource policy is the <code>Principal</code> <strong>field</strong>. Identity policies don't have one; resource policies require one — because the resource has to say <em>who</em> it's granting access to.</p>
<p>The superpower of resource policies is <strong>cross-account access</strong>. A bucket in account B can directly grant a user in account A, with no policy needed in B for that user. This is the foundation of most cross-account patterns in AWS.</p>
<hr />
<h2>3. SCP (Service Control Policy) — the account-wide ceiling</h2>
<p>SCPs live at the <strong>AWS Organizations</strong> level. You attach them to an Organizational Unit or an account, and they set the <em>maximum</em> set of permissions anyone in that account can ever have.</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RegionLock",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": "eu-west-1"
        }
      }
    }
  ]
}
</code></pre>
<p>This says: <em>"Anyone in this account — including admins — can only operate in</em> <code>eu-west-1</code><em>."</em> Alice could be carrying <code>AdministratorAccess</code> and she still couldn't launch a thing in <code>us-east-1</code>.</p>
<p>The critical thing to internalize: <strong>SCPs never grant permissions.</strong> If an SCP "allows" S3, that doesn't give anyone S3 access — they still need an identity policy for that. The SCP just defines the outer boundary of what's <em>possible</em> in the account. It's a filter, not a faucet.</p>
<hr />
<h2>4. Permission boundary — the per-identity ceiling</h2>
<p>A permission boundary does for a single identity what an SCP does for a whole account: it caps the maximum. You attach it to a specific user or role.</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "MaxAllowed",
      "Effect": "Allow",
      "Action": ["s3:*", "ec2:Describe*"],
      "Resource": "*"
    }
  ]
}
</code></pre>
<p>Attach this to Alice and her effective permissions become the <strong>intersection</strong> of her identity policy and this boundary. If her identity policy grants <code>iam:CreateUser</code>, the boundary silently blocks it — <code>iam</code> isn't in the ceiling. She can only ever act within <code>s3:*</code> and <code>ec2:Describe*</code>, no matter what her identity policy claims.</p>
<p>The killer use case is <strong>safe delegation</strong>. You can let a junior dev create their own IAM roles, while a permission boundary (enforced with a condition) guarantees that every role they create inherits the same cap. You hand out the <em>power to create</em> without handing out the <em>power to escalate</em>. They can build roles all day; none of them can ever be more powerful than the boundary.</p>
<hr />
<h2>5. Session policy — the one-session ceiling</h2>
<p>A session policy is the most ephemeral of the bunch. It's passed <em>inline</em> at the moment you call <code>sts:AssumeRole</code>, and it only applies to that single temporary session. It's stored nowhere permanently.</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ThisSessionReadOnly",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::reports-bucket/*"
    }
  ]
}
</code></pre>
<p>Imagine Alice assumes a role that normally allows <code>s3:*</code>. If she's running a script and wants a safety net, she can pass this session policy to <em>downscope</em> that session to read-only on one bucket. Same role, deliberately narrowed for one task.</p>
<p>Like SCPs and boundaries, a session policy <strong>can only restrict, never expand</strong>. You can't pass a session policy that grants more than the role already has.</p>
<hr />
<h2>6. Explicit Deny — the override that lives everywhere</h2>
<p>Here's the twist: <strong>"explicit Deny" isn't really its own policy type.</strong> It's a <code>"Effect": "Deny"</code> statement that can live inside <em>any</em> of the five types above. I'm listing it separately because it behaves so differently that it's worth treating as its own concept.</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BlockDeletion",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::reports-bucket/*"
    }
  ]
}
</code></pre>
<p>If this statement exists <em>anywhere</em> in the chain, Alice cannot delete objects from <code>reports-bucket</code> — even if ten other policies grant her <code>s3:*</code>. An explicit Deny is absolute. It's the emergency brake of the entire IAM system, and it's evaluated first for exactly that reason.</p>
<hr />
<h2>Putting it all together</h2>
<p>So how do these six interact when a real request comes in? AWS walks them in a fixed order, and the request must survive the whole gauntlet:</p>
<ol>
<li><p><strong>Explicit Deny</strong> anywhere → denied instantly.</p>
</li>
<li><p><strong>SCPs</strong> → if they exist and don't allow it → denied.</p>
</li>
<li><p><strong>Resource policies</strong> → if one allows it → ✅ done, allowed (the cross-account shortcut).</p>
</li>
<li><p><strong>Permission boundaries</strong> → if one exists and doesn't allow it → denied.</p>
</li>
<li><p><strong>Session policies</strong> → if one exists and doesn't allow it → denied.</p>
</li>
<li><p><strong>Identity policies</strong> → if one allows it → ✅ allowed. Otherwise → implicit deny.</p>
</li>
</ol>
<p>Notice the asymmetry, because it's the whole point:</p>
<ul>
<li><p><strong>Identity and resource policies</strong> are the only two doors marked ✅ <em>allow</em>.</p>
</li>
<li><p><strong>SCPs, boundaries, and session policies</strong> are walls — they can only say <em>no</em> or step aside.</p>
</li>
<li><p><strong>Explicit Deny</strong> trumps all of them.</p>
</li>
</ul>
<hr />
<h2>The one-sentence takeaway</h2>
<p>If you walk away with a single sentence, make it this:</p>
<blockquote>
<p><strong>Two policy types grant (identity + resource); three policy types cap (SCP, boundary, session); and an explicit Deny beats them all.</strong></p>
</blockquote>
<p>Once that clicks, the rest of IAM stops feeling like a wall of identical JSON and starts feeling like a system you can actually reason about.</p>
<hr />
<p><em>Thanks for reading. If this helped, I'm publishing more AWS deep-dives as I work through my Solutions Architect Professional prep — feel free to follow along.</em></p>
]]></content:encoded></item><item><title><![CDATA[Permission Boundaries Don't Grant Permissions — And Why That Matters]]></title><description><![CDATA[Permission Boundaries don't grant permissions. If you think they do, you're going to design IAM wrong. It's one of the most misunderstood concepts in AWS…]]></description><link>https://brahimserrhini.hashnode.dev/permission-boundaries-don-t-grant-permissions-and-why-that-matters</link><guid isPermaLink="true">https://brahimserrhini.hashnode.dev/permission-boundaries-don-t-grant-permissions-and-why-that-matters</guid><dc:creator><![CDATA[Brahim Serrhini]]></dc:creator><pubDate>Wed, 27 May 2026 16:51:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a15cfe4da253d50d4cdfe58/9b4ceb96-1ce8-4eba-ba1b-39bc2cd38716.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Permission Boundaries don't grant permissions. If you think they do, you're going to design IAM wrong.</strong></p>
<p>It's one of the most misunderstood concepts in AWS IAM — and the misunderstanding has real consequences when you start delegating role creation across a team. Here's the correct mental model, and the use case that makes it click.</p>
<h2>The mental model most engineers get wrong</h2>
<p>When developers first encounter Permission Boundaries, the intuition is to read them as an additional allow layer: <em>"this defines what the identity can also do."</em></p>
<p>That intuition is wrong, and it's the root of most broken IAM designs I've seen.</p>
<p>The correct model is the opposite:</p>
<blockquote>
<p><strong>A Permission Boundary doesn't grant any permissions. It defines the maximum permissions that an identity-based policy can ever grant.</strong></p>
</blockquote>
<p>It's a ceiling. Not a floor. Not an additive layer. A hard cap.</p>
<p>If an IAM user has an identity policy that says <code>Allow s3:*</code>, but a Permission Boundary that says <code>max = s3:GetObject</code>, the effective permissions are just <code>s3:GetObject</code>. The boundary didn't grant <code>GetObject</code> — the identity policy did. The boundary simply refused to let the rest of <code>s3:*</code> through.</p>
<p>The formula:</p>
<pre><code class="language-plaintext">Effective permissions = Identity Policy ∩ Permission Boundary
</code></pre>
<p>Both must allow the action. The boundary alone allows nothing.</p>
<h2>The use case that makes it click: delegating role creation</h2>
<p>The textbook definition doesn't stick until you see the real problem Permission Boundaries solve.</p>
<p>Picture a growing AWS team. Developers constantly need IAM roles for their Lambda functions, EC2 instances, and CI/CD pipelines. The admin becomes the bottleneck. The obvious move: give developers <code>iam:CreateRole</code> and <code>iam:AttachRolePolicy</code> so they can self-serve.</p>
<p>The problem is immediate. A developer with <code>iam:CreateRole</code> + <code>iam:AttachRolePolicy</code> can create a role with <code>AdministratorAccess</code> attached. Privilege escalation in one API call. The entire account is exposed.</p>
<p>This is exactly what Permission Boundaries were designed to solve.</p>
<p>The pattern:</p>
<ol>
<li><p>Define a Permission Boundary policy — call it <code>DeveloperBoundary</code> — listing the maximum permissions any role created by a developer is allowed to have. Read/write on specific S3 buckets, invoke specific Lambdas, write to specific DynamoDB tables. No IAM admin, no billing, no organization access.</p>
</li>
<li><p>Grant developers permission to create and manage roles — <strong>with a condition</strong>: any role they create <em>must</em> have <code>DeveloperBoundary</code> attached as its Permission Boundary.</p>
</li>
</ol>
<p>The condition in the delegated policy:</p>
<pre><code class="language-json">{
  "Effect": "Allow",
  "Action": [
    "iam:CreateRole",
    "iam:AttachRolePolicy",
    "iam:PutRolePolicy"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
    }
  }
}
</code></pre>
<p>Now the developer can create as many roles as they need. They can attach <code>AdministratorAccess</code> to those roles if they want — it doesn't matter. The boundary caps the effective permissions at whatever <code>DeveloperBoundary</code> allows. The escalation path is closed.</p>
<p>This is where Permission Boundaries stop being abstract and become the only clean answer.</p>
<h2>Three things that look alike but do different jobs</h2>
<p>Most IAM confusion comes from mixing up three mechanisms that share JSON syntax but serve different purposes:</p>
<ul>
<li><p><strong>IAM policies</strong> — grant permissions to an identity.</p>
</li>
<li><p><strong>Permission Boundaries</strong> — cap the maximum permissions an identity-based policy can grant. Grant nothing on their own.</p>
</li>
<li><p><strong>SCPs (Service Control Policies)</strong> — also a ceiling, but applied at the AWS Organization level, across entire accounts. Cap what <em>anyone</em> in the account can do, including the root user.</p>
</li>
</ul>
<p>All three can deny. Only IAM policies grant. Mixing these up is how teams end up with mysterious <code>AccessDenied</code> errors that take half a day to debug.</p>
<h2>Two takeaways</h2>
<p><strong>1. Permission Boundaries are a delegation tool, not a personal restriction.</strong> You rarely put a boundary on your own admin user. You put boundaries on identities you want to give <em>bounded freedom</em> to.</p>
<p><strong>2. If you can't articulate what a feature <em>doesn't</em> do, you don't fully understand it.</strong> "Permission Boundaries set max permissions" is the textbook answer. Designing the delegation pattern above requires the deeper distinction — that the boundary grants nothing, ever.</p>
]]></content:encoded></item></channel></rss>