<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kt</title>
    <description>The latest articles on DEV Community by kt (@kanywst).</description>
    <link>https://dev.to/kanywst</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3700180%2F04651b63-c6a1-4069-b356-a0f85c17e0bb.png</url>
      <title>DEV Community: kt</title>
      <link>https://dev.to/kanywst</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kanywst"/>
    <language>en</language>
    <item>
      <title>SPIFFE Compliance Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Sun, 31 May 2026 06:05:03 +0000</pubDate>
      <link>https://dev.to/kanywst/spiffe-compliance-deep-dive-5e29</link>
      <guid>https://dev.to/kanywst/spiffe-compliance-deep-dive-5e29</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;"This is SPIFFE compliant." "Our implementation is SPIFFE-compatible." Read the SPIRE, Istio, or Cilium docs and you bump into these phrases everywhere. But if you actually stop and ask what compliance means, the answer is surprisingly hard to nail down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If I run SPIRE, am I SPIFFE compliant?&lt;/li&gt;
&lt;li&gt;If I roll my own, what do I have to ship to call it compliant?&lt;/li&gt;
&lt;li&gt;Does an SVID have to support both X.509 and JWT? Or is one enough?&lt;/li&gt;
&lt;li&gt;Is there an official conformance test you can point at?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of people use the word without checking. I was one of them.&lt;/p&gt;

&lt;p&gt;So I cloned the upstream spec repo (&lt;code&gt;github.com/spiffe/spiffe&lt;/code&gt;) and read every document from top to bottom. What follows are my notes, organized around the question "what does the spec actually require?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/spiffe/spiffe.git ~/spiffe
&lt;span class="nb"&gt;ls&lt;/span&gt; ~/spiffe/standards/
&lt;span class="c"&gt;# JWT-SVID.md&lt;/span&gt;
&lt;span class="c"&gt;# SPIFFE-ID.md&lt;/span&gt;
&lt;span class="c"&gt;# SPIFFE.md&lt;/span&gt;
&lt;span class="c"&gt;# SPIFFE_Federation.md&lt;/span&gt;
&lt;span class="c"&gt;# SPIFFE_Trust_Domain_and_Bundle.md&lt;/span&gt;
&lt;span class="c"&gt;# SPIFFE_Workload_API.md&lt;/span&gt;
&lt;span class="c"&gt;# SPIFFE_Workload_Endpoint.md&lt;/span&gt;
&lt;span class="c"&gt;# X509-SVID.md&lt;/span&gt;
&lt;span class="c"&gt;# workloadapi.proto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight specs total. Read them in order and each one spells out where the hard requirements end and the soft suggestions begin. I'll pull out the load-bearing parts.&lt;/p&gt;




&lt;h2&gt;
  
  
  0. Prerequisites
&lt;/h2&gt;

&lt;p&gt;A few terms before we go any further. Skip this section if you've touched SPIFFE/SPIRE before.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "workload identity" means
&lt;/h3&gt;

&lt;p&gt;A way to cryptographically prove "who you are" to a workload (a process or container). IP addresses and hostnames can be spoofed. Kubernetes labels can be rewritten. Instead, a trusted &lt;strong&gt;Certificate Authority (CA)&lt;/strong&gt; signs a certificate or token that the workload presents, and verifiers check the signature.&lt;/p&gt;

&lt;p&gt;To distinguish this from human identity (the account you log into via OAuth), it's increasingly called &lt;strong&gt;Workload Identity&lt;/strong&gt; or &lt;strong&gt;Non-Human Identity (NHI)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  URI basics (RFC 3986)
&lt;/h3&gt;

&lt;p&gt;We'll be dealing with strings like &lt;code&gt;spiffe://example.com/payments/web-fe&lt;/code&gt;, so a quick refresher on URI structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spiffe://example.com/payments/web-fe
  |        |              |
scheme  authority        path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In SPIFFE the scheme is fixed at &lt;code&gt;spiffe&lt;/code&gt;, the authority is the &lt;strong&gt;Trust Domain&lt;/strong&gt;, and everything after that is the &lt;strong&gt;Workload Path&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  X.509, mTLS, JWT, JWS, JWK
&lt;/h3&gt;

&lt;p&gt;Signed data structures, or representations of keys.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;X.509&lt;/strong&gt;: the certificate format TLS uses. Binary (DER) or PEM encoded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mTLS (mutual TLS)&lt;/strong&gt;: both client and server present X.509 certificates and verify each other. Plain TLS only verifies the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT&lt;/strong&gt; (RFC 7519): a token made of JSON pieces joined with Base64URL. Common on the web.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWS&lt;/strong&gt; (RFC 7515): the structure that signs the JWT payload. &lt;strong&gt;A JWT requires a JWS to exist.&lt;/strong&gt; When the SPIFFE spec says "JWS Compact Serialization," it means the normal JWT layout: &lt;code&gt;header.payload.signature&lt;/code&gt; joined with dots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWK&lt;/strong&gt; (RFC 7517): a JSON representation of a public (or private) key. Looks like &lt;code&gt;{"kty":"RSA", "n":"...", "e":"AQAB"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWKS (JWK Set)&lt;/strong&gt;: a JSON array bundling multiple JWKs. If you've ever fetched &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; in OAuth/OIDC, you've seen one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bearer Token&lt;/strong&gt;: a token where possession alone grants access. Steal it, use it. JWTs are typically bearer tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SPIFFE spec stacks a SPIFFE ID on top of these and calls the result an &lt;strong&gt;SVID (SPIFFE Verifiable Identity Document)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X.509 based → &lt;strong&gt;X.509-SVID&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;JWT/JWS based → &lt;strong&gt;JWT-SVID&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Distribution of trust roots (CA keys) → &lt;strong&gt;SPIFFE Bundle&lt;/strong&gt; (a JWKS underneath)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  gRPC and Unix Domain Sockets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gRPC&lt;/strong&gt;: an RPC framework that pushes Protocol Buffers over HTTP/2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unix Domain Socket (UDS)&lt;/strong&gt;: an inter-process socket on the local host. You reach it by filesystem path (for example &lt;code&gt;/run/spire/agent.sock&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Workload API ships over these two together.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Shape of SPIFFE: Three Pillars
&lt;/h2&gt;

&lt;p&gt;The SPIFFE spec set, in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Hand the workload a &lt;code&gt;spiffe://...&lt;/code&gt; ID, issue a verifiable document (SVID) that carries it, and serve it through a local API."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The three pieces (ID, document, API) each get their own spec document.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. SPIFFE ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SPIFFE-ID.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the workload namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. SVID (X.509)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;X509-SVID.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How to carry a SPIFFE ID in an X.509 certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. SVID (JWT)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JWT-SVID.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How to carry a SPIFFE ID in a JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Workload API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SPIFFE_Workload_API.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gRPC service that issues SVIDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Workload Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SPIFFE_Workload_Endpoint.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conventions for the socket that exposes the API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SPIFFE_Trust_Domain_and_Bundle.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How to represent trust roots (CA keys)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SPIFFE_Federation.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How separate Trust Domains link up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't actually have to satisfy all of these to call yourself SPIFFE compliant. The next section explains why.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. What the Spec Itself Says "Compliant" Means
&lt;/h2&gt;

&lt;p&gt;This one sentence at the end of &lt;code&gt;SPIFFE-ID.md&lt;/code&gt; Section 1 does a lot of work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Conformance with this document is sufficient for the purposes of SPIFFE compliance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Meet &lt;code&gt;SPIFFE-ID.md&lt;/code&gt; and you can claim SPIFFE compliance. That's it. The Workload API, the Trust Bundle, the Federation spec, none of them are required by the letter of the spec.&lt;/p&gt;

&lt;p&gt;That's the formal floor. In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An ID by itself is useless without an &lt;strong&gt;SVID&lt;/strong&gt; carrying it.&lt;/li&gt;
&lt;li&gt;An SVID is useless without a &lt;strong&gt;Workload API&lt;/strong&gt; to deliver it.&lt;/li&gt;
&lt;li&gt;And neither is verifiable across hosts or trust domains without a &lt;strong&gt;Bundle&lt;/strong&gt; representing the trust root.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Practically SPIFFE compliant" means the three core specs plus Bundle, four documents total. If you cross trust domains, add &lt;strong&gt;Federation&lt;/strong&gt; to that list.&lt;/p&gt;

&lt;p&gt;The rest of this article walks through the MUST requirements of each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. SPIFFE ID: How You Name Things
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Anatomy of the URI
&lt;/h3&gt;

&lt;p&gt;A SPIFFE ID is an RFC 3986 URI with a fixed shape.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spiffe://trust-domain-name/path/segments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull out everything the spec marks MUST or MUST NOT and you get:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheme is fixed at &lt;code&gt;spiffe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;td&gt;Case-insensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust domain name is non-empty&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;host&lt;/code&gt; component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;userinfo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;spiffe://user:pass@td/&lt;/code&gt; is out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;port&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;spiffe://td:8080/&lt;/code&gt; is out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust domain name is lowercase&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Example.com&lt;/code&gt; is out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust domain charset is &lt;code&gt;[a-z0-9.-_]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;td&gt;Percent-encoding is out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;query&lt;/code&gt; or &lt;code&gt;fragment&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;?key=val&lt;/code&gt; and &lt;code&gt;#frag&lt;/code&gt; are out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No trailing slash on the path&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;spiffe://td/foo/&lt;/code&gt; is out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;.&lt;/code&gt; or &lt;code&gt;..&lt;/code&gt; segments&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;Relative path tokens forbidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whole URI under 2048 bytes&lt;/td&gt;
&lt;td&gt;MUST (SHOULD)&lt;/td&gt;
&lt;td&gt;Full URI length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust domain name under 255 bytes&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;td&gt;RFC 3986 host limit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.2 Trust Domain Names Are Unregulated
&lt;/h3&gt;

&lt;p&gt;The spec is up front about this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trust domain operators are free to choose any trust domain name they find suitable: there is no centralized authority for regulation or registration of trust domain names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike DNS, there is no registry. Anyone can call themselves &lt;code&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So what happens when two parties pick the same name?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a collision does occur, those trust domains will continue to operate independently but will be unable to federate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While they stay independent, nothing breaks. The moment they try to federate (link up across trust domains), the two sides hold different keys, so neither can verify the other's certificates. They just fail to connect.&lt;/p&gt;

&lt;p&gt;The practical workaround is to use a DNS name you already own. If you're auto-generating, a UUID works.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Path Design
&lt;/h3&gt;

&lt;p&gt;What the path means is up to the implementer. The spec gives three example patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pattern A, identify the service directly&lt;/strong&gt;: &lt;code&gt;spiffe://staging.example.com/payments/mysql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern B, mirror the orchestrator's ID structure&lt;/strong&gt;: &lt;code&gt;spiffe://k8s-west.example.com/ns/staging/sa/default&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern C, opaque&lt;/strong&gt;: &lt;code&gt;spiffe://example.com/9eebccd2-12bf-40a6-b262-65fe0487d453&lt;/code&gt; (metadata managed elsewhere)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SPIRE's default on Kubernetes is Pattern B (&lt;code&gt;/ns/&amp;lt;namespace&amp;gt;/sa/&amp;lt;service-account&amp;gt;&lt;/code&gt;), and that's close to the de facto convention. It maps Kubernetes Pod Identity directly, so the operational view stays legible.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. SVID: X.509 Profile Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 An X.509-SVID Is Just an X.509 Certificate With a URI SAN
&lt;/h3&gt;

&lt;p&gt;Read &lt;code&gt;X509-SVID.md&lt;/code&gt; and there are no new fields. &lt;strong&gt;It's a normal X.509 certificate with rules layered on top about how to use the existing ones.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core rule is simple: &lt;strong&gt;the Subject Alternative Name extension's URI type holds exactly one SPIFFE ID&lt;/strong&gt;. Everything else (&lt;code&gt;Subject&lt;/code&gt;, &lt;code&gt;Basic Constraints&lt;/code&gt;, &lt;code&gt;Key Usage&lt;/code&gt;, &lt;code&gt;Extended Key Usage&lt;/code&gt;) carries its usual X.509 meaning. &lt;strong&gt;The spec only constrains which values are legal for SPIFFE use.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Leaf SVID vs Signing SVID
&lt;/h3&gt;

&lt;p&gt;The X.509 field values differ between Leaf and Signing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Leaf SVID&lt;/th&gt;
&lt;th&gt;Signing SVID (CA)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cA&lt;/code&gt; (Basic Constraints)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt; MUST&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; MUST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;keyCertSign&lt;/code&gt; (Key Usage)&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cRLSign&lt;/code&gt; (Key Usage)&lt;/td&gt;
&lt;td&gt;MUST NOT&lt;/td&gt;
&lt;td&gt;MAY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;digitalSignature&lt;/code&gt; (Key Usage)&lt;/td&gt;
&lt;td&gt;MUST&lt;/td&gt;
&lt;td&gt;(not specified)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPIFFE ID path&lt;/td&gt;
&lt;td&gt;Non-root (at least one segment) MUST&lt;/td&gt;
&lt;td&gt;No path (&lt;code&gt;spiffe://td&lt;/code&gt;) SHOULD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of URI SANs&lt;/td&gt;
&lt;td&gt;Exactly one MUST&lt;/td&gt;
&lt;td&gt;Exactly one MUST&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A Leaf SVID can sign (mTLS client auth, API request signing) but cannot issue certificates for other parties. Same position as an ordinary client certificate.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Extra Checks at Validation Time
&lt;/h3&gt;

&lt;p&gt;Section 5.2 of &lt;code&gt;X509-SVID.md&lt;/code&gt; makes it clear that &lt;strong&gt;standard X.509 path validation is not enough&lt;/strong&gt;. Using a certificate as an SVID requires these additional checks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fspiffe-compliance-deep-dive%2Fdiagrams%2F04-x509-svid-validation.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fspiffe-compliance-deep-dive%2Fdiagrams%2F04-x509-svid-validation.png" alt="X.509-SVID validation flow" width="800" height="1492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Forget these and you open the door to vulnerabilities like an intermediate CA being used as a leaf certificate. Libraries like &lt;code&gt;go-spiffe/v2&lt;/code&gt; handle this for you. Only worry about it when writing your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. SVID: JWT Profile Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 A JWT-SVID Is Just a JWS
&lt;/h3&gt;

&lt;p&gt;From Section 1 of &lt;code&gt;JWT-SVID.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JWT-SVIDs are standard JWT tokens with a handful of restrictions applied.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A normal JWT with a few extra constraints, nothing more. Format is JWS Compact Serialization (the &lt;code&gt;header.payload.signature&lt;/code&gt; layout). JWS JSON Serialization is &lt;strong&gt;MUST NOT&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 The Core Restriction: Reject &lt;code&gt;alg: none&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;JWT has a well-known footgun: set &lt;code&gt;alg&lt;/code&gt; to &lt;code&gt;none&lt;/code&gt; in the header and the token sails through unverified.&lt;/p&gt;

&lt;p&gt;The JWT-SVID spec pins &lt;code&gt;alg&lt;/code&gt; to one of these nine values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;alg&lt;/code&gt; value&lt;/th&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RS256 / RS384 / RS512&lt;/td&gt;
&lt;td&gt;RSASSA-PKCS1-v1_5 + SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PS256 / PS384 / PS512&lt;/td&gt;
&lt;td&gt;RSASSA-PSS + SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ES256 / ES384 / ES512&lt;/td&gt;
&lt;td&gt;ECDSA + SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything else (especially &lt;code&gt;none&lt;/code&gt; or the symmetric &lt;code&gt;HS*&lt;/code&gt; family) &lt;strong&gt;MUST be rejected&lt;/strong&gt;. That single rule closes off most of the classic JOSE vulnerability surface.&lt;/p&gt;

&lt;p&gt;Required claims:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: the SPIFFE ID of the workload.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt;: at least one audience.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt;: expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;kid&lt;/code&gt; is optional in the header but required on the Bundle JWK side so verifiers can pick the right key.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Always Check &lt;code&gt;aud&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;JWT-SVID is a bearer token. Anyone holding it can use it. To soften that blow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt; MUST be set.&lt;/li&gt;
&lt;li&gt;The receiver MUST check that its own ID is in &lt;code&gt;aud&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Single audience strongly recommended.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The example in Section 7.2 of the spec shows the failure mode:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if Alice has a token with audiences Bob and Chuck, and transmits that token to Chuck, then Chuck can impersonate Alice by sending the same token to Bob.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alice issues a token addressed to both Bob and Chuck. Chuck replays it to Bob and impersonates Alice. &lt;strong&gt;One token, one audience. That's the rule.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Workload API: How SVIDs Get Delivered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 gRPC, Local Only
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SPIFFE_Workload_Endpoint.md&lt;/code&gt;, summarized: the Workload API is a gRPC endpoint on a Unix Domain Socket (or localhost TCP) within the same host. In practice, the SPIRE Agent running on each node binds to &lt;code&gt;/run/spire/agent.sock&lt;/code&gt;, and every container on that host talks to it over gRPC. The Agent handles the conversation with the SPIRE Server (the central CA) over a separate network. The workload itself never reaches the outside.&lt;/p&gt;

&lt;p&gt;The MUSTs around transport and accessibility:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gRPC required&lt;/td&gt;
&lt;td&gt;Prefer UDS over TCP (SHOULD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No TLS&lt;/td&gt;
&lt;td&gt;In fact, MUST NOT require it. At bootstrap the workload has no trust root yet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confined to one host&lt;/td&gt;
&lt;td&gt;SHOULD (don't make it reachable from other hosts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;workload.spiffe.io: true&lt;/code&gt; metadata&lt;/td&gt;
&lt;td&gt;MUST (SSRF defense)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No authentication handshake&lt;/td&gt;
&lt;td&gt;MUST NOT require one. Caller is identified at the OS layer instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last point is unusual. A typical gRPC service authenticates clients via certificates or tokens. The Workload API flips that: the workload doesn't announce itself, the server identifies it via the OS. This identification flow is called &lt;strong&gt;Workload Attestation&lt;/strong&gt;. Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch the connecting PID from the UDS via &lt;code&gt;getpeerucred&lt;/code&gt; (or equivalent).&lt;/li&gt;
&lt;li&gt;Inspect the PID's cgroup, or query the Kubernetes API server.&lt;/li&gt;
&lt;li&gt;Conclude "you're the web-fe container in pod foo-bar-1234."&lt;/li&gt;
&lt;li&gt;Issue the SPIFFE ID that container should hold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.2 Five RPCs Across Two Profiles
&lt;/h3&gt;

&lt;p&gt;The Workload API has two profiles (X.509 and JWT) and five RPCs between them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;RPC&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X.509&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FetchX509SVID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stream&lt;/td&gt;
&lt;td&gt;SVID + Bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X.509&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FetchX509Bundles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stream&lt;/td&gt;
&lt;td&gt;Bundles only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FetchJWTSVID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unary&lt;/td&gt;
&lt;td&gt;JWT for a given audience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FetchJWTBundles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stream&lt;/td&gt;
&lt;td&gt;JWKS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValidateJWTSVID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unary&lt;/td&gt;
&lt;td&gt;Delegate JWT verification to the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From Section 1 of &lt;code&gt;SPIFFE_Workload_API.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Both profiles are mandatory and MUST be supported by SPIFFE implementations. However, operators MAY administratively disable a specific profile in their deployment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A SPIFFE-compliant Workload API has to implement both X.509 and JWT profiles (the operator is allowed to turn one off in their deployment). This bites in practice. Even popular OSS libraries often bolt JWT on later.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3 Why It Streams
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FetchX509SVID&lt;/code&gt;, &lt;code&gt;FetchJWTBundles&lt;/code&gt;, and &lt;code&gt;FetchX509Bundles&lt;/code&gt; return as gRPC server-side streams so the server can push:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New certificates during key rotation.&lt;/li&gt;
&lt;li&gt;CRL (revocation list) updates.&lt;/li&gt;
&lt;li&gt;New state without the workload paying reconnect cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client &lt;strong&gt;SHOULD keep the connection open&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fspiffe-compliance-deep-dive%2Fdiagrams%2F07-workload-api-rotation-sequence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fspiffe-compliance-deep-dive%2Fdiagrams%2F07-workload-api-rotation-sequence.png" alt="Workload API streaming during CA rotation and revocation" width="800" height="947"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every message ships the full current state, not a delta (spec sections 4.3 and 4.4). That sidesteps the whole anti-entropy headache of incremental sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Discovery: &lt;code&gt;SPIFFE_ENDPOINT_SOCKET&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;How does a client find the Workload API? From spec section 4:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Clients may be explicitly configured with the socket location, or may utilize the well-known environment variable &lt;code&gt;SPIFFE_ENDPOINT_SOCKET&lt;/code&gt;. If not explicitly configured, conforming clients MUST fall back to the environment variable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without explicit configuration, look at &lt;code&gt;SPIFFE_ENDPOINT_SOCKET&lt;/code&gt;. The value is URI-formatted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Unix Domain Socket&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SPIFFE_ENDPOINT_SOCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unix:///run/spire/agent.sock

&lt;span class="c"&gt;# TCP (only on hosts with a specific reason)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SPIFFE_ENDPOINT_SOCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tcp://127.0.0.1:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Official libraries like &lt;code&gt;go-spiffe/v2&lt;/code&gt; do this automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Trust Domain and Bundle: Building the Trust Root
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7.1 A Bundle Is a Keyring Shaped Like a JWKS
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SPIFFE_Trust_Domain_and_Bundle.md&lt;/code&gt; in one line: &lt;strong&gt;SPIFFE Bundle = RFC 7517 JWK Set + SPIFFE-specific metadata&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;JWKS is the same format you've seen as &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; on Cognito, Auth0, and Google Identity Platform. The SPIFFE Bundle extends it to hold both X.509 CA certificates and JWT signing keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spiffe_sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12035488&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spiffe_refresh_hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2419200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RSA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x509-svid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"x5c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64 DER encoding of X.509 CA cert&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64urlUint-encoded modulus&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AQAB"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RSA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;JWT key id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jwt-svid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64urlUint-encoded modulus&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AQAB"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are unique to a SPIFFE Bundle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;use&lt;/code&gt; parameter&lt;/strong&gt;: MUST be either &lt;code&gt;x509-svid&lt;/code&gt; or &lt;code&gt;jwt-svid&lt;/code&gt;. Without it the verifier can't tell which kind of SVID the key validates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;spiffe_sequence&lt;/code&gt;&lt;/strong&gt;: monotonically increasing counter. Increments every time the Bundle is updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7.2 Keep Trust Domains Isolated
&lt;/h3&gt;

&lt;p&gt;Section 6.2 of the Bundle spec:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a root key is shared across multiple trust domains, it becomes critically important that authentication and authorization implementations carefully check the trust domain name component of an identity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't share keys across trust domains. If you do, verifiers had better check the trust domain name strictly, or an SVID from the wrong trust domain becomes a forgery vector.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fspiffe-compliance-deep-dive%2Fdiagrams%2F08-trust-domain-isolation.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fspiffe-compliance-deep-dive%2Fdiagrams%2F08-trust-domain-isolation.png" alt="Trust domain isolation, shared key vs separated keys" width="800" height="845"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One trust domain, one independent set of keys. That's the iron rule of SPIFFE operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3 Key Rotation: Add First, Remove Later
&lt;/h3&gt;

&lt;p&gt;Bundle updates work as "add the new one, then remove the old." The Bundle moves through four states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Bundle contents&lt;/th&gt;
&lt;th&gt;Issuer signs new SVIDs with&lt;/th&gt;
&lt;th&gt;Verifier accepts SVIDs signed by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[K1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;K1&lt;/td&gt;
&lt;td&gt;K1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add K2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[K1, K2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;K1 (still)&lt;/td&gt;
&lt;td&gt;K1 or K2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[K1, K2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;K2&lt;/td&gt;
&lt;td&gt;K1 or K2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop K1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[K2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;K2&lt;/td&gt;
&lt;td&gt;K2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;spiffe_refresh_hint&lt;/code&gt; (seconds) controls how often the workload re-fetches the Bundle. The spec's example value is 28 days (2419200 seconds), which is far too long if revocation matters. In practice, 5 minutes to 1 hour is the realistic production range.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. What Existing Implementations Actually Cover
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8.1 SPIRE
&lt;/h3&gt;

&lt;p&gt;The reference implementation. Satisfies every item by definition. Workload API streaming, JWT-SVID ValidateJWTSVID delegation, the whole list.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.2 Istio
&lt;/h3&gt;

&lt;p&gt;Istio holds the workload's SVID inside each sidecar (Envoy). The on-the-wire interface to Envoy is &lt;strong&gt;SDS (Secret Discovery Service)&lt;/strong&gt;, which is an Envoy upstream API, not Istio-specific. From a SPIFFE-compliance standpoint, the relevant fact is that Istio's istio-agent feeds SVIDs into Envoy via SDS rather than exposing the SPIFFE Workload API to workloads. The payload format is SPIFFE-compatible, but the Workload Endpoint spec is not implemented.&lt;/p&gt;

&lt;p&gt;Istio can also be wired to use a SPIRE-backed SDS server. In that setup SPIRE is the CA, and the Workload Endpoint spec becomes implemented through SPIRE.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.3 Cilium
&lt;/h3&gt;

&lt;p&gt;Cilium has two unrelated paths that get conflated under the SPIFFE label, and they need to be kept apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 1, SPIRE-backed Mutual Authentication (Beta since v1.14, still Beta in v1.19)&lt;/strong&gt;. Cilium auto-deploys a SPIRE Agent on each node and rides on top. Cilium itself doesn't implement the SPIFFE spec directly. It wraps SPIRE. This path is opt-in (&lt;code&gt;authentication.mutual.spire.enabled=true&lt;/code&gt;) and has been since v1.14.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 2, ztunnel-based transparent mTLS (Beta, added v1.19)&lt;/strong&gt;. Cilium picked up the same ztunnel data plane Istio ambient mode uses. Per the Cilium docs, ztunnel's certificates are generated via OpenSSL and stored as Kubernetes Secrets. SPIRE is not in the loop, and the docs do not claim SPIFFE compliance for this mode. Whether the certificates carry SPIFFE IDs in their SANs is an implementation detail I haven't verified, so don't take "Cilium ztunnel is SPIFFE compliant" as a settled claim.&lt;/p&gt;

&lt;p&gt;If someone says "Cilium is SPIFFE compliant", ask which feature: the SPIRE-backed mutual auth, or ztunnel.&lt;/p&gt;

&lt;p&gt;So when a doc says "Istio is SPIFFE compliant", read it as "the SVID format is compliant, the Workload Endpoint spec is not." Same caution applies to Cilium plus the version question.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. 2026 Updates: Recent Spec Movement
&lt;/h2&gt;

&lt;p&gt;Tracking the &lt;code&gt;spiffe/spiffe&lt;/code&gt; repo, a couple of things have moved since late 2025.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;draft-wit-svid&lt;/code&gt; branch, active since late 2025&lt;/strong&gt;. A candidate successor to JWT-SVID aimed at integration with IETF's WIMSE (Workload Identity in Multi-System Environments) work. PR #361 ("Introduce WIT-SVID Token Document") landed on the branch 2026-01-21. Two changes worth noting: WIT-SVID makes Proof of Possession via the &lt;code&gt;cnf&lt;/code&gt; claim mandatory (RFC 7800 confirmation), structurally closing off the bearer-token replay risk of JWT-SVID; and PR #372 (2026-01-26) prohibits the &lt;code&gt;aud&lt;/code&gt; claim entirely (audience scoping is delegated to the PoP layer). Still an experimental branch, not on main, so I don't count it toward "compliance" yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR #381, merged 2026-03-26&lt;/strong&gt;. "Strengthen validation language, and clarify leaf SPIFFE ID requirements." Tightens the path requirements and validation language around leaf SPIFFE IDs. Low impact for most implementations, worth a re-read if you wrote your own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "what compliance means" I described here tracks the &lt;code&gt;main&lt;/code&gt; branch as of May 2026. If WIT-SVID gets merged into &lt;code&gt;main&lt;/code&gt;, sections 4 to 5 grow by one more SVID profile with stricter rules than JWT-SVID.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Running the Checklist: &lt;code&gt;scc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Walking sections 3 to 7 by hand gets old after the second time someone hands you an SVID and asks "is this compliant?". I packaged the static slice of the checklist as a single-binary CLI: &lt;a href="https://github.com/0-draft/spiffe-compliance-checker" rel="noopener noreferrer"&gt;&lt;code&gt;scc&lt;/code&gt;&lt;/a&gt; (spiffe-compliance-checker).&lt;/p&gt;

&lt;p&gt;Each subcommand reads one artifact and emits one line per MUST / SHOULD clause, with the spec file and section cited inline. Exit code is &lt;code&gt;1&lt;/code&gt; on any MUST failure, &lt;code&gt;0&lt;/code&gt; otherwise. SHOULD violations surface as &lt;code&gt;WARN&lt;/code&gt; and do not change the exit code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;kanywst/tap/spiffe-compliance-checker
&lt;span class="c"&gt;# or: go install github.com/0-draft/spiffe-compliance-checker/cmd/scc@latest&lt;/span&gt;

scc &lt;span class="nb"&gt;id&lt;/span&gt;        &lt;span class="s1"&gt;'spiffe://example.com/payments/web-fe'&lt;/span&gt;
scc x509-svid leaf.pem
scc jwt-svid  &amp;lt;token&amp;gt;
scc bundle    bundle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failing run reads as a spec walkthrough rather than an opaque error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9neuaeucg0hj4i8yg78u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9neuaeucg0hj4i8yg78u.gif" alt="scc demo" width="799" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What it covers right now: SPIFFE ID syntax, X.509-SVID structural rules (URI SAN, Basic Constraints, Key Usage criticality, EKU), JWT-SVID claims, Trust Bundle JWKS shape including base64 + DER validation of &lt;code&gt;x5c&lt;/code&gt;. What it does not: live Workload API behaviour, federation endpoint trust, signature verification against a specific bundle. Those need a running Agent and are out of scope for a static checker.&lt;/p&gt;

&lt;p&gt;Source, issues, releases: &lt;a href="https://github.com/0-draft/spiffe-compliance-checker" rel="noopener noreferrer"&gt;https://github.com/0-draft/spiffe-compliance-checker&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SPIFFE compliance has two definitions and you should know which one you're talking about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;spec-defined floor&lt;/strong&gt; is SPIFFE-ID plus SVID. That's it.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;practical floor&lt;/strong&gt; is that plus Workload API plus Trust Bundle. Federation joins the list only if you cross trust domains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something claims SPIFFE compliance, ask at what level.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully compliant (SPIRE)&lt;/strong&gt;: meets every spec, interoperable across the board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVID-compatible (Istio)&lt;/strong&gt;: SVID format is compliant, Workload Endpoint is custom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPIRE-dependent (Cilium SPIRE path)&lt;/strong&gt;: bundles a SPIFFE implementation. The rest is its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not yet claimable (Cilium ztunnel)&lt;/strong&gt;: a separate mTLS path that doesn't go through SPIRE and hasn't been declared SPIFFE compliant by upstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just want to know whether an artifact in front of you is compliant? Run it through &lt;code&gt;scc&lt;/code&gt; (section 10). Building your own implementation? Walk the MUSTs in sections 3 to 7 one at a time. SVID format alone is feasible from scratch. The Workload API is where the scope explodes. &lt;a href="https://github.com/spiffe/go-spiffe" rel="noopener noreferrer"&gt;go-spiffe/v2&lt;/a&gt; and &lt;a href="https://github.com/spiffe/java-spiffe" rel="noopener noreferrer"&gt;java-spiffe&lt;/a&gt; are the official libraries. Wrapping them is the saner starting point than reimplementing the gRPC service.&lt;/p&gt;

&lt;p&gt;What makes SPIFFE useful is that workload authentication completes inside a vendor-neutral spec. AWS IAM, Google IAM, Kubernetes Service Accounts, none of them have to be in the trust path. Wire SPIFFE in once and cross-environment authentication stops relying on IP allowlists and pre-shared secrets.&lt;/p&gt;

</description>
      <category>spiffe</category>
      <category>identity</category>
      <category>security</category>
      <category>workloadidentity</category>
    </item>
    <item>
      <title>AWS SigV4 and SigV4A Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Sat, 30 May 2026 11:19:22 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-sigv4-and-sigv4a-deep-dive-12li</link>
      <guid>https://dev.to/kanywst/aws-sigv4-and-sigv4a-deep-dive-12li</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hitting S3 from &lt;code&gt;boto3&lt;/code&gt;, I had never thought about SigV4. The SDK does everything. I knew an &lt;code&gt;Authorization: AWS4-HMAC-SHA256 ...&lt;/code&gt; header was being assembled somewhere under the hood, but I had never built one by hand.&lt;/p&gt;

&lt;p&gt;Multi-Region Access Point (MRAP) destroyed that complacency. The instant I hit S3 through MRAP from Lambda, an algorithm I had never seen called &lt;code&gt;AWS4-ECDSA-P256-SHA256&lt;/code&gt; showed up instead of the usual SigV4, and the old &lt;code&gt;botocore&lt;/code&gt; I had pinned locally crashed with &lt;code&gt;InvalidSignature&lt;/code&gt;. AWS has two signing schemes: &lt;strong&gt;SigV4&lt;/strong&gt; and &lt;strong&gt;SigV4A&lt;/strong&gt;. The latter is asymmetric, using ECDSA instead of HMAC.&lt;/p&gt;

&lt;p&gt;This article dissects both, in this order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why AWS uses a custom signature instead of Bearer Token&lt;/li&gt;
&lt;li&gt;Background: what HMAC and SHA-256 contribute&lt;/li&gt;
&lt;li&gt;The four SigV4 steps (Canonical Request / StringToSign / SigningKey derivation / Signature)&lt;/li&gt;
&lt;li&gt;SigV4 written in 80 lines of Python&lt;/li&gt;
&lt;li&gt;Chunked Upload (STREAMING-AWS4-HMAC-SHA256-PAYLOAD)&lt;/li&gt;
&lt;li&gt;Streaming SigV4 (WebSocket / IoT MQTT)&lt;/li&gt;
&lt;li&gt;What is inside a Presigned URL&lt;/li&gt;
&lt;li&gt;SigV4A: asymmetric signing on ECDSA P-256&lt;/li&gt;
&lt;li&gt;SigV4 vs SigV4A comparison&lt;/li&gt;
&lt;li&gt;Clock Skew traps and debugging tips&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Why a custom signature instead of Bearer Token
&lt;/h2&gt;

&lt;p&gt;In the REST world, &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; is the default. OAuth 2.0 is basically the same. Bearer means "whoever holds the token is legitimate", and if one is stolen it is over. Under HTTPS that is usually fine in practice, but AWS &lt;strong&gt;explicitly refused that design&lt;/strong&gt;. There are three reasons.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F01-bearer-vs-sigv4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F01-bearer-vs-sigv4.png" alt="Bearer Token vs SigV4 design comparison" width="800" height="866"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The three design goals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never put the Secret Key on the wire&lt;/strong&gt;: &lt;code&gt;SecretAccessKey&lt;/code&gt; is an absurdly powerful credential. Sending it on every call is a non-starter. SigV4 does not sign with the Secret Key itself; it signs with a short-lived key derived from it through HMAC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay protection&lt;/strong&gt;: the signature covers an &lt;strong&gt;ISO8601 timestamp&lt;/strong&gt; and the &lt;strong&gt;CredentialScope&lt;/strong&gt; (date + region + service), so it is bound to a moment in time and a place. A captured signature replayed the next day will not pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper detection&lt;/strong&gt;: HTTP method, URI, query string, headers, and the SHA-256 of the body are all in the signed string. Flip a single byte in transit and the signature mismatches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SigV4 proves three things at once: who sent it, when, and what was in it. Completely different model from Bearer.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Background: HMAC and SHA-256 in one paragraph
&lt;/h2&gt;

&lt;p&gt;SigV4 sits on top of HMAC-SHA256. &lt;strong&gt;SHA-256&lt;/strong&gt; compresses arbitrary-length input into a fixed 256-bit digest. It is a one-way function with collision resistance (you cannot produce two inputs with the same hash) and preimage resistance (you cannot reverse it). &lt;strong&gt;HMAC&lt;/strong&gt; (Hash-based MAC) wraps a key around a message: &lt;code&gt;HMAC(key, msg) = SHA256(key XOR opad || SHA256(key XOR ipad || msg))&lt;/code&gt;. Anyone without the key cannot reproduce the output. SigV4 chains HMAC four times to derive a signing key, then HMACs the StringToSign with it. The hex of that final HMAC becomes &lt;code&gt;Signature=...&lt;/code&gt; in the Authorization header. SigV4A swaps only that last step from HMAC to ECDSA. Hold that mental model and the rest is detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The four SigV4 steps at a glance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F02-four-steps-overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F02-four-steps-overview.png" alt="Four SigV4 steps overview" width="800" height="946"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One step at a time from here.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Step 1: Building the Canonical Request
&lt;/h2&gt;

&lt;p&gt;The signature only works if "the same request always serializes to the same string". Header order and URI escape variants have to be flattened out. That flattening is what the Canonical Request does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CanonicalRequest =
  HTTPRequestMethod + '\n' +
  CanonicalURI + '\n' +
  CanonicalQueryString + '\n' +
  CanonicalHeaders + '\n' +
  SignedHeaders + '\n' +
  HashedPayload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules for each element, in one diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F03-canonical-request-rules.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F03-canonical-request-rules.png" alt="Canonical Request normalization rules" width="800" height="795"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three traps that bite hard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URI encoded twice&lt;/strong&gt;: for AWS APIs other than S3, the path is URI-encoded &lt;strong&gt;twice&lt;/strong&gt;. A frequent SDK bug. S3 is the only exception, encoded once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query string sort&lt;/strong&gt;: &lt;code&gt;b=2&amp;amp;a=1&lt;/code&gt; becomes &lt;code&gt;a=1&amp;amp;b=2&lt;/code&gt;. When the same key appears multiple times, sort the values too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header value trim&lt;/strong&gt;: strip leading and trailing whitespace, then collapse runs of internal whitespace to a single space. &lt;code&gt;Foo:  bar  baz&lt;/code&gt; becomes &lt;code&gt;foo:bar baz&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonical_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# URI: encode once for S3, twice for others (this example assumes S3)
&lt;/span&gt;    &lt;span class="n"&gt;canonical_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/-_.~&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Query: sort by key, encode values
&lt;/span&gt;    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;canonical_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-_.~&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-_.~&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Headers: lowercase + sort + value trim
&lt;/span&gt;    &lt;span class="n"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="n"&gt;sorted_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;canonical_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sorted_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;signed_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Payload: SHA256 hex
&lt;/span&gt;    &lt;span class="n"&gt;hashed_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_uri&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_headers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signed_headers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hashed_payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;signed_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashed_payload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;canonical_headers&lt;/code&gt; ends with &lt;code&gt;\n&lt;/code&gt;, and the line after it brings another &lt;code&gt;\n&lt;/code&gt;, so the serialized form has two newlines in a row. That is spec-correct. "Fix" it to a single newline and &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt; greets you immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Step 2: Building the StringToSign
&lt;/h2&gt;

&lt;p&gt;Once the Canonical Request exists, SHA256 it and combine the result with the signing scope (algorithm + datetime + region + service) into a single string. That is the StringToSign.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;StringToSign =
  Algorithm + '\n' +
  RequestDateTime + '\n' +
  CredentialScope + '\n' +
  HEX(SHA256(CanonicalRequest))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concrete example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS4-HMAC-SHA256
20260517T120000Z
20260517/us-east-1/s3/aws4_request
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CredentialScope&lt;/code&gt; has the shape &lt;code&gt;&amp;lt;date&amp;gt;/&amp;lt;region&amp;gt;/&amp;lt;service&amp;gt;/aws4_request&lt;/code&gt;. That alone tells AWS "this signature is for this day, this region, and this service". A signature scoped to &lt;code&gt;s3&lt;/code&gt; cannot be reused against &lt;code&gt;dynamodb&lt;/code&gt;. That is the core of replay protection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;string_to_sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amz_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_req&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS4-HMAC-SHA256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/aws4_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;hashed_cr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical_req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;amz_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hashed_cr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;amz_date&lt;/code&gt; is the basic ISO8601 form &lt;code&gt;20260517T120000Z&lt;/code&gt; (no &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;:&lt;/code&gt;). &lt;code&gt;scope_date&lt;/code&gt; is just &lt;code&gt;20260517&lt;/code&gt;. Mismatching them is a classic bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Step 3: Deriving the SigningKey (4-stage HMAC chain)
&lt;/h2&gt;

&lt;p&gt;This is the heart of SigV4. You never sign with the Secret Key itself. You chain HMAC four times from the Secret Key to build &lt;strong&gt;kSigning&lt;/strong&gt;, and sign with that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F04-signing-key-derivation.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F04-signing-key-derivation.png" alt="Four-stage HMAC chain that derives the signing key" width="800" height="959"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why four stages. &lt;strong&gt;Key separation&lt;/strong&gt;. kDate is "a key valid only for this day", kRegion is "valid only for this day and this region", and so on. Each stage narrows the scope further. Even if an attacker leaks kRegion, it cannot be used on another day or in another region. &lt;strong&gt;The Secret Key is permanent, but kSigning is disposable, scoped to one day, one region, one service.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;k_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k_signing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws4_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;k_signing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;"AWS4" + SecretAccessKey&lt;/code&gt; prefix is hard-coded in the spec. Forget the &lt;code&gt;AWS4&lt;/code&gt; and &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt; lands on the first request.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Step 4: Computing the Signature and the Authorization Header
&lt;/h2&gt;

&lt;p&gt;The last step is just HMAC the StringToSign with kSigning and hex-encode it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_signing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_signing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Authorization header is assembled like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: AWS4-HMAC-SHA256
  Credential=&amp;lt;AccessKeyId&amp;gt;/&amp;lt;scope&amp;gt;,
  SignedHeaders=&amp;lt;signed&amp;gt;,
  Signature=&amp;lt;hex&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260517/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Credential&lt;/code&gt; holds the AccessKeyId and the CredentialScope, &lt;code&gt;SignedHeaders&lt;/code&gt; lists the signed header names with &lt;code&gt;;&lt;/code&gt; separators, and &lt;code&gt;Signature&lt;/code&gt; is the hex string. This is exactly what the SDK is assembling for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Full implementation: hitting S3 GetObject with SigV4
&lt;/h2&gt;

&lt;p&gt;The four steps wired together in under 80 lines. Pure &lt;code&gt;urllib&lt;/code&gt;, no SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;

&lt;span class="n"&gt;ACCESS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIAIOSFODNN7EXAMPLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;REGION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SERVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BUCKET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kt-sigv4-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BUCKET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.s3.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hmac_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws4_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sigv4_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;amz_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y%m%dT%H%M%SZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scope_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y%m%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SERVICE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/aws4_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;canonical_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/-_.~&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;canonical_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;payload_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;headers_to_sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-amz-content-sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-amz-date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amz_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;sorted_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers_to_sign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;canonical_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;headers_to_sign&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sorted_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;signed_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;canonical_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_uri&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;canonical_headers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signed_headers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hashed_cr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;string_to_sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS4-HMAC-SHA256&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;amz_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hashed_cr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;k_signing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SERVICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_signing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;authorization&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS4-HMAC-SHA256 Credential=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCESS_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SignedHeaders=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signed_headers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Signature=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers_to_sign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sigv4_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical failures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgot &lt;code&gt;host&lt;/code&gt; in headers_to_sign: SignatureDoesNotMatch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-amz-date&lt;/code&gt; and the date inside &lt;code&gt;Authorization&lt;/code&gt; differ: SignatureDoesNotMatch&lt;/li&gt;
&lt;li&gt;Body is empty but you forgot to compute payload_hash: SignatureDoesNotMatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK does all of this correctly. When writing your own, the fastest debugging path is &lt;strong&gt;byte-for-byte diff against what the SDK produces&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. End-to-end sequence: Client and Server verification
&lt;/h2&gt;

&lt;p&gt;The server side (AWS) reproduces the exact same procedure to build the signature, then compares against what the client sent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F05-client-server-verification.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F05-client-server-verification.png" alt="Client signs and server reverifies sequence" width="800" height="869"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When AWS returns &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt;, &lt;strong&gt;the response body contains the Canonical Request that AWS itself reconstructed&lt;/strong&gt;. That is gold for debugging. Diff your own Canonical Request against the one AWS built and the divergent element (header trim, URI encoding, missing header) pops out immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Chunked Upload: STREAMING-AWS4-HMAC-SHA256-PAYLOAD
&lt;/h2&gt;

&lt;p&gt;For large uploads to S3, hashing the entire body up front is not realistic. Hashing a 10 GB file before you even start sending it is a latency disaster. So S3 has a STREAMING mode where &lt;strong&gt;each chunk is signed individually&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The headers look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD
Content-Encoding: aws-chunked
x-amz-decoded-content-length: &amp;lt;original body size&amp;gt;
Content-Length: &amp;lt;size including chunk headers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body is chunked-transfer-style but with a custom format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10000;chunk-signature=&amp;lt;sig1&amp;gt;\r\n
&amp;lt;8192 byte chunk 1&amp;gt;\r\n
10000;chunk-signature=&amp;lt;sig2&amp;gt;\r\n
&amp;lt;8192 byte chunk 2&amp;gt;\r\n
...
0;chunk-signature=&amp;lt;sigN&amp;gt;\r\n
\r\n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each chunk's signature is computed by chaining the previous chunk's signature with the current chunk's hash. Swap a chunk in the middle and every signature after it falls apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F06-chunked-signature-chain.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F06-chunked-signature-chain.png" alt="Chunk signatures chained through previous-signature" width="800" height="1720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The StringToSign gets a chunk-specific extension.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS4-HMAC-SHA256-PAYLOAD
&amp;lt;amz-date&amp;gt;
&amp;lt;scope&amp;gt;
&amp;lt;previous-signature&amp;gt;
HEX(SHA256(""))
HEX(SHA256(chunk-data))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;previous-signature&lt;/code&gt; is what closes the chain. The stream terminates with a &lt;strong&gt;zero-byte chunk&lt;/strong&gt;. There is also a &lt;code&gt;STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER&lt;/code&gt; variant that appends trailer headers like CRC32C for an extra integrity check.&lt;/p&gt;

&lt;p&gt;Writing this by hand is basically masochism. Let the SDK handle it. But knowing the mechanics makes questions like "why is Content-Length different from x-amz-decoded-content-length" trivial to answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Streaming SigV4: WebSocket / IoT MQTT
&lt;/h2&gt;

&lt;p&gt;SigV4 also rides on &lt;strong&gt;WebSocket&lt;/strong&gt; and &lt;strong&gt;MQTT over WebSocket&lt;/strong&gt;, not just plain HTTP. IoT Core, API Gateway WebSocket, and CloudWatch Logs Live Tail all sit here.&lt;/p&gt;

&lt;p&gt;These use a &lt;strong&gt;Presigned URL form that embeds SigV4 into the connection URL's query string&lt;/strong&gt;. The &lt;code&gt;Authorization&lt;/code&gt; header is hard to attach to a WebSocket Upgrade, so cramming everything into the URL lets the handshake complete in one round trip. With MQTT over WebSocket the URL ends up as &lt;code&gt;wss://&amp;lt;endpoint&amp;gt;/mqtt?X-Amz-Algorithm=...&amp;amp;X-Amz-Signature=...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The streaming case is special because &lt;strong&gt;the connection lives for a long time&lt;/strong&gt;. The SigV4 signature itself is checked only once at connection establishment, so even after &lt;code&gt;X-Amz-Expires&lt;/code&gt; passes, the existing connection keeps going (depending on the AWS implementation). Reconnecting requires a fresh signature.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. What is inside a Presigned URL
&lt;/h2&gt;

&lt;p&gt;A Presigned URL is &lt;strong&gt;the Authorization header crammed into the query string&lt;/strong&gt;. Heavily used for "hand someone a URL and let the browser download the S3 object directly".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://kt-bucket.s3.us-east-1.amazonaws.com/report.pdf
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &amp;amp;X-Amz-Credential=AKIA.../20260517/us-east-1/s3/aws4_request
  &amp;amp;X-Amz-Date=20260517T120000Z
  &amp;amp;X-Amz-Expires=900
  &amp;amp;X-Amz-SignedHeaders=host
  &amp;amp;X-Amz-Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Properties.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;X-Amz-Expires&lt;/strong&gt;: in seconds. &lt;strong&gt;Maximum 604800 (= 7 days)&lt;/strong&gt;. The 7-day cap exists because the SigV4 signing key rotates on roughly a 7-day cycle (kDate is per-day, but kSigning is valid for about 7 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Amz-SignedHeaders=host&lt;/strong&gt;: no body, no extra headers, so only host needs to be signed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload hash handling&lt;/strong&gt;: presigned URLs use either &lt;code&gt;UNSIGNED-PAYLOAD&lt;/code&gt; or the actual body hash. Looser than the header-based form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch out for these.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Presigned URL minted with IAM Role temporary credentials (e.g. STS AssumeRole) is capped by the credential's own lifetime&lt;/strong&gt;. &lt;code&gt;AssumeRole&lt;/code&gt; defaults to 1 hour for &lt;code&gt;DurationSeconds&lt;/code&gt;, configurable up to 12 hours, but once the SessionToken expires the URL dies. This is the standard root cause when someone passes &lt;code&gt;--expires-in 604800&lt;/code&gt; and the URL still dies after 1 hour.&lt;/li&gt;
&lt;li&gt;If you are handing the URL to a browser, mint it with an &lt;strong&gt;IAM User long-term key&lt;/strong&gt; or raise the Role's MaxSessionDuration. EC2 Instance Profile lands in the same trap: IMDS auto-rotates, but after the rotation any URL signed with the previous credentials is dead.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  13. SigV4A: the asymmetric variant
&lt;/h2&gt;

&lt;p&gt;Now the main event. The instant you hit &lt;strong&gt;Multi-Region Access Point (MRAP)&lt;/strong&gt;, the SDK silently flips from SigV4 to SigV4A. The algorithm name is &lt;code&gt;AWS4-ECDSA-P256-SHA256&lt;/code&gt;. It uses &lt;strong&gt;ECDSA (Elliptic Curve Digital Signature Algorithm)&lt;/strong&gt; with NIST P-256 instead of HMAC.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F07-sigv4a-kdf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F07-sigv4a-kdf.png" alt="SigV4A keypair derivation from SecretAccessKey" width="800" height="1278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it actually works
&lt;/h3&gt;

&lt;p&gt;SigV4A &lt;strong&gt;derives an ECDSA P-256 keypair from the SecretAccessKey through a KDF&lt;/strong&gt;. The spec uses &lt;code&gt;input_key = "AWS4A" || sk&lt;/code&gt;, the label &lt;code&gt;AWS4-ECDSA-P256-SHA256&lt;/code&gt;, the &lt;code&gt;akid&lt;/code&gt; (AccessKeyId) as context, and runs a counter that iterates until the derived scalar lands below the P-256 order &lt;code&gt;n&lt;/code&gt;. The resulting scalar &lt;code&gt;c&lt;/code&gt; becomes the private key as &lt;code&gt;k = c + 1&lt;/code&gt;, and the public key is &lt;code&gt;Q = k * G&lt;/code&gt;. The verifier needs only the public key to check the signature. The canonical implementation lives in &lt;code&gt;key_derivation.c&lt;/code&gt; in &lt;code&gt;awslabs/aws-c-auth&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Multi-Region forces this
&lt;/h3&gt;

&lt;p&gt;MRAP is a representative endpoint for a set of buckets across multiple regions, and any request can be routed to any of them. SigV4 &lt;strong&gt;bakes the region name directly into CredentialScope&lt;/strong&gt;, so a signature minted for &lt;code&gt;us-east-1&lt;/code&gt; simply cannot be verified at &lt;code&gt;us-west-2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;SigV4A &lt;strong&gt;drops the region segment from CredentialScope entirely&lt;/strong&gt; and replaces it with the &lt;strong&gt;&lt;code&gt;X-Amz-Region-Set&lt;/code&gt;&lt;/strong&gt; header that declares "this signature is valid in us-east-1, us-west-2, and eu-west-1". CredentialScope changes like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigV4:  20260517/us-east-1/s3/aws4_request
SigV4A: 20260517/s3/aws4_request   (region segment removed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;X-Amz-Region-Set&lt;/code&gt; value is a comma-separated list like &lt;code&gt;us-east-1,us-west-2&lt;/code&gt;, and wildcards like &lt;code&gt;us-east-*&lt;/code&gt; or a bare &lt;code&gt;*&lt;/code&gt; are allowed too.&lt;/p&gt;

&lt;p&gt;On top of that, every region can verify independently with only the public key. &lt;strong&gt;Distributing a symmetric HMAC key to every region is a security risk&lt;/strong&gt; (compromise one region and they all leak), which is exactly where asymmetric signing pays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the ECDSA here deterministic
&lt;/h3&gt;

&lt;p&gt;Standard ECDSA needs a fresh random nonce &lt;code&gt;k&lt;/code&gt; for every signature (the same input produces different signatures each time). Bias or reuse of &lt;code&gt;k&lt;/code&gt; is a fatal mistake that leaks the private key. SigV4A's implementation lives in AWS Common Runtime (&lt;code&gt;awslabs/aws-c-auth&lt;/code&gt; + &lt;code&gt;aws-c-cal&lt;/code&gt;), and the nonce comes from the OS RNG, so &lt;strong&gt;the same request produces a different signature on every call&lt;/strong&gt;. Whether it follows RFC 6979 (deterministic ECDSA) is not stated explicitly in public docs. "&lt;code&gt;awscrt&lt;/code&gt; handles the nonce correctly" is enough to know in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling your own is brutal
&lt;/h3&gt;

&lt;p&gt;ECDSA has heavy math, so if you go custom, use the &lt;code&gt;cryptography&lt;/code&gt; library. AWS officially recommends &lt;code&gt;aws-crt&lt;/code&gt; (a C library bound into Python through &lt;code&gt;awscrt&lt;/code&gt;). &lt;code&gt;botocore&lt;/code&gt; treats &lt;code&gt;awscrt&lt;/code&gt; as an &lt;strong&gt;optional dependency&lt;/strong&gt;. Install with &lt;code&gt;pip install botocore[crt]&lt;/code&gt; (or &lt;code&gt;pip install boto3[crt]&lt;/code&gt;) and SigV4A kicks in automatically against MRAP. Plain &lt;code&gt;boto3&lt;/code&gt; without the crt extra crashes against MRAP with &lt;code&gt;MissingDependencyException&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. SigV4 vs SigV4A side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;SigV4&lt;/th&gt;
&lt;th&gt;SigV4A&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AWS4-HMAC-SHA256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AWS4-ECDSA-P256-SHA256&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key type&lt;/td&gt;
&lt;td&gt;Symmetric (HMAC)&lt;/td&gt;
&lt;td&gt;Asymmetric (ECDSA P-256)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key derivation&lt;/td&gt;
&lt;td&gt;4-stage HMAC chain (kDate to kRegion to kService to kSigning)&lt;/td&gt;
&lt;td&gt;One-shot KDF with a counter to derive an ECDSA keypair (label &lt;code&gt;AWS4-ECDSA-P256-SHA256&lt;/code&gt;, input &lt;code&gt;AWS4A&lt;/code&gt; + secret)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region in CredentialScope&lt;/td&gt;
&lt;td&gt;Fixed (e.g. &lt;code&gt;us-east-1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Segment removed. &lt;code&gt;X-Amz-Region-Set&lt;/code&gt; header declares the regions (wildcards allowed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;AWS recomputes the same HMAC from the same Secret&lt;/td&gt;
&lt;td&gt;AWS verifies the ECDSA signature with the public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main use case&lt;/td&gt;
&lt;td&gt;Any single-region API call&lt;/td&gt;
&lt;td&gt;Multi-Region Access Point, replication paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature determinism&lt;/td&gt;
&lt;td&gt;Deterministic (same input gives same signature)&lt;/td&gt;
&lt;td&gt;Non-deterministic (&lt;code&gt;awscrt&lt;/code&gt; uses a random nonce)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;HMAC is ultra-light (microseconds)&lt;/td&gt;
&lt;td&gt;ECDSA is heavier (milliseconds)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7-day Presigned URL&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported (same conditions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDK support&lt;/td&gt;
&lt;td&gt;All SDKs&lt;/td&gt;
&lt;td&gt;Through &lt;code&gt;aws-crt&lt;/code&gt; (Python needs &lt;code&gt;botocore[crt]&lt;/code&gt;; Go/Java/JS bundle it)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For working developers the only thing that matters in practice: &lt;strong&gt;the moment MRAP enters the picture the SDK flips to SigV4A; everything else stays on SigV4&lt;/strong&gt;. You almost never reach for SigV4A by hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. SigV4 vs SigV4A: scope and verifier differences in one figure
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F08-sigv4-vs-sigv4a-scope.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sigv4-and-sigv4a-deep-dive%2Fdiagrams%2F08-sigv4-vs-sigv4a-scope.png" alt="SigV4 vs SigV4A scope and verifier comparison" width="800" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SigV4 assumes &lt;strong&gt;1 request = 1 region&lt;/strong&gt;, so HMAC is enough. SigV4A is built around &lt;strong&gt;1 request landing in any of several regions&lt;/strong&gt;, which makes a shared HMAC untenable and forces ECDSA. That is the structural difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. The Clock Skew trap
&lt;/h2&gt;

&lt;p&gt;SigV4's &lt;code&gt;CredentialScope&lt;/code&gt; carries &lt;code&gt;&amp;lt;date&amp;gt;&lt;/code&gt; and &lt;code&gt;x-amz-date&lt;/code&gt; carries a second-precision timestamp. AWS rejects requests whose timestamp is &lt;strong&gt;more than ±15 minutes off the current time&lt;/strong&gt; (the S3 docs state this explicitly; the tolerance varies slightly by service).&lt;/p&gt;

&lt;p&gt;Three classic ways to step on this.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker container NTP drift&lt;/strong&gt;: when the container is not NTP-synced, host clock skew kills you instantly. Containers with no visible &lt;code&gt;hwclock&lt;/code&gt; and no &lt;code&gt;ntpd&lt;/code&gt; are the worst offenders. Lambda is fine because AWS keeps it in sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2 with chrony missing&lt;/strong&gt;: AL2023 ships &lt;code&gt;chronyd&lt;/code&gt; by default, but custom AMIs that strip it out drift over time. Point chrony at &lt;code&gt;169.254.169.123&lt;/code&gt; (Amazon Time Sync Service) and the problem disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old Lambda layers / virtualized clocks&lt;/strong&gt;: BPF-based sandboxes where the clock is pinned. Upgrading the Lambda runtime usually fixes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Error examples.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SignatureDoesNotMatch:
  The request signature we calculated does not match the signature you provided.
  Check your key and signing method.
RequestTimeTooSkewed:
  The difference between the request time and the current time is too large.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RequestTimeTooSkewed&lt;/code&gt; is always clock skew, 100%. &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt; is either clock skew or a Canonical Request construction bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SigV4 solves "request signing that detects tampering and replay without ever exposing the secret". Chain HMAC four times to derive a signing key, SHA256 the Canonical Request to build a StringToSign, then HMAC the whole thing for the final signature. Write the four steps by hand once and everything the SDK was hiding finally becomes visible.&lt;/p&gt;

&lt;p&gt;SigV4A is &lt;strong&gt;SigV4 extended to multi-region by going asymmetric (ECDSA P-256)&lt;/strong&gt;. Strip the region from CredentialScope, declare the regions in &lt;code&gt;X-Amz-Region-Set&lt;/code&gt;, and let each region verify independently with the public key. The SDK flips to it automatically when MRAP is involved, so you basically never reach for it by hand.&lt;/p&gt;

&lt;p&gt;The two real-world traps are always the same: &lt;strong&gt;clock skew&lt;/strong&gt; and &lt;strong&gt;canonical-request normalization mistakes&lt;/strong&gt;. Read the exact error message (&lt;code&gt;RequestTimeTooSkewed&lt;/code&gt; vs &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt; vs &lt;code&gt;AccessDenied&lt;/code&gt;) and diff your Canonical Request against the one AWS echoes back. Get that far and SigV4 stops biting.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>sigv4</category>
      <category>security</category>
      <category>signing</category>
    </item>
    <item>
      <title>AWS IAM Roles Anywhere Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Thu, 28 May 2026 15:46:19 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-iam-roles-anywhere-deep-dive-j51</link>
      <guid>https://dev.to/kanywst/aws-iam-roles-anywhere-deep-dive-j51</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;"I want to drop a file from an on-prem server into S3."&lt;br&gt;
"I want to read DynamoDB from a Kubernetes pod sitting in my datacenter."&lt;br&gt;
"I want to pull a secret from AWS Secrets Manager from an app in someone else's cloud."&lt;/p&gt;

&lt;p&gt;Do this the naive way and you end up here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an IAM User&lt;/li&gt;
&lt;li&gt;Issue an access key (the &lt;code&gt;AKIA...&lt;/code&gt; kind)&lt;/li&gt;
&lt;li&gt;Paste it into &lt;code&gt;~/.aws/credentials&lt;/code&gt;, env vars, or some on-prem Secrets Manager&lt;/li&gt;
&lt;li&gt;Use it forever&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where most production incidents start today. Long-lived access keys leak and stay leaked, rotation gets forgotten, and there is no record of who copied them where or when.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS IAM Roles Anywhere&lt;/strong&gt; is the mechanism that hands IAM Role temporary credentials to &lt;strong&gt;workloads outside AWS&lt;/strong&gt; without distributing any long-lived key. The key material is replaced by an &lt;strong&gt;X.509 certificate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article goes deep on Roles Anywhere.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Vocabulary you need first
&lt;/h2&gt;

&lt;p&gt;Roles Anywhere sits on top of two things: IAM Role and PKI (the certificate world). If either is fuzzy you will get lost fast. The bare minimum below.&lt;/p&gt;
&lt;h3&gt;
  
  
  IAM Role / STS / temporary credentials
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What to remember&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A box that says "who is allowed to take this permission on". It has two parts: a &lt;strong&gt;Trust Policy&lt;/strong&gt; (who can assume it) and a &lt;strong&gt;Permission Policy&lt;/strong&gt; (what the assumer can do)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AssumeRole&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The act of taking on a Role. When it succeeds, you immediately get back a set of &lt;strong&gt;temporary credentials&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;STS (Security Token Service)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The AWS service that issues temporary credentials. The &lt;code&gt;sts:AssumeRole&lt;/code&gt; family of APIs lands here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Temporary credentials&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A three-piece set: &lt;code&gt;AccessKeyId&lt;/code&gt; + &lt;code&gt;SecretAccessKey&lt;/code&gt; + &lt;code&gt;SessionToken&lt;/code&gt;. Default lifetime 1 hour, up to 12 hours via the Role config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  X.509 certificate / CA / PKI
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What to remember&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;X.509 certificate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A digital document where a CA signs "this public key belongs to this entity". The cert sitting in front of any HTTPS site is the same shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CA (Certificate Authority)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The party that issues certificates. Trusting a CA's certificate means trusting every certificate it has ever issued&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private CA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A CA for closed environments such as inside a company. AWS sells a managed version called &lt;strong&gt;AWS Private CA (formerly ACM PCA)&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PKI (Public Key Infrastructure)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The whole ecosystem of issuing, distributing, and revoking certificates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private key&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The key paired with the certificate. Digital signatures are made with it. &lt;strong&gt;Never put it on the network&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly: Roles Anywhere is the mechanism that &lt;strong&gt;makes &lt;code&gt;AssumeRole&lt;/code&gt; callable using an X.509 certificate's private-key signature, instead of an IAM User's long-term key&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The big picture and the three actors
&lt;/h2&gt;

&lt;p&gt;Roles Anywhere has three specific concepts: &lt;strong&gt;Trust Anchor / Profile / Role&lt;/strong&gt;. Pin them down visually first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F01-roles-anywhere-overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F01-roles-anywhere-overview.png" alt="Roles Anywhere overview: workload outside AWS, credential helper, Trust Anchor / Profile / Role, CreateSession to STS" width="800" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The three actors and their jobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trust Anchor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The declaration "I (this AWS account) trust this CA"&lt;/td&gt;
&lt;td&gt;A reference to an AWS Private CA, or the PEM of an external CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The declaration "for callers authenticated via this CA, which Roles can they use and with what session limits"&lt;/td&gt;
&lt;td&gt;A list of allowed Role ARNs + session duration + optional Session Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The permission body that activates once assumed&lt;/td&gt;
&lt;td&gt;Trust Policy (allowing the Roles Anywhere service principal) + Permission Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Walking through each in order.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Trust Anchor: the root of trust
&lt;/h2&gt;

&lt;p&gt;A Trust Anchor is the declaration &lt;strong&gt;"this AWS account trusts this CA"&lt;/strong&gt;. When Roles Anywhere sees a certificate, it walks the chain to confirm the certificate was issued by this CA.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F02-trust-anchor-chain.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F02-trust-anchor-chain.png" alt="Certificate chain from end-entity up through intermediate CA to root CA, with the root CA registered as Trust Anchor" width="800" height="1454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are two ways to create a Trust Anchor.&lt;/p&gt;
&lt;h3&gt;
  
  
  A. Use AWS Private CA (PCA) as the source
&lt;/h3&gt;

&lt;p&gt;AWS Private CA (formerly ACM PCA) is AWS's managed paid CA. When creating a Trust Anchor you say "use this PCA", and every certificate issued by that PCA is trusted automatically.&lt;/p&gt;

&lt;p&gt;PCA has two modes (Tokyo region reference pricing, varies by region):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;General Purpose Mode&lt;/strong&gt;: &lt;strong&gt;400 USD/month&lt;/strong&gt; plus per-certificate issuance fees. Full features (CRL publication, long-lived certs, freely issued via API). Capable as a replacement for an internal PKI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-Lived Certificate Mode&lt;/strong&gt;: &lt;strong&gt;50 USD/month&lt;/strong&gt; plus per-certificate issuance fees. Only for certificates with &lt;strong&gt;lifetime under 7 days&lt;/strong&gt;, no CRL publication. Optimised for the Roles Anywhere "issue short-lived certs frequently" usage pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upside: issuance, revocation, and renewal automation lean on AWS. The AWS Private CA Issuer for &lt;code&gt;cert-manager&lt;/code&gt; lets Kubernetes consume it.&lt;/p&gt;

&lt;p&gt;Downside: &lt;strong&gt;either mode bills a flat monthly fee just for having a CA stood up&lt;/strong&gt;. For verification or dev work, an external CA is cheaper.&lt;/p&gt;
&lt;h3&gt;
  
  
  B. Upload an existing external CA
&lt;/h3&gt;

&lt;p&gt;Upload a CA certificate in PEM and register it as a Trust Anchor. If you already have an internal PKI (HashiCorp Vault, smallstep &lt;code&gt;step-ca&lt;/code&gt;, an OpenSSL-based homemade CA, etc.), the extra cost is zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rolesanywhere create-trust-anchor &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"my-internal-ca"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="s1"&gt;'{
        "sourceType": "CERTIFICATE_BUNDLE",
        "sourceData": {
            "x509CertificateData": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----"
        }
    }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With an external CA you own &lt;strong&gt;revocation management (CRL updates)&lt;/strong&gt;. Covered in §8.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Profile: which Role, with what guardrails
&lt;/h2&gt;

&lt;p&gt;A Profile is &lt;strong&gt;"the usage rules for callers authenticated against a given Trust Anchor"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F03-profile-structure.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F03-profile-structure.png" alt="Profile branches into multiple Roles plus Session Policy and Session Duration constraints" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What goes into a Profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;roleArns&lt;/strong&gt;: list of IAM Role ARNs that may be assumed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;durationSeconds&lt;/strong&gt;: maximum session lifetime (900 to 43200 seconds, i.e. 15 minutes to 12 hours, capped by the Role's &lt;code&gt;MaxSessionDuration&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sessionPolicy / managedPolicyArns&lt;/strong&gt;: extra policy that applies only to the session. "The Role's Permission Policy is X, but calls coming through this Profile are further narrowed to Y"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Session Policy intersects with the Role's Permission Policy via &lt;strong&gt;AND&lt;/strong&gt;. Even if the Role allows &lt;code&gt;s3:*&lt;/code&gt;, narrowing the Session Policy to &lt;code&gt;s3:GetObject&lt;/code&gt; means only &lt;code&gt;s3:GetObject&lt;/code&gt; is effectively permitted.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Role's Trust Policy: who is allowed to call
&lt;/h2&gt;

&lt;p&gt;The Role itself is created the usual way, but its &lt;strong&gt;Trust Policy&lt;/strong&gt; (the policy saying "who can Assume this Role") has a Roles Anywhere-specific shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rolesanywhere.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sts:TagSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sts:SetSourceIdentity"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/x509Subject/CN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server-a.example.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:rolesanywhere:ap-northeast-1:123456789012:trust-anchor/abc-..."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The service principal is &lt;code&gt;rolesanywhere.amazonaws.com&lt;/code&gt;&lt;/strong&gt;. Without this entry, AssumeRole via the Roles Anywhere CreateSession path will not work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowing &lt;code&gt;sts:TagSession&lt;/code&gt;&lt;/strong&gt; lets the certificate's Subject / SAN / Issuer be injected as session tags (more on this below)&lt;/li&gt;
&lt;li&gt;Conditions like &lt;strong&gt;&lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt;&lt;/strong&gt; let you &lt;strong&gt;narrow down further based on the certificate contents&lt;/strong&gt;. Fine-grained control like "only certificates with CN &lt;code&gt;server-a.example.com&lt;/code&gt; may assume this" is possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws:SourceArn&lt;/code&gt;&lt;/strong&gt; pins the call to a specific Trust Anchor, so a request coming from a Trust Anchor you did not expect is denied&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Session tags auto-populated from the certificate
&lt;/h3&gt;

&lt;p&gt;A session created through CreateSession + AssumeRole automatically carries certificate attributes as tags. The common ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag key&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Common Name of the certificate Subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:PrincipalTag/x509SAN/DNS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DNS name in the Subject Alternative Name (SAN)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:PrincipalTag/x509SAN/URI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URI in SAN (e.g. a SPIFFE ID like &lt;code&gt;spiffe://example.com/ns/prod/sa/app&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:PrincipalTag/x509Issuer/CN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CN of the issuing CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:PrincipalTag/x509Serial&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Certificate serial number&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small bit of supplementary vocabulary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SAN (Subject Alternative Name)&lt;/strong&gt;: an X.509 extension field that holds additional identifiers beyond Common Name, including multiple hostnames, IPs, or URIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPIFFE ID&lt;/strong&gt;: a URI-format workload identifier defined by the SPIFFE spec (&lt;code&gt;spiffe://...&lt;/code&gt;). If your internal identity platform uses SPIFFE, dropping a SPIFFE ID into the SAN URI lets the AWS side reference it directly in a Condition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The upshot: &lt;strong&gt;the Role's Permission Policy can also use &lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt; in its Conditions&lt;/strong&gt;. Patterns like "the &lt;code&gt;server-a.example.com&lt;/code&gt; certificate can only write under the &lt;code&gt;prefix=server-a/*&lt;/code&gt; portion of S3" become possible. That is &lt;strong&gt;ABAC (Attribute Based Access Control)&lt;/strong&gt;: instead of carving permissions up by Role (RBAC-style), &lt;strong&gt;principal attributes (tags) dynamically tighten what is allowed&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The CreateSession flow
&lt;/h2&gt;

&lt;p&gt;Tracing what actually happens from the client's first call until temporary credentials are in hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F04-create-session-sequence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F04-create-session-sequence.png" alt="CreateSession sequence: workload signs request with private key via credential helper, Roles Anywhere verifies cert and CRL, then STS returns temp credentials" width="800" height="897"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A "SigV4 X.509 variant" sits on the SigV4 frame but signs with an X.509 private key&lt;/strong&gt;. Normal AWS APIs sign with &lt;code&gt;AWS4-HMAC-SHA256&lt;/code&gt; (symmetric HMAC using the access key). &lt;code&gt;CreateSession&lt;/code&gt; uses one of &lt;strong&gt;&lt;code&gt;AWS4-X509-RSA-SHA256&lt;/code&gt; / &lt;code&gt;AWS4-X509-ECDSA-SHA256&lt;/code&gt; / &lt;code&gt;AWS4-X509-MLDSA&lt;/code&gt;&lt;/strong&gt; (the last one is post-quantum, PQC-capable) depending on the key type. The Canonical Request construction is the same as SigV4. Only the final step is an asymmetric X.509 signature instead of HMAC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The private key never leaves the credential helper&lt;/strong&gt;. It does not go on the network&lt;/li&gt;
&lt;li&gt;The credentials handed back are &lt;strong&gt;plain IAM temporary credentials&lt;/strong&gt;. The SDK sees nothing unusual. Subsequent S3 / DynamoDB calls go out as normal SigV4 (HMAC)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. What is inside the credential helper
&lt;/h2&gt;

&lt;p&gt;Hand-rolling the &lt;code&gt;CreateSession&lt;/code&gt; signing on the client side is not realistic, so AWS ships an official binary called &lt;strong&gt;&lt;code&gt;aws_signing_helper&lt;/code&gt;&lt;/strong&gt; (repo: &lt;code&gt;aws/rolesanywhere-credential-helper&lt;/code&gt;). That binary is the credential helper.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws_signing_helper&lt;/code&gt; plugs into the AWS CLI / SDK &lt;code&gt;credential_process&lt;/code&gt; spec. Drop this into &lt;code&gt;~/.aws/config&lt;/code&gt; and both the CLI and any SDK transparently fetch credentials through Roles Anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile myapp]&lt;/span&gt;
&lt;span class="py"&gt;credential_process&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bin/aws_signing_helper credential-process &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--certificate /etc/pki/server.pem &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--private-key /etc/pki/server.key &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--trust-anchor-arn arn:aws:rolesanywhere:ap-northeast-1:123456789012:trust-anchor/xxxx &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--profile-arn       arn:aws:rolesanywhere:ap-northeast-1:123456789012:profile/yyyy &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--role-arn          arn:aws:iam::123456789012:role/MyAppRole&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A call like &lt;code&gt;aws s3 ls --profile myapp&lt;/code&gt; triggers &lt;code&gt;credential_process&lt;/code&gt;, which runs &lt;code&gt;aws_signing_helper&lt;/code&gt;, which returns the JSON. The AWS CLI / SDK takes care of &lt;strong&gt;automatic credential refresh&lt;/strong&gt;. No manual refresh needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the private key lives
&lt;/h3&gt;

&lt;p&gt;The choice of private-key storage sets the ceiling on your Roles Anywhere operational quality. &lt;code&gt;aws_signing_helper&lt;/code&gt; supports the following backends. Leak resistance rises as you go down the table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Properties&lt;/th&gt;
&lt;th&gt;Leak resistance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;File&lt;/strong&gt; (&lt;code&gt;/etc/pki/server.key&lt;/code&gt; etc.)&lt;/td&gt;
&lt;td&gt;Easiest. Anyone with read access can &lt;code&gt;cat&lt;/code&gt; it&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;OS keystore&lt;/strong&gt; (Windows Certificate Store / macOS Keychain)&lt;/td&gt;
&lt;td&gt;Protected by OS access control&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;PKCS#11 module&lt;/strong&gt; (HSM / smartcard)&lt;/td&gt;
&lt;td&gt;Key stays inside the HSM, only signing operations are delegated&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;TPM 2.0&lt;/strong&gt; (secure chip on the motherboard)&lt;/td&gt;
&lt;td&gt;Key sealed into hardware, non-exportable&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In high-security environments, sealing the key into PKCS#11 (HSM) or TPM 2.0 so that &lt;strong&gt;the key cannot be extracted at all&lt;/strong&gt; is the right move. &lt;code&gt;aws_signing_helper&lt;/code&gt; exposes flags like &lt;code&gt;--cert-selector&lt;/code&gt; and &lt;code&gt;--tpm-key-handle&lt;/code&gt; to wire into these backends.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Revoking a certificate
&lt;/h2&gt;

&lt;p&gt;You will eventually need to kill a compromised server's certificate immediately. Roles Anywhere supports &lt;strong&gt;CRL (Certificate Revocation List)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Importing a CRL
&lt;/h3&gt;

&lt;p&gt;Generate a revocation list on the CA side and register it in Roles Anywhere as PEM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rolesanywhere import-crl &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"my-ca-crl"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--crl-data&lt;/span&gt; file://crl.pem &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--trust-anchor-arn&lt;/span&gt; arn:aws:rolesanywhere:ap-northeast-1:123456789012:trust-anchor/xxxx &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once registered, Roles Anywhere checks the CRL on every &lt;code&gt;CreateSession&lt;/code&gt;. Calls from revoked certificates are rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-integration with AWS Private CA
&lt;/h3&gt;

&lt;p&gt;When the Trust Anchor source is PCA, calling &lt;code&gt;revoke-certificate&lt;/code&gt; on the PCA causes the PCA to publish a CRL into S3 &lt;strong&gt;within about 30 minutes&lt;/strong&gt;. The standard pattern is to catch that with Lambda and feed it to the &lt;code&gt;ImportCrl&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F05-crl-auto-integration.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F05-crl-auto-integration.png" alt="CRL auto-integration sequence: operator revokes cert in PCA, PCA emits CRL to S3, Lambda picks it up and calls ImportCrl on Roles Anywhere" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Temporarily disabling the CRL check
&lt;/h3&gt;

&lt;p&gt;When you need to disable it during incident response, &lt;code&gt;DisableCrl&lt;/code&gt; flips the check off and &lt;code&gt;EnableCrl&lt;/code&gt; turns it back on. In normal production, leave it on.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. When it fits, and when it does not
&lt;/h2&gt;

&lt;p&gt;Roles Anywhere is powerful but not for every case. The decision flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F06-when-to-use-decision.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-iam-roles-anywhere-deep-dive%2Fdiagrams%2F06-when-to-use-decision.png" alt="Decision tree: workload location and caller type lead to Instance Profile, OIDC federation, Identity Center, or IAM Roles Anywhere" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workloads running inside AWS do not need Roles Anywhere&lt;/strong&gt;. Instance Profile / Execution Role / Pod Identity are the natural choice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your CI can speak OIDC, prefer OIDC&lt;/strong&gt;. GitHub Actions / GitLab CI / etc. emit OIDC officially. Roles Anywhere is unnecessary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you cannot emit OIDC, you already have an X.509-based PKI, and the server is fully on-prem&lt;/strong&gt;, that is where Roles Anywhere shines&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where Roles Anywhere is a poor fit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;"We do not have an internal PKI yet" cases: standing up a CA before introducing Roles Anywhere is much more work than Roles Anywhere itself. If there is an OIDC path, take that&lt;/li&gt;
&lt;li&gt;"Thousands of clients, each with its own certificate, CRL updated daily" cases: at this scale CRL propagation lag and other factors need real design work&lt;/li&gt;
&lt;li&gt;"Calling from inside a VPC" cases: anything inside a VPC is already inside AWS, so attaching a Role the normal way is enough&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. Pricing and limits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The IAM Roles Anywhere service itself is free&lt;/strong&gt;. No per-CreateSession request fees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Private CA is the only billed component&lt;/strong&gt;. &lt;strong&gt;400 USD/month&lt;/strong&gt; for General Purpose, &lt;strong&gt;50 USD/month&lt;/strong&gt; for Short-Lived Certificate, plus per-certificate issuance fees&lt;/li&gt;
&lt;li&gt;Using an external CA (HashiCorp Vault / step-ca / homemade PKI etc.) avoids the PCA bill (you pay in operational effort instead)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Main limits (from official Service Quotas, per Region)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Increase request&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trust Anchors per account&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profiles per account&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roles per Profile&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not allowed&lt;/strong&gt; (hard limit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registered certificates per Trust Anchor&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not allowed&lt;/strong&gt; (two slots, for rotation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRLs per Trust Anchor&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not allowed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CreateSession rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 req/sec&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session lifetime&lt;/td&gt;
&lt;td&gt;15 minutes to 12 hours&lt;/td&gt;
&lt;td&gt;Within the Role's &lt;code&gt;MaxSessionDuration&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that &lt;strong&gt;&lt;code&gt;CreateSession&lt;/code&gt; is 10 req/sec&lt;/strong&gt;. A design that pulls short-lived credentials in volume will hit this per-Region rate. The basic pattern is to &lt;strong&gt;cache credentials on the client side&lt;/strong&gt; and refresh just before expiration. &lt;code&gt;aws_signing_helper&lt;/code&gt;'s &lt;code&gt;credential_process&lt;/code&gt; does this automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Don'ts and Do's
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;❌ Don't&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop the private key as a plaintext file readable by anyone&lt;/li&gt;
&lt;li&gt;Reuse one certificate across multiple servers (you lose the ability to tell which one leaked)&lt;/li&gt;
&lt;li&gt;Write a Trust Policy that trusts &lt;code&gt;rolesanywhere.amazonaws.com&lt;/code&gt; alone, with no &lt;code&gt;aws:SourceArn&lt;/code&gt; or certificate Conditions&lt;/li&gt;
&lt;li&gt;Skip CRL operations entirely (i.e. no way to invalidate a cert on compromise)&lt;/li&gt;
&lt;li&gt;Issue certificates with absurdly long lifetimes (e.g. 10 years)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;✅ Do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seal the private key into &lt;strong&gt;TPM 2.0 / HSM (PKCS#11) / OS keystore&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;One server, one certificate. Put a hostname or SPIFFE-ID-equivalent identifier in the Subject / SAN&lt;/li&gt;
&lt;li&gt;Issue &lt;strong&gt;short-lived certificates&lt;/strong&gt; (e.g. 24 hours to a few days) with &lt;strong&gt;automatic renewal&lt;/strong&gt;. &lt;code&gt;cert-manager&lt;/code&gt; and &lt;code&gt;step-ca&lt;/code&gt;'s auto-renew machinery is the standard pattern&lt;/li&gt;
&lt;li&gt;Put both &lt;code&gt;aws:SourceArn&lt;/code&gt; (Trust Anchor) and &lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt; into the Role's Trust Policy&lt;/li&gt;
&lt;li&gt;Automate CRL ingestion and keep &lt;code&gt;DisableCrl&lt;/code&gt; ready as an operations break-glass&lt;/li&gt;
&lt;li&gt;Use session tags (&lt;code&gt;aws:PrincipalTag/x509...&lt;/code&gt;) to drive ABAC in the Role's Permission Policy&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Wrap-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IAM Roles Anywhere hands IAM Role temporary credentials to workloads outside AWS without distributing long-lived keys&lt;/li&gt;
&lt;li&gt;It uses X.509 certificates as the key material, with the issuing CA registered as a Trust Anchor&lt;/li&gt;
&lt;li&gt;Three actors: Trust Anchor (the trusted CA) / Profile (which Role can be used) / Role (the actual permission)&lt;/li&gt;
&lt;li&gt;CreateSession is not plain SigV4. It uses a distinct flow signed with the certificate's private key, and the private key never goes on the network&lt;/li&gt;
&lt;li&gt;The official &lt;code&gt;aws_signing_helper&lt;/code&gt; ships as a &lt;code&gt;credential_process&lt;/code&gt; provider, so existing CLI and SDK code works as-is&lt;/li&gt;
&lt;li&gt;Seal the private key in TPM / HSM / OS keystore. A plain file on disk is an incident waiting to happen&lt;/li&gt;
&lt;li&gt;Revocation goes through CRL via &lt;code&gt;ImportCrl&lt;/code&gt;. PCA-backed setups can auto-integrate via S3&lt;/li&gt;
&lt;li&gt;If you are inside AWS, Roles Anywhere is unnecessary. If CI can speak OIDC, OIDC wins. On-prem, multi-cloud, and existing-PKI worlds are where it actually fits&lt;/li&gt;
&lt;li&gt;The service itself is free. Only PCA costs money&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/introduction.html" rel="noopener noreferrer"&gt;What is AWS Identity and Access Management Roles Anywhere?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/trust-model.html" rel="noopener noreferrer"&gt;The IAM Roles Anywhere trust model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/credential-helper.html" rel="noopener noreferrer"&gt;Get temporary security credentials from IAM Roles Anywhere&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/rolesanywhere-credential-helper" rel="noopener noreferrer"&gt;aws/rolesanywhere-credential-helper (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/security/extend-aws-iam-roles-to-workloads-outside-of-aws-with-iam-roles-anywhere/" rel="noopener noreferrer"&gt;Extend AWS IAM roles to workloads outside of AWS with IAM Roles Anywhere&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/security/set-up-aws-private-certificate-authority-to-issue-certificates-for-use-with-iam-roles-anywhere/" rel="noopener noreferrer"&gt;Set up AWS Private Certificate Authority to issue certificates for use with IAM Roles Anywhere&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/12/iam-roles-anywhere-credential-helper-tpm-2-0/" rel="noopener noreferrer"&gt;IAM Roles Anywhere credential helper now supports TPM 2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unit42.paloaltonetworks.com/aws-roles-anywhere/" rel="noopener noreferrer"&gt;Roles Here? Roles There? Roles Anywhere: Exploring the Security of AWS IAM Roles Anywhere (Unit 42)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>iam</category>
      <category>security</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Microsegmentation Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Wed, 27 May 2026 15:03:02 +0000</pubDate>
      <link>https://dev.to/kanywst/microsegmentation-deep-dive-from-the-crumbling-castle-wall-to-identity-driven-east-west-control-1c67</link>
      <guid>https://dev.to/kanywst/microsegmentation-deep-dive-from-the-crumbling-castle-wall-to-identity-driven-east-west-control-1c67</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A few years back, an SRE I know said this to me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The firewall on the outside is hardened to hell. But once you're in, it's over. Inside the LAN, everything from Pod-to-Pod to SMB just flows."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You cannot defend a modern system by making the outside stronger. You have to slice the inside of the LAN, the gaps between Pods and containers, the East-West traffic between VMs, into many small zones and police each one. That is &lt;strong&gt;microsegmentation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article tries to nail down microsegmentation at the resolution of implementation and operations, not as a vibe-word. Why VLAN and firewall hit a wall, how the four implementation styles (hypervisor / agent / cloud-native / Identity) actually differ, and how players like VMware NSX, Illumio, Akamai Guardicore, Cilium, and Google BeyondCorp each took their own shot at the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  0. Prerequisites for reading this
&lt;/h2&gt;

&lt;p&gt;A quick glossary for terms used later. Skip if you already know them.&lt;/p&gt;

&lt;h3&gt;
  
  
  North-South traffic and East-West traffic
&lt;/h3&gt;

&lt;p&gt;Traffic crossing the boundary of a data center or cluster is &lt;strong&gt;North-South&lt;/strong&gt;. A user's browser hitting a web server, for example. Traffic &lt;em&gt;inside&lt;/em&gt; the data center, server to server, Pod to Pod, container to container, is &lt;strong&gt;East-West&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In modern systems, East-West dwarfs North-South in both volume and variety. With microservices, containers, and data lakes, a single user request often fans out into dozens of internal API calls. A perimeter firewall sees none of that East-West.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero Trust and "Never Trust, Always Verify"
&lt;/h3&gt;

&lt;p&gt;The idea of throwing out the assumption that "the internal LAN is trusted." Every connection is treated as if it were external, and authorization happens every time based on identity, device state, and context. NIST SP 800-207 is the de facto standard spec.&lt;/p&gt;

&lt;p&gt;Microsegmentation is the main piece of "how do you actually implement Zero Trust at the network layer."&lt;/p&gt;

&lt;h3&gt;
  
  
  PEP / PDP (Policy Enforcement Point / Policy Decision Point)
&lt;/h3&gt;

&lt;p&gt;The core model defined by NIST SP 800-207.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDP (Policy Decision Point)&lt;/strong&gt;: the "brain" that evaluates policy and returns allow/deny. Internally split in two.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PE (Policy Engine)&lt;/strong&gt;: the decision logic itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PA (Policy Administrator)&lt;/strong&gt;: opens and closes sessions based on PE's verdict.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;PEP (Policy Enforcement Point)&lt;/strong&gt;: the "hand" that actually applies the PDP's decision to the wire.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In microsegmentation, the PEP is something sitting right next to each workload: the host OS firewall, the hypervisor's virtual NIC, a sidecar proxy, an eBPF program. The PE consumes identity, device posture, threat intel, behavioral baselines, and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F01-pep-pdp-model.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F01-pep-pdp-model.png" alt="PEP and PDP model" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comparing microsegmentation products comes down to two axes: &lt;strong&gt;where you put the PEP&lt;/strong&gt; and &lt;strong&gt;what signals the PDP can use to decide&lt;/strong&gt;. The four implementation styles in the next sections all map onto one corner of this picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  VLAN, ACL, Security Group
&lt;/h3&gt;

&lt;p&gt;The classic cast of segmentation. VLANs split broadcast domains at L2. ACLs (Access Control List) sit on routers and switches and permit traffic per IP/port. In cloud, Security Groups act as stateful L3/L4 ACLs. All of them are IP/location-based, which (we will see) is exactly the root cause of today's pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  IP-based vs Identity-driven
&lt;/h3&gt;

&lt;p&gt;What you use as the &lt;em&gt;subject&lt;/em&gt; of a policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP-based&lt;/strong&gt;: "allow &lt;code&gt;10.0.1.0/24&lt;/code&gt; to reach &lt;code&gt;10.0.2.5:443&lt;/code&gt;." The address is the subject. VLAN, ACL, and traditional firewalls all live here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity-driven&lt;/strong&gt;: "allow Pods with &lt;code&gt;role=web&lt;/code&gt; to reach Pods with &lt;code&gt;role=db&lt;/code&gt; on &lt;code&gt;tcp/5432&lt;/code&gt;." A label or a cryptographic ID is the subject.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a world where Pod IPs churn every minute because of autoscaling and rescheduling, IP-based policy cannot keep up. Identity-driven exists precisely to dodge that problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  L3/L4 control vs L7 control
&lt;/h3&gt;

&lt;p&gt;How deep the firewall looks before it decides.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;L3/L4&lt;/strong&gt;: IP address and port/protocol. "Allow &lt;code&gt;tcp/5432&lt;/code&gt; from &lt;code&gt;10.0.1.0/24&lt;/code&gt;." VLAN, ACL, Security Group, nftables, classic NSX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L7&lt;/strong&gt;: HTTP method, URL path, gRPC method, Kafka topic, etc. "Allow only &lt;code&gt;POST /api/payment&lt;/code&gt;, block &lt;code&gt;DELETE&lt;/code&gt;." Service meshes (Istio / Envoy), Cilium's L7 policy, Cisco Secure Workload reach into this layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L7 control lets you cut off SQL injection or unauthorized admin-API calls one layer earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  SPIFFE / SPIRE and SVID
&lt;/h3&gt;

&lt;p&gt;A spec (SPIFFE) for giving workloads a &lt;em&gt;cryptographic ID instead of an IP&lt;/em&gt;, plus its reference implementation (SPIRE). A URI of the form &lt;code&gt;spiffe://trust-domain/workload-path&lt;/code&gt; gets embedded into the &lt;strong&gt;URI SAN (Subject Alternative Name)&lt;/strong&gt; field of an X.509 cert. That cert is called an &lt;strong&gt;SVID (SPIFFE Verifiable Identity Document)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During an mTLS handshake, you read the URI SAN out of the peer's SVID and decide "the peer is &lt;code&gt;spiffe://prod/cart&lt;/code&gt;, so allow." That is the foundation of identity-driven microsegmentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  eBPF
&lt;/h3&gt;

&lt;p&gt;A Linux kernel mechanism for safely running small programs inside the kernel. Cilium uses it to do packet filtering, policy evaluation, and L7 observation in one pass, all in kernel space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Dependency Map (ADM)
&lt;/h3&gt;

&lt;p&gt;The core feature of agent-based products like Illumio. It aggregates flow telemetry from every host and automatically draws "&lt;strong&gt;which workload talks to which workload, on which port&lt;/strong&gt;." If you start enforcement without first looking at this map, your policy will halt the business the moment it lands (more on this in section 5).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why "macro" stopped being enough
&lt;/h2&gt;

&lt;p&gt;A quick walk through the history. Microsegmentation did not pop out of nowhere; it is the endpoint of 20 years of "make the segments smaller."&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 The castle wall era (perimeter-only)
&lt;/h3&gt;

&lt;p&gt;The classic 2000s network: a single wall between the LAN and the internet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F02-castle-wall.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F02-castle-wall.png" alt="Castle wall perimeter model" width="800" height="1743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem is obvious. &lt;strong&gt;Once anyone steps inside the wall, the inside is flat.&lt;/strong&gt; The moment an attacker owns one workstation, every internal DB, every printer, every dev server is theirs.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 VLAN + firewall zones (macrosegmentation)
&lt;/h3&gt;

&lt;p&gt;The next generation split the LAN into rough chunks with VLANs and per-zone firewalls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F03-vlan-zones.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F03-vlan-zones.png" alt="VLAN and firewall zones" width="800" height="2370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A real step forward, but "inside the zone" is still flat. Once an attacker is on the web tier, the other web servers in the same VLAN are wide open. In an era where one service has fanned out into dozens or hundreds of workloads, this granularity does not cut it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Microsegmentation (per-workload)
&lt;/h3&gt;

&lt;p&gt;The endpoint: &lt;strong&gt;every single workload gets its own firewall.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F04-per-workload-pep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F04-per-workload-pep.png" alt="Per-workload PEP" width="800" height="3126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The PEP sits right next to the workload.&lt;/strong&gt; Not a physical firewall box. The host OS filter, the hypervisor's virtual NIC, a sidecar proxy, an eBPF program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default deny.&lt;/strong&gt; "Anything not explicitly allowed is dropped" is the starting position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy is written per application.&lt;/strong&gt; "Web to Cart," using services or roles as subjects.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1.4 Macro vs micro
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Macro (VLAN/Zone)&lt;/th&gt;
&lt;th&gt;Micro (Workload)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Granularity&lt;/td&gt;
&lt;td&gt;Subnet / zone&lt;/td&gt;
&lt;td&gt;Workload / Pod / process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subject&lt;/td&gt;
&lt;td&gt;IP, subnet&lt;/td&gt;
&lt;td&gt;Service name, label, SPIFFE ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mainly guards&lt;/td&gt;
&lt;td&gt;North-South&lt;/td&gt;
&lt;td&gt;East-West&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy lifetime&lt;/td&gt;
&lt;td&gt;Long, pinned to IP&lt;/td&gt;
&lt;td&gt;Dynamic, driven by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resistance to lateral movement&lt;/td&gt;
&lt;td&gt;Weak inside the zone&lt;/td&gt;
&lt;td&gt;Cuts per workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of firewalls&lt;/td&gt;
&lt;td&gt;A handful to dozens&lt;/td&gt;
&lt;td&gt;Hundreds to hundreds of thousands (logically)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is not "macro becomes unnecessary," it is "microsegmentation adds one more layer of finer defense inside the macro."&lt;/p&gt;




&lt;h2&gt;
  
  
  2. NotPetya and ransomware pressed the "decision button"
&lt;/h2&gt;

&lt;p&gt;The thing that pushed microsegmentation from "idea" to "product category" was &lt;strong&gt;NotPetya&lt;/strong&gt; in 2017. Once you understand how that incident unfolded, the sudden global appetite for investment makes sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 What happened
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F05-notpetya-sequence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F05-notpetya-sequence.png" alt="NotPetya lateral movement sequence" width="799" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Damage came to roughly $300M for Maersk alone, and over $10B globally (Forrester / Armis estimates). Merck, FedEx's TNT Express, Mondelez, and Reckitt got dragged in too.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 What would have happened with microsegmentation
&lt;/h3&gt;

&lt;p&gt;A single-line equivalent policy, "never permit SMB 445/tcp between workstations," would have shut down the main lateral path (EternalBlue over SMB). The secondary spread after Mimikatz hijacked the DC could have been localized too, if admin paths were narrowed to "from jump host only." Forrester's post-mortem said it plainly: microsegmentation and fine-grained internal controls should be rolled out as part of a Zero Trust strategy.&lt;/p&gt;

&lt;p&gt;After that, alongside Zero Trust, microsegmentation became a board-level topic as "the precondition for lowering your ransomware insurance premium." Gartner forecasts that &lt;strong&gt;by 2026, 60% of organizations pursuing Zero Trust will use multiple forms of microsegmentation in combination&lt;/strong&gt; (the same figure was under 5% three years prior).&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The four implementation styles
&lt;/h2&gt;

&lt;p&gt;There are dozens of vendors, but architecturally they cluster into four families.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F06-four-styles.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F06-four-styles.png" alt="Four implementation styles" width="800" height="1025"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 A. Hypervisor (VMware NSX)
&lt;/h3&gt;

&lt;p&gt;The classic for VM-heavy shops. Every ESXi host has a &lt;strong&gt;Distributed Firewall (DFW)&lt;/strong&gt; baked into the hypervisor kernel, doing stateful filtering right at the VM's virtual NIC (vNIC).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F07-nsx-hypervisor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F07-nsx-hypervisor.png" alt="NSX hypervisor DFW" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What stands out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app does not change.&lt;/strong&gt; No agent inside the guest OS. The policy "moves with the VM" on vMotion (the session table travels too).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Near line-rate&lt;/strong&gt; (close to the physical NIC's theoretical throughput). Being a kernel module, the perf hit is minimal.&lt;/li&gt;
&lt;li&gt;The weakness is VMware lock-in, plus the fact that you need a different stack the moment physical servers, containers, or cloud VMs enter the picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 B. Agent (Illumio / Akamai Guardicore / Cisco Secure Workload)
&lt;/h3&gt;

&lt;p&gt;Drop a lightweight agent into every host OS, and have it drive the OS-native firewall (Windows Filtering Platform, Linux nftables/iptables, macOS pf).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F08-agent-style.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F08-agent-style.png" alt="Agent-based microsegmentation" width="800" height="1885"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Illumio collects telemetry from its agents (VEN: Virtual Enforcement Node), auto-builds an &lt;strong&gt;Application Dependency Map (ADM)&lt;/strong&gt;, and uses that as the canvas for writing policy. Akamai Guardicore ships its own light kernel module, plus differentiators like &lt;strong&gt;deception (decoys)&lt;/strong&gt; and an &lt;strong&gt;agentless mode powered by NVIDIA BlueField DPUs&lt;/strong&gt; (Data Processing Units that run segmentation on the server's NIC). Cisco Secure Workload (formerly Tetration, renamed in 2020) traces back to slurping flows out of data center switch ASICs and learning behavior with ML.&lt;/p&gt;

&lt;p&gt;What stands out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure-agnostic.&lt;/strong&gt; The same abstraction (labels/tags) works across on-prem, bare metal, cloud, and containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong visualization via ADM.&lt;/strong&gt; Real value shows up before policy: "let's see what is actually talking to what."&lt;/li&gt;
&lt;li&gt;The weakness is "you have to run an agent on every host." OS version compatibility, updates, and support windows for legacy OSes (Windows Server 2003 and friends) become operational pain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.3 C. Cloud-native (Security Group / NSG / NetworkPolicy)
&lt;/h3&gt;

&lt;p&gt;Take the mechanisms cloud providers and orchestrators already ship with, and lean on them hard for microsegmentation purposes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F09-cloud-native-sg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F09-cloud-native-sg.png" alt="Cloud-native Security Groups" width="800" height="824"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing about AWS Security Groups is that &lt;strong&gt;you can put an SG where an IP would normally go&lt;/strong&gt;. Writing &lt;code&gt;source=sg-web&lt;/code&gt; effectively gives you role-based policy. Azure NSG and GCP VPC Firewall Rules have the same trick. On the Kubernetes side, &lt;code&gt;NetworkPolicy&lt;/code&gt; plays this role: write allow rules with &lt;code&gt;podSelector&lt;/code&gt; and &lt;code&gt;namespaceSelector&lt;/code&gt;, and the policy tracks Pod IP changes for you.&lt;/p&gt;

&lt;p&gt;What stands out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Effectively free&lt;/strong&gt; to start. Cloud-standard, no extra agent.&lt;/li&gt;
&lt;li&gt;The weakness is central policy management and org-wide consistency. Once SGs hit five digits, humans cannot manage them by hand. You need a higher layer like AWS Firewall Manager, OPA, or Kyverno. Also, no L7 control (you cannot permit by HTTP method).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.4 D. Identity-driven (Cilium / Istio + SPIFFE)
&lt;/h3&gt;

&lt;p&gt;The newest family. &lt;strong&gt;The subject of policy is not an IP, it is a cryptographic ID.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F10-identity-driven.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F10-identity-driven.png" alt="Identity-driven with Cilium and SPIFFE" width="800" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Cilium, Pod IPs can change, but the &lt;strong&gt;Cilium Identity (a numeric value)&lt;/strong&gt; computed from labels stays stable. eBPF looks at the Identity attached to a packet to decide allow/deny, so "Cart Pod restarted and got a new IP" does not break policy.&lt;/p&gt;

&lt;p&gt;With Istio + SPIFFE, each sidecar (Envoy) receives an SVID. At mTLS time, the URI SAN is read and compared against &lt;code&gt;principals&lt;/code&gt; in an &lt;code&gt;AuthorizationPolicy&lt;/code&gt; (e.g. &lt;code&gt;cluster.local/ns/prod/sa/cart&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;What stands out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Doesn't fall apart when IPs are ephemeral.&lt;/strong&gt; Pod churn, autoscaling, serverless-style dynamic environments, all fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L7 control&lt;/strong&gt; is on the table (HTTP method/path, gRPC methods, Kafka topics).&lt;/li&gt;
&lt;li&gt;The weakness is the learning curve, and the need to run a CA / Identity layer underneath.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.5 Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Main PEP&lt;/th&gt;
&lt;th&gt;Subject&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hypervisor&lt;/td&gt;
&lt;td&gt;VMware NSX&lt;/td&gt;
&lt;td&gt;vNIC (DFW)&lt;/td&gt;
&lt;td&gt;VM tag&lt;/td&gt;
&lt;td&gt;Perf, follows VM moves&lt;/td&gt;
&lt;td&gt;VMware only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;Illumio, Akamai&lt;/td&gt;
&lt;td&gt;OS firewall&lt;/td&gt;
&lt;td&gt;Label&lt;/td&gt;
&lt;td&gt;Cross-environment, ADM&lt;/td&gt;
&lt;td&gt;Agent ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud-native&lt;/td&gt;
&lt;td&gt;AWS SG, K8s NetworkPolicy&lt;/td&gt;
&lt;td&gt;Cloud / CNI&lt;/td&gt;
&lt;td&gt;SG / label&lt;/td&gt;
&lt;td&gt;Cheap, standard&lt;/td&gt;
&lt;td&gt;No L7, weak central mgmt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity-driven&lt;/td&gt;
&lt;td&gt;Cilium, Istio&lt;/td&gt;
&lt;td&gt;eBPF / sidecar&lt;/td&gt;
&lt;td&gt;SPIFFE ID, label&lt;/td&gt;
&lt;td&gt;Great for dynamic env, L7&lt;/td&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Large orgs &lt;strong&gt;mix all of these&lt;/strong&gt;. SG at the AWS edge, Cilium inside the Kubernetes layer, NSX for data center VMs, Illumio agents on the legacy Windows servers. The "60% using multiple forms" in Gartner's 2026 forecast is exactly this.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. How large enterprises actually use it
&lt;/h2&gt;

&lt;p&gt;Spec sheets only get you so far. Five real-world cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Google: BeyondCorp (identity-driven across the whole company)
&lt;/h3&gt;

&lt;p&gt;In late 2009, Google got hit by a Chinese state-sponsored campaign known as &lt;strong&gt;Operation Aurora&lt;/strong&gt;. Initial intrusion was a &lt;strong&gt;spear-phishing email plus an Internet Explorer zero-day (CVE-2010-0249)&lt;/strong&gt;: clicking the malicious link planted the Hydraq trojan. From that beachhead, attackers moved laterally across Google's flat internal network and walked out with intellectual property, source code included. Google went public in January 2010.&lt;/p&gt;

&lt;p&gt;Google's answer was &lt;strong&gt;BeyondCorp&lt;/strong&gt;. They flipped the premise 180 degrees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop treating the internal LAN as a trusted zone. Throw out the VPN.&lt;/li&gt;
&lt;li&gt;Don't authorize by "what network are you on," authorize every request based on &lt;strong&gt;user ID + device ID + posture&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Microsegmentation is the core feature that stops malware moving from a user toward an app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F11-beyondcorp-sequence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fmicrosegmentation-deep-dive%2Fdiagrams%2F11-beyondcorp-sequence.png" alt="Google BeyondCorp access flow" width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key here: the AP (Access Proxy) is the &lt;strong&gt;PEP sitting in front of every app&lt;/strong&gt;, and apps are reachable only via the AP. Whether you're on the office LAN is irrelevant. This is identity-driven microsegmentation taken to its logical extreme.&lt;/p&gt;

&lt;p&gt;A commercial version is sold externally as &lt;strong&gt;BeyondCorp Enterprise&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Maersk: rebuilding after NotPetya
&lt;/h3&gt;

&lt;p&gt;Maersk's global ops were down for roughly 10 days after NotPetya. They &lt;strong&gt;rebuilt 45,000 PCs and 4,000 servers in about 10 days&lt;/strong&gt; (per Bleeping Computer reporting). Post-mortems (CSO Online, ComputerWeekly) point to SMB v1 and Windows domain admin paths running flat across the org, which let the malware spread sideways.&lt;/p&gt;

&lt;p&gt;Specific details of the post-incident security rebuild are limited in public reporting, but Maersk's then-CISO repeatedly said in conference talks and interviews that "the same attack still works against plenty of other companies." That pushed the Maersk case into the canonical reference for "this is when microsegmentation plus Zero Trust investment took off industry-wide."&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 A Global 250 bank: SWIFT compliance with Illumio
&lt;/h3&gt;

&lt;p&gt;From Illumio's official case study. A Global 250 bank needed to comply with &lt;strong&gt;SWIFT CSP (Customer Security Programme)&lt;/strong&gt;, which required closing every SWIFT-related server into a "per-host logical isolation zone."&lt;/p&gt;

&lt;p&gt;The traditional path (physical firewalls):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weeks of lead time per rule change.&lt;/li&gt;
&lt;li&gt;Mismatch with DevOps speed.&lt;/li&gt;
&lt;li&gt;Extra hardware cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How Illumio solved it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the agent in observation mode across every SWIFT-related host.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Application Dependency Map&lt;/strong&gt; to visualize real traffic.&lt;/li&gt;
&lt;li&gt;Generate policy from observed flows.&lt;/li&gt;
&lt;li&gt;Gradually flip to enforcement mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Illumio's framing of the bank's stance: "&lt;strong&gt;you cannot write policy for traffic you do not understand, so start from the ADM.&lt;/strong&gt;" The bank is now extending Illumio into SDLC (Software Development Life Cycle: dev, test, staging environments) too, using it to stand up isolated test environments for remote vendors.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 A major healthcare org: protecting 6,000 assets and medical IoT (Akamai Guardicore)
&lt;/h3&gt;

&lt;p&gt;From Akamai's case study. Over 6,000 assets, plus bedside monitors and medical IoT, all sharing a flat network. Lateral movement toward patient data and payment data was the headline risk.&lt;/p&gt;

&lt;p&gt;Why Guardicore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a software overlay, so no infrastructure changes were needed.&lt;/li&gt;
&lt;li&gt;The same policy language works across on-prem and AWS.&lt;/li&gt;
&lt;li&gt;Even devices that cannot host an agent (medical IoT, etc.) can be covered by flow observation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.5 A global manufacturer: SMB control across 2,000 workstations (Akamai Guardicore)
&lt;/h3&gt;

&lt;p&gt;Another Akamai case. A manufacturer with mixed "office + factory" sites worldwide, slicing up a flat workstation network in stages.&lt;/p&gt;

&lt;p&gt;The first phase rolled Guardicore out to 2,000 workstations. The IT security team's quote: "&lt;strong&gt;network visibility improved by 1,000%.&lt;/strong&gt;" They cut off &lt;strong&gt;pass-the-hash&lt;/strong&gt; (the classic move of replaying a stolen Windows password hash to authenticate remotely) and ransomware spreading workstation-to-workstation over SMB by making SMB between workstations default-deny.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Implementation pitfalls (where projects die on Day 1 / Day 2)
&lt;/h2&gt;

&lt;p&gt;Picking the right product does not save you. The places where projects get stuck are pretty consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 "Policy without visibility" always breaks something
&lt;/h3&gt;

&lt;p&gt;A lot of teams get cocky after a PoC and flip enforcement in production. The blast radius is huge, and the root cause is always the same: &lt;strong&gt;you put in default-deny without first understanding what is talking to what.&lt;/strong&gt; Without 2 to 4 weeks (months, for business systems) in observation mode collecting flows, then policy generation, then staged rollout, you will almost certainly take down a production system and watch the project get frozen.&lt;/p&gt;

&lt;p&gt;That is the same reason Illumio insists "&lt;strong&gt;the ADM is the protagonist; policy is derived from the ADM&lt;/strong&gt;," and the reason Cisco Secure Workload makes a song and dance about ML-based behavior learning. The rule is &lt;strong&gt;Discovery, then Modeling, then Enforcement&lt;/strong&gt;, in that order.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Label design decides your fate
&lt;/h3&gt;

&lt;p&gt;With both agent and identity-driven styles, policy is written as set algebra over labels. Dirty labels produce exponentially dirty policy.&lt;/p&gt;

&lt;p&gt;A practical axis set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;environment&lt;/strong&gt;: prod / staging / dev&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;application&lt;/strong&gt;: payments / cart / catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;role / tier&lt;/strong&gt;: web / app / db / cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;data classification&lt;/strong&gt;: pii / pci / public&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;region&lt;/strong&gt; (optional): us-east-1 / ap-northeast-1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lock in a plan to attach at least these four axes to every asset &lt;em&gt;before&lt;/em&gt; enforcement starts. Re-labeling later is, in practice, impossible.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 The last 10% is the hardest
&lt;/h3&gt;

&lt;p&gt;Discovery explains most of the traffic. The remaining 10%:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yearly batch jobs that have not run during the observation window.&lt;/li&gt;
&lt;li&gt;Legacy systems that were supposed to be retired but are still running.&lt;/li&gt;
&lt;li&gt;Traffic that only fires during a DR cutover.&lt;/li&gt;
&lt;li&gt;Stray scripts a developer set up on their own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deciding "the observation window never saw it, so deny" leads to the yearly batch tipping over a production system. The opposite, "allow everything just in case," makes the whole exercise pointless.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run in &lt;code&gt;alert-only&lt;/code&gt; mode for a while (let traffic through but log it).&lt;/li&gt;
&lt;li&gt;Stay in observation mode for one full business cycle (e.g. 13 months).&lt;/li&gt;
&lt;li&gt;Triage every unsanctioned flow that surfaces in that window.&lt;/li&gt;
&lt;li&gt;Then, finally, flip to enforcement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The organization and management who can accept that rhythm matter more than the technology choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Don't try to do everything at once
&lt;/h3&gt;

&lt;p&gt;A project that tries to segment the entire organization in one swing almost always fails. Elisity's and Gartner's guides list "&lt;strong&gt;tried to do everything at once and stalled&lt;/strong&gt;" as the most common failure mode.&lt;/p&gt;

&lt;p&gt;What actually works is roughly four phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1&lt;/strong&gt;: Crown jewels (SWIFT, payment DB, KMS, CA, etc.). Small blast radius, but very expensive if down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2&lt;/strong&gt;: Production app tiers (Web / App / DB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3&lt;/strong&gt;: Endpoint and office networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4&lt;/strong&gt;: OT and legacy systems (the hardest).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start with the Crown Jewels, where business impact is biggest and flows are most knowable, and expand outward. Each phase is 3 to 6 months. Be ready for 18 to 36 months total.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you only take three things away:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Microsegmentation is about stopping lateral movement.&lt;/strong&gt; Look at East-West (inside), not at the North-South boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move from IP-based to ID-based.&lt;/strong&gt; Stop extending VLAN/ACL; redesign around labels, SPIFFE IDs, IAM roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility before enforcement.&lt;/strong&gt; Reverse Discovery -&amp;gt; Modeling -&amp;gt; Enforcement and the project gets pickled.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>security</category>
      <category>zerotrust</category>
      <category>networking</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>AWS IAM Roles Anywhere Hands-On</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Tue, 26 May 2026 15:22:04 +0000</pubDate>
      <link>https://dev.to/kanywst/iam-roles-anywhere-hands-on-2fh8</link>
      <guid>https://dev.to/kanywst/iam-roles-anywhere-hands-on-2fh8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You'll always have cases where you want to hit AWS from a home laptop or an on-prem server.&lt;/p&gt;

&lt;p&gt;For a long time, the only way to give credentials to code running "outside AWS" was &lt;strong&gt;"stick an IAM User's long-lived key (&lt;code&gt;AKIA...&lt;/code&gt;) into an env var or config file."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This sucks for a bunch of reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the long-lived key leaks, attackers can use it forever&lt;/li&gt;
&lt;li&gt;Rotation is annoying enough that you end up using the same key for two years&lt;/li&gt;
&lt;li&gt;The "1 server = 1 IAM User" model breaks down (User limit, audit nightmare)&lt;/li&gt;
&lt;li&gt;CloudTrail can't tell you which physical machine made the call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IAM Roles Anywhere&lt;/strong&gt; is the 2022 feature that fixes this. &lt;strong&gt;By using an X.509 certificate as your identity to AWS&lt;/strong&gt;, you can pull temporary credentials without any IAM User or long-lived key in the picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F01-before-after.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F01-before-after.png" alt="Before and after: long-lived key vs X.509 cert" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article does that &lt;strong&gt;inside your Mac/Linux box&lt;/strong&gt;. &lt;strong&gt;No real CA involved. We make one self-signed Root CA and issue one end-entity certificate&lt;/strong&gt;. We register only the Root CA's public certificate with AWS.&lt;/p&gt;

&lt;p&gt;End state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your laptop has a &lt;strong&gt;certificate + private key&lt;/strong&gt; acting as your AWS identity&lt;/li&gt;
&lt;li&gt;Zero long-lived keys&lt;/li&gt;
&lt;li&gt;Temporary credentials (&lt;code&gt;ASIA...&lt;/code&gt;) come back from AWS&lt;/li&gt;
&lt;li&gt;You hit S3 with them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cost: &lt;strong&gt;$0&lt;/strong&gt;. Roles Anywhere itself has no extra charge (per AWS docs). Certificates are generated locally, and creating Trust Anchor / Profile / Role is free.&lt;/p&gt;

&lt;p&gt;Total time: about &lt;strong&gt;60 minutes&lt;/strong&gt;. Each command has a one-line explanation, so this works even if X.509 / PKI is new to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One AWS account&lt;/li&gt;
&lt;li&gt;AWS CLI (v2)&lt;/li&gt;
&lt;li&gt;A Mac or Linux terminal (&lt;code&gt;openssl&lt;/code&gt; is preinstalled)&lt;/li&gt;
&lt;li&gt;Windows users: run this inside WSL2&lt;/li&gt;
&lt;li&gt;Theory background is in &lt;a href="https://dev.to/kanywst/aws-iam-roles-anywhere-deep-dive"&gt;AWS IAM Roles Anywhere Deep Dive&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've never touched X.509 / PKI, you can still follow along. Term cheatsheet first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CA (Certificate Authority)&lt;/strong&gt;: the authority that issues certificates. Today, you are the CA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root CA&lt;/strong&gt;: the topmost CA. Its private key signs end-entity certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-entity certificate&lt;/strong&gt;: the actual certificate you use. Signed by the Root CA's private key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CN (Common Name)&lt;/strong&gt;: the "name" field of the certificate. Roles Anywhere Trust Policy can match on it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you know "sign with private key, verify with public key," you have enough background.&lt;/p&gt;




&lt;h2&gt;
  
  
  The whole flow
&lt;/h2&gt;

&lt;p&gt;Plan in one diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F02-whole-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F02-whole-flow.png" alt="End-to-end flow: from setup through cleanup" width="800" height="2731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build the PKI material by hand (Steps 1-2), connect it to AWS (Steps 3-5), then make actual calls (Steps 6-8).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 0: Environment setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  0-1. Check the tools
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl version
&lt;span class="c"&gt;# OpenSSL 3.x.x (preinstalled on Mac/Linux)&lt;/span&gt;

aws &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# aws-cli/2.x.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS: &lt;code&gt;brew install awscli&lt;/code&gt; (OpenSSL ships with the OS)&lt;/li&gt;
&lt;li&gt;Linux: install via your package manager&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  0-2. Create a working directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/roles-anywhere-handson
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/roles-anywhere-handson
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every command from here runs inside this directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  0-3. Put Account ID in an env var
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; Account &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Create a self-signed Root CA
&lt;/h2&gt;

&lt;p&gt;You're the CA. We won't use a real one (DigiCert, Let's Encrypt, etc.). We'll generate a self-signed Root CA with &lt;code&gt;openssl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F03-root-ca-steps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F03-root-ca-steps.png" alt="Root CA creation: key then self-signed cert" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1-1. CA private key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; ca.key 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;ca.key&lt;/code&gt;. &lt;strong&gt;Never let this file leave your box&lt;/strong&gt;. It is literally the CA's body.&lt;/p&gt;

&lt;h3&gt;
  
  
  1-2. CA self-signed certificate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; ca.key &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=handson-root-ca/O=Hands-On"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-addext&lt;/span&gt; &lt;span class="s2"&gt;"basicConstraints=critical,CA:TRUE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-addext&lt;/span&gt; &lt;span class="s2"&gt;"keyUsage=critical,keyCertSign,cRLSign"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gotcha: &lt;strong&gt;&lt;code&gt;basicConstraints=CA:TRUE&lt;/code&gt; is required&lt;/strong&gt;. Without it, registering this cert as a Trust Anchor fails with &lt;strong&gt;&lt;code&gt;Incorrect basic constraints for CA certificate&lt;/code&gt;&lt;/strong&gt;. &lt;code&gt;keyUsage&lt;/code&gt; similarly needs CA values (&lt;code&gt;keyCertSign&lt;/code&gt; + &lt;code&gt;cRLSign&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ca.crt&lt;/code&gt; is your public certificate. This is what you upload to AWS. Inspect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; ca.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Subject: CN = handson-root-ca, O = Hands-On&lt;/code&gt; is the name you set&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Issuer:&lt;/code&gt; is the same value (you signed yourself, so Subject = Issuer, which is what self-signed means)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Not Before / Not After:&lt;/code&gt; give you a 365-day validity from today&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's your Root CA.&lt;/p&gt;

&lt;h3&gt;
  
  
  1-3. Why self-signed is fine here
&lt;/h3&gt;

&lt;p&gt;For a browser TLS cert, "self-signed = don't trust" is the rule. But Roles Anywhere works on a model where &lt;strong&gt;AWS only trusts the Root CAs you registered&lt;/strong&gt; (that's what a Trust Anchor is). Registering only your self-signed Root CA with AWS closes off a private trust loop. Only certificates derived from your CA work with AWS.&lt;/p&gt;

&lt;p&gt;In production you'd use an internal PKI or AWS Private CA. For learning purposes, self-signed is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Issue an end-entity certificate
&lt;/h2&gt;

&lt;p&gt;Now that you have a CA, use it to issue the end-entity certificate (the actual one you'll use).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F04-end-entity-steps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F04-end-entity-steps.png" alt="End-entity cert issuance: key, CSR, then signed cert" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2-1. End-entity private key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; client.key 2048
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;client.key&lt;/code&gt; is the &lt;strong&gt;private key that lives on the machine making Roles Anywhere calls (your laptop today)&lt;/strong&gt;. This must also never leave the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  2-2. Generate a CSR (Certificate Signing Request)
&lt;/h3&gt;

&lt;p&gt;A CSR is the paper that says "I am this name, please sign me" sent to the CA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; client.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=handson-client-01/O=Hands-On"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CN is &lt;code&gt;handson-client-01&lt;/code&gt;. The Roles Anywhere Trust Policy can pin to "only certs with this CN," so this value matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  2-3. Sign with the CA
&lt;/h3&gt;

&lt;p&gt;The end-entity certificate needs two extensions Roles Anywhere requires. Pass them through an extension file using &lt;code&gt;openssl x509 -extfile&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; client.ext &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; client.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CA&lt;/span&gt; ca.crt &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca.key &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client.crt &lt;span class="nt"&gt;-days&lt;/span&gt; 90 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-extfile&lt;/span&gt; client.ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gotcha: without &lt;code&gt;keyUsage = digitalSignature&lt;/code&gt; and &lt;code&gt;extendedKeyUsage = clientAuth&lt;/code&gt;, the Roles Anywhere CreateSession call fails with &lt;strong&gt;&lt;code&gt;Untrusted certificate. Insufficient certificate&lt;/code&gt;&lt;/strong&gt;. Both are required.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;client.crt&lt;/code&gt; is now ready. 90-day validity.&lt;/p&gt;

&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; client.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Subject: CN = handson-client-01&lt;/code&gt; is the end-entity's name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Issuer: CN = handson-root-ca&lt;/code&gt; proves the Root CA signed it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PKI material is now ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ca.key    : Root CA private key (secret)
ca.crt    : Root CA public certificate (upload to AWS)
client.key: End-entity private key (lives on the client machine)
client.crt: End-entity public certificate (lives on the client machine)
client.csr: CSR (no longer needed, you can delete)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Register the Trust Anchor with AWS
&lt;/h2&gt;

&lt;p&gt;Tell Roles Anywhere &lt;strong&gt;"I trust this Root CA"&lt;/strong&gt;. That's a &lt;strong&gt;Trust Anchor&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3-1. Create one from the AWS Console
&lt;/h3&gt;

&lt;p&gt;AWS Console → IAM → &lt;strong&gt;Roles Anywhere&lt;/strong&gt; (bottom of the left nav) → &lt;strong&gt;Manage&lt;/strong&gt; → &lt;strong&gt;Create a trust anchor&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trust anchor name&lt;/strong&gt;: &lt;code&gt;handson-trust-anchor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: pick &lt;strong&gt;External certificate bundle&lt;/strong&gt; (use the other option if you're using AWS Private CA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate bundle&lt;/strong&gt;: paste the contents of &lt;code&gt;ca.crt&lt;/code&gt;. Print it with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;ca.crt
&lt;span class="c"&gt;# Copy everything from -----BEGIN CERTIFICATE----- to -----END CERTIFICATE-----&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Create trust anchor&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3-2. Or do it via CLI (optional)
&lt;/h3&gt;

&lt;p&gt;If you hate the GUI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CA_CERT_BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;ca.crt&lt;span class="si"&gt;)&lt;/span&gt;

aws rolesanywhere create-trust-anchor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; handson-trust-anchor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="s2"&gt;"sourceType=CERTIFICATE_BUNDLE,sourceData={x509CertificateData=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;ca.crt | jq &lt;span class="nt"&gt;-Rs&lt;/span&gt; .&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enabled&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell escaping is gnarly. Doing it from the Console is easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  3-3. Save the Trust Anchor ARN
&lt;/h3&gt;

&lt;p&gt;Copy the ARN shown on the post-creation page into an env var.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:rolesanywhere:us-east-1:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:trust-anchor/abc123..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Create the IAM Role
&lt;/h2&gt;

&lt;p&gt;This is the Role that Roles Anywhere will Assume on your behalf. &lt;strong&gt;In the Trust Policy, set &lt;code&gt;rolesanywhere.amazonaws.com&lt;/code&gt; as the Principal&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4-1. Trust Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; trust-policy.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "rolesanywhere.amazonaws.com" },
    "Action": [
      "sts:AssumeRole",
      "sts:TagSession",
      "sts:SetSourceIdentity"
    ],
    "Condition": {
      "ArnEquals": {
        "aws:SourceArn": "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"
      }
    }
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Principal: { "Service": "rolesanywhere.amazonaws.com" }&lt;/code&gt;&lt;/strong&gt;: not an IAM User or Role. The Principal is the Roles Anywhere AWS service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Action: sts:AssumeRole + sts:TagSession + sts:SetSourceIdentity&lt;/code&gt;&lt;/strong&gt;: Roles Anywhere doesn't just AssumeRole. It also injects Session Tags and Source Identity derived from the cert&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Condition: aws:SourceArn = Trust Anchor ARN&lt;/code&gt;&lt;/strong&gt;: lock down the Assume to only happen through this specific Trust Anchor (Confused Deputy mitigation)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4-2. Create the Role and attach an Identity Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; handson-rolesanywhere-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; file://trust-policy.json

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; identity-policy.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:ListAllMyBuckets"],
    "Resource": "*"
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;aws iam put-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; handson-rolesanywhere-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-name&lt;/span&gt; s3-list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://identity-policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Identity Policy is minimal. &lt;code&gt;s3:ListAllMyBuckets&lt;/code&gt; only. Anything that proves "the credentials work" is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  4-3. Save the Role ARN
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws iam get-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; handson-rolesanywhere-role &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Role.Arn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ROLE_ARN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Create the Roles Anywhere Profile
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Profile&lt;/strong&gt; is the link between Trust Anchor and Role. It says "callers coming through this Trust Anchor can Assume this Role."&lt;/p&gt;

&lt;h3&gt;
  
  
  5-1. Create from the Console
&lt;/h3&gt;

&lt;p&gt;IAM → Roles Anywhere → &lt;strong&gt;Profiles&lt;/strong&gt; → &lt;strong&gt;Create a profile&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile name&lt;/strong&gt;: &lt;code&gt;handson-profile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles&lt;/strong&gt;: select &lt;code&gt;handson-rolesanywhere-role&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session policy&lt;/strong&gt; (optional): you can cap permissions here, leave it empty for now&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create profile&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5-2. Save the Profile ARN
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PROFILE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:rolesanywhere:us-east-1:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:profile/def456..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AWS side is done.&lt;/p&gt;

&lt;h3&gt;
  
  
  5-3. The trust structure you just built
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F05-trust-structure.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-roles-anywhere-hands-on%2Fdiagrams%2F05-trust-structure.png" alt="Trust structure from cert to temporary credentials" width="800" height="1614"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Install aws_signing_helper
&lt;/h2&gt;

&lt;p&gt;The Roles Anywhere API doesn't use plain SigV4. It requires &lt;strong&gt;certificate-based signing&lt;/strong&gt;. Writing this by hand every time is painful, so AWS ships an official helper binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  6-1. Download
&lt;/h3&gt;

&lt;p&gt;As of May 2026, the latest version is &lt;code&gt;1.8.3&lt;/code&gt;. URLs are per platform.&lt;/p&gt;

&lt;p&gt;macOS (Intel):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://rolesanywhere.amazonaws.com/releases/1.8.3/X86_64/MacOS/Sonoma/aws_signing_helper
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x aws_signing_helper
./aws_signing_helper version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS (Apple Silicon / M1, M2, M3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://rolesanywhere.amazonaws.com/releases/1.8.3/Aarch64/MacOS/Sonoma/aws_signing_helper
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x aws_signing_helper
./aws_signing_helper version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux (x86_64):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://rolesanywhere.amazonaws.com/releases/1.8.3/X86_64/Linux/Amzn2023/aws_signing_helper
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x aws_signing_helper
./aws_signing_helper version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux (ARM64 / Raspberry Pi etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://rolesanywhere.amazonaws.com/releases/1.8.3/Aarch64/Linux/Amzn2023/aws_signing_helper
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x aws_signing_helper
./aws_signing_helper version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check for newer releases on &lt;a href="https://github.com/aws/rolesanywhere-credential-helper/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;. Just swap the version (&lt;code&gt;1.8.3&lt;/code&gt;) in the URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  6-2. What the helper does
&lt;/h3&gt;

&lt;p&gt;The helper takes three things (Trust Anchor ARN, Profile ARN, Role ARN) as arguments, sends an HTTPS request signed with &lt;code&gt;client.crt&lt;/code&gt; + &lt;code&gt;client.key&lt;/code&gt; to Roles Anywhere, and returns &lt;code&gt;ASIA...&lt;/code&gt; temporary credentials.&lt;/p&gt;

&lt;p&gt;Common mix-up: &lt;strong&gt;Roles Anywhere is not mTLS&lt;/strong&gt;. The TLS handshake is server-only. The client cert is never exchanged at the TLS layer (AWS does not send a &lt;code&gt;CertificateRequest&lt;/code&gt;). Authentication happens at the &lt;strong&gt;application layer&lt;/strong&gt;. The scheme extends SigV4 and is called &lt;code&gt;AWS4-X509-RSA-SHA256&lt;/code&gt; (for RSA keys) or &lt;code&gt;AWS4-X509-ECDSA-SHA256&lt;/code&gt; (for EC keys).&lt;/p&gt;

&lt;p&gt;The request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://rolesanywhere.&amp;lt;region&amp;gt;.amazonaws.com/sessions
Authorization: AWS4-X509-RSA-SHA256 Credential=..., Signature=...
X-Amz-X509: &amp;lt;base64 of client.crt&amp;gt;
Body: &amp;lt;JSON for CreateSession&amp;gt;
   ↑ Signature is the RSA signature over the Canonical Request, signed with client.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain SigV4 signs with "HMAC using Secret Access Key." Roles Anywhere signs with "RSA using private key, with the certificate carried in &lt;code&gt;X-Amz-X509&lt;/code&gt;." Think of it as SigV4 where the symmetric HMAC has been swapped for asymmetric RSA-Sign.&lt;/p&gt;

&lt;h3&gt;
  
  
  6-3. AWS verifies in two stages
&lt;/h3&gt;

&lt;p&gt;"Does the Trust Anchor verify the body signature?" is a natural question. The answer is no. Verification splits in two.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chain validation (this is where the Trust Anchor matters)&lt;/strong&gt;: is the &lt;code&gt;client.crt&lt;/code&gt; in &lt;code&gt;X-Amz-X509&lt;/code&gt; derived from the Root CA registered as Trust Anchor? Are validity, KeyUsage, and EKU sane? If a CRL is configured, is the cert non-revoked? After this passes, AWS treats &lt;code&gt;client.crt&lt;/code&gt; as trusted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature verification&lt;/strong&gt;: extract the public key from the trusted &lt;code&gt;client.crt&lt;/code&gt;, and verify the RSA signature on the request body with it. After this passes, AWS knows the holder of &lt;code&gt;client.key&lt;/code&gt; sent the request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So &lt;strong&gt;the public key in the Trust Anchor's &lt;code&gt;ca.crt&lt;/code&gt; does not directly verify the request signature&lt;/strong&gt;. The Trust Anchor decides "do we trust &lt;code&gt;client.crt&lt;/code&gt;?" and the actual request signature is verified by the public key inside &lt;code&gt;client.crt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After both pass, the Roles Anywhere service internally performs the equivalent of &lt;code&gt;sts:AssumeRole&lt;/code&gt; and returns &lt;code&gt;ASIA...&lt;/code&gt; (that's why the Trust Policy's Principal is &lt;code&gt;rolesanywhere.amazonaws.com&lt;/code&gt;). &lt;strong&gt;Clients never hit STS directly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The payoff: AWS never holds a copy of your private key. A regular long-lived key is a shared secret because Secret Access Key exists on both AWS and your side. Roles Anywhere is &lt;strong&gt;asymmetric&lt;/strong&gt;, so AWS only ever sees the public key (= certificate). The only thing whose leak hurts you is your own &lt;code&gt;client.key&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Pull temporary credentials and call AWS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7-1. Get credentials via credential-process
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./aws_signing_helper credential-process &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/roles-anywhere-handson/client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-key&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/roles-anywhere-handson/client.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trust-anchor-arn&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile-arn&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROFILE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, you get JSON back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AccessKeyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASIA..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SecretAccessKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SessionToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...(long)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Expiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-24T13:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Temporary credentials starting with &lt;code&gt;ASIA...&lt;/code&gt;&lt;/strong&gt; are in your hands. Zero long-lived keys involved. Cert + private key alone got AWS to hand back temporary credentials. That's the core of Roles Anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  7-2. Wire it into the AWS CLI
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;aws_signing_helper credential-process&lt;/code&gt; by hand every time is tedious. Drop a &lt;code&gt;credential_process&lt;/code&gt; config into &lt;code&gt;~/.aws/config&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.aws/config &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;

[profile rolesanywhere-handson]
region = &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
credential_process = &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;/roles-anywhere-handson/aws_signing_helper credential-process --certificate &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;/roles-anywhere-handson/client.crt --private-key &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;/roles-anywhere-handson/client.key --trust-anchor-arn &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; --profile-arn &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROFILE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; --role-arn &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the &lt;code&gt;aws&lt;/code&gt; CLI invokes the helper automatically to fetch credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; rolesanywhere-handson
&lt;span class="c"&gt;# bucket list shows up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;aws sts get-caller-identity --profile rolesanywhere-handson&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"UserId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AROA...:2a9902119721ab7b5c19a878c4c66af705895b42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sts::.../assumed-role/handson-rolesanywhere-role/2a9902119721ab7b5c19a878c4c66af705895b42"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The long hex string at the tail of &lt;code&gt;UserId&lt;/code&gt; and &lt;code&gt;Arn&lt;/code&gt; is the end-entity certificate's serial number&lt;/strong&gt;. Not the CN (this trips people up). Roles Anywhere uses the &lt;strong&gt;certificate serial number (big-endian hex, lowercase)&lt;/strong&gt; as the session name automatically. To confirm, compare with the lowercased output of &lt;code&gt;openssl x509 -in client.crt -noout -serial&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The CN itself lands in &lt;code&gt;sourceIdentity&lt;/code&gt; (visible in CloudTrail in the next section). Remember it as &lt;strong&gt;"session name = serial, sourceIdentity = CN"&lt;/strong&gt; on two axes.&lt;/p&gt;

&lt;h3&gt;
  
  
  7-3. Verify in CloudTrail
&lt;/h3&gt;

&lt;p&gt;Console → CloudTrail → Event history → Lookup attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event name&lt;/strong&gt;: &lt;code&gt;AssumeRole&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Roles Anywhere AssumeRole event shows up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rolesanywhere.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWSService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invokedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rolesanywhere.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"profileArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"roleArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"trustAnchorArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...(contents of end-entity cert)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseElements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"credentialSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN=handson-client-01"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;sourceIdentity&lt;/code&gt; automatically carries the certificate's &lt;code&gt;CN&lt;/code&gt;&lt;/strong&gt;. Even without ExternalId or explicit Source Identity config, Roles Anywhere stamps the CN as the audit trail for "who called this."&lt;/p&gt;

&lt;p&gt;Downstream CloudTrail events (the S3 ls call, etc.) inherit &lt;code&gt;sessionContext.sourceIdentity = CN=handson-client-01&lt;/code&gt;. In production, aggregating on this gives you "which physical server (= which certificate) made the call" in one query.&lt;/p&gt;

&lt;p&gt;Other Subject fields (&lt;code&gt;O&lt;/code&gt;, &lt;code&gt;OU&lt;/code&gt;, etc.) don't land in &lt;code&gt;sourceIdentity&lt;/code&gt;. They land in &lt;strong&gt;&lt;code&gt;aws:PrincipalTag/x509Subject/&amp;lt;attribute&amp;gt;&lt;/code&gt;&lt;/strong&gt; injected into the Session (used in Step 8).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Restrict Trust Policy by CN
&lt;/h2&gt;

&lt;p&gt;Right now, &lt;strong&gt;any end-entity certificate derived from the Root CA&lt;/strong&gt; can Assume the Role. In production you usually want "only certificates with this specific CN."&lt;/p&gt;

&lt;h3&gt;
  
  
  8-1. Add a CN match condition
&lt;/h3&gt;

&lt;p&gt;Add an &lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt; condition to the Trust Policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; trust-policy.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "rolesanywhere.amazonaws.com" },
    "Action": [
      "sts:AssumeRole",
      "sts:TagSession",
      "sts:SetSourceIdentity"
    ],
    "Condition": {
      "ArnEquals": {
        "aws:SourceArn": "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"
      },
      "StringEquals": {
        "aws:PrincipalTag/x509Subject/CN": "handson-client-01"
      }
    }
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;aws iam update-assume-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; handson-rolesanywhere-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://trust-policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Roles Anywhere parses the cert, it &lt;strong&gt;auto-injects each Subject attribute as a Session Tag&lt;/strong&gt;. You can use &lt;code&gt;x509Subject/CN&lt;/code&gt;, &lt;code&gt;x509Subject/O&lt;/code&gt;, &lt;code&gt;x509Issuer/CN&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  8-2. Verify behavior
&lt;/h3&gt;

&lt;p&gt;Calling with the cert whose CN is &lt;code&gt;handson-client-01&lt;/code&gt; works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--profile&lt;/span&gt; rolesanywhere-handson
&lt;span class="c"&gt;# success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling with a cert that has a different CN (build &lt;code&gt;client2.key&lt;/code&gt; / &lt;code&gt;client2.crt&lt;/code&gt; and swap them in) returns &lt;strong&gt;AccessDenied&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For comparison, build a cert with a different CN&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; client2.key 2048
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; client2.key &lt;span class="nt"&gt;-out&lt;/span&gt; client2.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=other-client/O=Hands-On"&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; client2.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; ca.crt &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca.key &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; client2.crt &lt;span class="nt"&gt;-days&lt;/span&gt; 90 &lt;span class="nt"&gt;-sha256&lt;/span&gt;

./aws_signing_helper credential-process &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/roles-anywhere-handson/client2.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-key&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/roles-anywhere-handson/client2.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trust-anchor-arn&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile-arn&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROFILE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# AccessDenied: cert passes the Trust Anchor (same CA derived) but fails&lt;/span&gt;
&lt;span class="c"&gt;# the Role Trust Policy condition (CN=handson-client-01)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;strong&gt;"trust one CA, split Roles per CN"&lt;/strong&gt;. Manage 100 servers with one CA but keep one Role per CN. That's how this scales in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 9: Cleanup
&lt;/h2&gt;

&lt;p&gt;Delete things at the end. Order matters because of dependencies (Profile → Trust Anchor → Role).&lt;/p&gt;

&lt;h3&gt;
  
  
  9-1. Delete AWS resources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Delete Profile (from Console or via CLI)&lt;/span&gt;
&lt;span class="nv"&gt;PROFILE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROFILE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;/ &lt;span class="s1"&gt;'{print $NF}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
aws rolesanywhere delete-profile &lt;span class="nt"&gt;--profile-id&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROFILE_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Delete Trust Anchor&lt;/span&gt;
&lt;span class="nv"&gt;TA_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;/ &lt;span class="s1"&gt;'{print $NF}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
aws rolesanywhere delete-trust-anchor &lt;span class="nt"&gt;--trust-anchor-id&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TA_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Detach Role policy, then delete the Role&lt;/span&gt;
aws iam delete-role-policy &lt;span class="nt"&gt;--role-name&lt;/span&gt; handson-rolesanywhere-role &lt;span class="nt"&gt;--policy-name&lt;/span&gt; s3-list
aws iam delete-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; handson-rolesanywhere-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  9-2. Delete local certificates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/roles-anywhere-handson
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  9-3. Edit ~/.aws/config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Open in an editor and remove the [profile rolesanywhere-handson] section&lt;/span&gt;
vim ~/.aws/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is gone. Zero bill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Built the full path for calling AWS from "outside AWS" without long-lived keys, by hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build one self-signed Root CA&lt;/strong&gt; (2 openssl commands)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue an end-entity certificate&lt;/strong&gt; (3 openssl commands)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register the CA cert as a Trust Anchor with AWS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In the IAM Role's Trust Policy, trust &lt;code&gt;rolesanywhere.amazonaws.com&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bind Trust Anchor and Role via a Profile&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drop &lt;code&gt;aws_signing_helper&lt;/code&gt; locally and wire it into &lt;code&gt;credential_process&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-CN role split inside one CA via &lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt; in the Trust Policy&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;End state: zero long-lived keys (&lt;code&gt;AKIA...&lt;/code&gt;) on your laptop. &lt;code&gt;ASIA...&lt;/code&gt; temporary credentials come out of AWS. Revoke the certificate and access dies immediately. CloudTrail automatically stamps the CN into &lt;code&gt;sourceIdentity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What changes when this scales to production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Switch the Root CA to AWS Private CA or your internal PKI&lt;/strong&gt;: with a self-signed CA, the CA private key's safety is now the whole trust loop's safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate cert distribution&lt;/strong&gt;: every new CN needs a freshly issued end-entity cert. Use a pipeline (Step CA, Smallstep, HashiCorp Vault PKI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operate a CRL&lt;/strong&gt;: leaked certs need to die instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roles Anywhere itself is a small API + official helper, but &lt;strong&gt;the operational hard part is PKI lifecycle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What you might explore next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Combine EKS Service Accounts with Roles Anywhere as an IRSA-free path&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect GitHub self-hosted runners to AWS via Roles Anywhere&lt;/strong&gt; (GitHub-hosted runners are better with OIDC)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotate end-entity certs every 24 hours using Smallstep&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/introduction.html" rel="noopener noreferrer"&gt;IAM Roles Anywhere User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/credential-helper.html" rel="noopener noreferrer"&gt;aws_signing_helper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/trust-model.html" rel="noopener noreferrer"&gt;Conditions on &lt;code&gt;aws:PrincipalTag/x509Subject/CN&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kanywst/aws-iam-deep-dive-2b81"&gt;AWS IAM Deep Dive (previous post)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kanywst/aws-sts-deep-dive"&gt;AWS STS Deep Dive (theory)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kanywst/aws-iam-roles-anywhere-deep-dive"&gt;IAM Roles Anywhere Deep Dive (theory)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>iam</category>
      <category>sts</category>
      <category>security</category>
    </item>
    <item>
      <title>AWS STS Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Sun, 24 May 2026 11:34:59 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-sts-deep-dive-19ha</link>
      <guid>https://dev.to/kanywst/aws-sts-deep-dive-19ha</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/kanywst/aws-iam-deep-dive-2b81"&gt;the previous IAM article&lt;/a&gt;, I argued you should drop long-lived IAM User keys (&lt;code&gt;AKIA...&lt;/code&gt;) and lean on Role + STS. Back then I described STS as "the temporary credential issuer" in one line.&lt;/p&gt;

&lt;p&gt;Then I kept tripping over AssumeRole edge cases in real work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is the actual difference between &lt;code&gt;AssumeRole&lt;/code&gt;, &lt;code&gt;AssumeRoleWithSAML&lt;/code&gt;, and &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;?"&lt;/li&gt;
&lt;li&gt;"A SaaS vendor told me to pass &lt;code&gt;ExternalId&lt;/code&gt;. What is it for?"&lt;/li&gt;
&lt;li&gt;"CloudTrail keeps showing &lt;code&gt;assumed-role/XXX/session-yyy&lt;/code&gt;. Where does the &lt;code&gt;session-yyy&lt;/code&gt; part come from?"&lt;/li&gt;
&lt;li&gt;"I chained Role A to Role B and suddenly the session expires after 1 hour. Why?"&lt;/li&gt;
&lt;li&gt;"Where does &lt;code&gt;AssumeRoot&lt;/code&gt; from 2024 re:Invent fit?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;STS is not a single API. It is a small universe: &lt;strong&gt;6 separate issuance APIs plus Source Identity, External ID, Session Tag, and Session Policy&lt;/strong&gt;. This article opens all of it. Treat it as the sequel to the IAM piece.&lt;/p&gt;

&lt;p&gt;Outline.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What STS actually does (including Global vs Regional)&lt;/li&gt;
&lt;li&gt;The 3 fields of a temporary credential and how they ride on SigV4&lt;/li&gt;
&lt;li&gt;The 6 STS APIs as a decision tree&lt;/li&gt;
&lt;li&gt;Trust Policy vs Identity Policy (one more careful pass)&lt;/li&gt;
&lt;li&gt;External ID: stopping the Confused Deputy&lt;/li&gt;
&lt;li&gt;Source Identity: keeping the original human in CloudTrail&lt;/li&gt;
&lt;li&gt;Session Tag and Transitive Tag: the engine behind ABAC&lt;/li&gt;
&lt;li&gt;Session Policy: narrowing through AssumeRole arguments&lt;/li&gt;
&lt;li&gt;Role Chaining: the 1-hour wall&lt;/li&gt;
&lt;li&gt;DurationSeconds: 15 minutes to 12 hours&lt;/li&gt;
&lt;li&gt;Wiring GitHub Actions OIDC (a quick look)&lt;/li&gt;
&lt;li&gt;How it shows up in CloudTrail&lt;/li&gt;
&lt;li&gt;AssumeRoot (2024): Centralized Root Access&lt;/li&gt;
&lt;li&gt;Do this / avoid that&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Foundations (IAM principals, policy evaluation order, SigV4) are covered in the previous article. Read that first if you skipped it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. What STS Does
&lt;/h2&gt;

&lt;p&gt;STS (Security Token Service) is &lt;strong&gt;the AWS service that issues temporary credentials&lt;/strong&gt;. One hostname, 8 APIs (6 for issuance, 2 helpers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Global and Regional both exist
&lt;/h3&gt;

&lt;p&gt;The STS endpoint comes in 2 shapes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;hostname&lt;/th&gt;
&lt;th&gt;Physical location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sts.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hosted only in us-east-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sts.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt; (e.g. &lt;code&gt;sts.ap-northeast-1.amazonaws.com&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Independently hosted in each Region&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;IAM itself (Users, Roles, Policy definitions) is Global, but &lt;strong&gt;the issuance act is regionalized&lt;/strong&gt;. This is the central design point. The current official line: &lt;strong&gt;use Regional&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: Hitting a nearby Region endpoint is obviously faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt;: The Global endpoint is hosted only in us-east-1, so it goes down with us-east-1. Regional endpoints are independent per Region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token validity scope&lt;/strong&gt;: SessionTokens issued from a Regional endpoint are &lt;strong&gt;valid in every Region&lt;/strong&gt;. Tokens issued from the Global endpoint only work in &lt;strong&gt;default-enabled Regions (not Opt-in Regions)&lt;/strong&gt;. To use a newer Opt-in Region, you must issue from a Regional endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Around July 31, 2025, the SDK defaults for boto3 v1.40.0, PHP, C++, .NET, and Tools for PowerShell flipped to Regional (Global used to be the default). Go, Node, and Java were already Regional by default. Whatever SDK you are using without thinking about it is basically already Regional.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F01-sts-endpoints.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F01-sts-endpoints.png%3Fv%3D2" alt="STS endpoints: Global vs Regional" width="759" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the CLI, set &lt;code&gt;AWS_STS_REGIONAL_ENDPOINTS=regional&lt;/code&gt; (or &lt;code&gt;sts_regional_endpoints = regional&lt;/code&gt; in &lt;code&gt;~/.aws/config&lt;/code&gt;) to make the choice explicit. New projects should standardize on Regional without thinking.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The 3 fields of a temporary credential
&lt;/h2&gt;

&lt;p&gt;What STS hands you is &lt;strong&gt;not a single "token" string&lt;/strong&gt;. It is a triple.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AccessKeyId&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 chars starting with &lt;code&gt;ASIA&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Safe to expose (it rides on every request)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SecretAccessKey&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40-char base64&lt;/td&gt;
&lt;td&gt;Leak = compromise. HMAC key for SigV4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SessionToken&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hundreds to thousands of characters&lt;/td&gt;
&lt;td&gt;Leak = compromise. Without it the temporary credential is not accepted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Expiration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ISO8601 UTC timestamp&lt;/td&gt;
&lt;td&gt;One second past this and you re-fetch from STS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;IAM User long-lived keys start with &lt;code&gt;AKIA...&lt;/code&gt;. STS temporary credentials start with &lt;code&gt;ASIA...&lt;/code&gt;. &lt;strong&gt;The first 4 characters tell you long-lived vs temporary&lt;/strong&gt;. Remember this and your first triage on CloudTrail or &lt;code&gt;~/.aws/credentials&lt;/code&gt; gets fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  SessionToken rides as an extra SigV4 header
&lt;/h3&gt;

&lt;p&gt;With long-lived keys, SigV4 uses &lt;code&gt;AccessKeyId&lt;/code&gt; and &lt;code&gt;SecretAccessKey&lt;/code&gt;. With STS temporary credentials, you have to &lt;strong&gt;add one more HTTP header&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamodb.ap-northeast-1.amazonaws.com&lt;/span&gt;
&lt;span class="na"&gt;X-Amz-Date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20260517T120000Z&lt;/span&gt;
&lt;span class="na"&gt;X-Amz-Security-Token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IQoJb3JpZ2luX2VjEM3...(continues for hundreds of chars)&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS4-HMAC-SHA256&lt;/span&gt;
&lt;span class="s"&gt;  Credential=ASIAEXAMPLE/20260517/ap-northeast-1/dynamodb/aws4_request,&lt;/span&gt;
&lt;span class="s"&gt;  SignedHeaders=host;x-amz-date;x-amz-security-token,&lt;/span&gt;
&lt;span class="s"&gt;  Signature=abc123...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put SessionToken in the &lt;code&gt;X-Amz-Security-Token&lt;/code&gt; header&lt;/strong&gt;. AWS uses this to decide "this is not a long-lived IAM User key, it is an STS-issued temporary credential" and looks up validity and policy accordingly.&lt;/li&gt;
&lt;li&gt;Include &lt;code&gt;x-amz-security-token&lt;/code&gt; in &lt;code&gt;SignedHeaders&lt;/code&gt; so it is covered by the signature (tamper resistance).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SDKs handle all of this automatically, so you rarely write it by hand. But when "it works from a container via the SDK but &lt;code&gt;curl&lt;/code&gt; returns 403," this header is almost always the cause.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. STS APIs: a decision tree
&lt;/h2&gt;

&lt;p&gt;STS has multiple similarly named APIs and people mix them up every time. Picture first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F02-sts-api-decision-tree.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F02-sts-api-decision-tree.png%3Fv%3D2" alt="STS API decision tree" width="720" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One line per API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;DurationSeconds max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AssumeRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switch into an IAM Role in the same or another account&lt;/td&gt;
&lt;td&gt;Role's MaxSessionDuration (15 min to 12 h)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AssumeRoleWithSAML&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass a SAML 2.0 IdP assertion and switch into a Role&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass an OIDC IdP id_token and switch into a Role&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AssumeRoot&lt;/code&gt; (2024)&lt;/td&gt;
&lt;td&gt;From the Management Account, take Root-equivalent permissions on a Member Account for 15 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fixed 15 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GetSessionToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IAM User long-lived key plus (optional) MFA, upgraded to a temporary credential&lt;/td&gt;
&lt;td&gt;12 h (1 h when the caller is root)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GetFederationToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IAM User long-lived key mints a temporary credential for another identity (federated user)&lt;/td&gt;
&lt;td&gt;12 h (1 h when the caller is root)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DecodeAuthorizationMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expand the encoded AccessDenied message into something human-readable&lt;/td&gt;
&lt;td&gt;(not an issuer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GetCallerIdentity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns "if I call AWS right now with these credentials, who am I?"&lt;/td&gt;
&lt;td&gt;(not an issuer)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Frequency in real work: &lt;strong&gt;&lt;code&gt;AssumeRole&lt;/code&gt; &amp;gt; &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; (CI) &amp;gt; &lt;code&gt;AssumeRoleWithSAML&lt;/code&gt; (corporate IdP) &amp;gt; &lt;code&gt;DecodeAuthorizationMessage&lt;/code&gt; (debugging)&lt;/strong&gt;. &lt;code&gt;GetSessionToken&lt;/code&gt; and &lt;code&gt;GetFederationToken&lt;/code&gt; are IAM User-era APIs. With Identity Center and OIDC unifying humans and CI, they barely show up anymore.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not confuse &lt;code&gt;GetSessionToken&lt;/code&gt; with &lt;code&gt;AssumeRole&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The name &lt;code&gt;GetSessionToken&lt;/code&gt; suggests "the API that returns you a session token." It actually means &lt;strong&gt;"take an IAM User's long-lived key and upgrade it to a temporary credential, optionally with MFA."&lt;/strong&gt; You gain no new permissions. You get the same permissions as the original IAM User, just with an Expiration and (if MFA was used) the &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt; condition set.&lt;/p&gt;

&lt;p&gt;The canonical use case is enforcing MFA via &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt; in an IAM Policy condition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:TerminateInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BoolIfExists"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aws:MultiFactorAuthPresent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run as an IAM User from the CLI, call &lt;code&gt;get-session-token --serial-number &amp;lt;MFA ARN&amp;gt; --token-code &amp;lt;6 digits&amp;gt;&lt;/code&gt; to swap in a credential that carries the MFA condition. Save it as a separate profile in &lt;code&gt;~/.aws/credentials&lt;/code&gt; and use that profile for everything sensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GetFederationToken&lt;/code&gt; is basically retired
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GetFederationToken&lt;/code&gt; lets an IAM User long-lived key mint a temporary credential on behalf of someone else. It mattered when you stood up a custom corporate IdP server that held the IAM User key and minted credentials for authenticated employees.&lt;/p&gt;

&lt;p&gt;That era is over. The same outcome ships via &lt;strong&gt;Identity Center + Permission Set + SAML/OIDC&lt;/strong&gt;, without any long-lived IAM User key in the picture. New designs do not reach for &lt;code&gt;GetFederationToken&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Trust Policy vs Identity Policy
&lt;/h2&gt;

&lt;p&gt;"I have a few policies on this Role, but what is the Trust Policy?" is a recurring question. Roles carry &lt;strong&gt;two kinds of policy&lt;/strong&gt; evaluated at different moments.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Where it lives&lt;/th&gt;
&lt;th&gt;Evaluation moment&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trust Policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Role's &lt;code&gt;AssumeRolePolicyDocument&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;When &lt;code&gt;AssumeRole&lt;/code&gt; is called&lt;/td&gt;
&lt;td&gt;"Who is allowed to assume this Role?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Identity Policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Policies attached to the Role&lt;/td&gt;
&lt;td&gt;On each API call after assuming&lt;/td&gt;
&lt;td&gt;"What can the assumed Role do?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F03-trust-identity-policy-flow.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F03-trust-identity-policy-flow.png%3Fv%3D2" alt="Trust Policy and Identity Policy evaluation flow" width="720" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both are JSON policy documents with nearly identical syntax. The one difference: &lt;strong&gt;whether you write &lt;code&gt;Principal&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trust Policy &lt;strong&gt;requires &lt;code&gt;Principal&lt;/code&gt;&lt;/strong&gt;. You explicitly say "from &lt;code&gt;arn:aws:iam::111:user/alice&lt;/code&gt;," who can assume.&lt;/li&gt;
&lt;li&gt;Identity Policy has no &lt;code&gt;Principal&lt;/code&gt;. The owner is the attached Role, no ambiguity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: a Trust Policy that allows GitHub Actions OIDC federation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Federated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:my-org/my-repo:ref:refs/heads/main"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The common mistake here: &lt;strong&gt;writing &lt;code&gt;sts:AssumeRole&lt;/code&gt; for the Action&lt;/strong&gt;. OIDC requires &lt;code&gt;sts:AssumeRoleWithWebIdentity&lt;/code&gt;. SAML needs &lt;code&gt;sts:AssumeRoleWithSAML&lt;/code&gt;. Plain role-switching needs &lt;code&gt;sts:AssumeRole&lt;/code&gt;. Memorize this: &lt;strong&gt;the API name you call equals the Action name in the Trust Policy&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. External ID: stopping the Confused Deputy
&lt;/h2&gt;

&lt;p&gt;If a SaaS vendor has ever asked you to "add this ExternalId to your AWS Role's Trust Policy," that is the Confused Deputy defense.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Confused Deputy
&lt;/h3&gt;

&lt;p&gt;An attack where "a third party (the Deputy) who legitimately holds permissions on your behalf" is tricked by a different attacker into exercising those permissions for someone other than you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F04-confused-deputy-attack.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F04-confused-deputy-attack.png%3Fv%3D2" alt="Confused Deputy attack via SaaS Vendor" width="760" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Victim's Trust Policy only says "the Vendor's AWS account may assume this Role." The SaaS vendor's system mints AssumeRole calls on behalf of many customers. If the Vendor's tenant isolation is sloppy, the attacker registers "my tenant's Role ARN is (actually the Victim's Role ARN)" and the Vendor unwittingly assumes the Victim's Role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Close the hole with ExternalId
&lt;/h3&gt;

&lt;p&gt;ExternalId is &lt;strong&gt;"a value only the Victim knows, that the SaaS vendor must include when assuming the Victim's Role for that tenant."&lt;/strong&gt; The Victim writes it into the Trust Policy, the Vendor passes it via &lt;code&gt;--external-id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F05-externalid-blocks-attack.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F05-externalid-blocks-attack.png%3Fv%3D2" alt="ExternalId blocks the Confused Deputy attack" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Victim's Trust Policy looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::SAAS_VENDOR_ACCOUNT:root"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sts:ExternalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ab8a3c7e-4d1f-4d9b-90e4-..."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ExternalId is &lt;strong&gt;not a secret password&lt;/strong&gt;. The Vendor knows it. It is written in plain text in the Trust Policy. It is just &lt;strong&gt;"a unique string the customer (the Victim) and the SaaS agreed on."&lt;/strong&gt; Its job is to make AWS re-validate the Vendor-internal tenant routing.&lt;/p&gt;

&lt;p&gt;Vendor responsibilities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a &lt;strong&gt;distinct ExternalId per customer&lt;/strong&gt; (UUID recommended).&lt;/li&gt;
&lt;li&gt;Never pass customer A's ExternalId into customer B's AssumeRole. No internal swaps.&lt;/li&gt;
&lt;li&gt;Show ExternalId only to the customer it belongs to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Customer (Victim) responsibilities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always&lt;/strong&gt; put the Vendor-supplied ExternalId into your Trust Policy's Condition.&lt;/li&gt;
&lt;li&gt;Treat "an external vendor Role with no ExternalId" as a design defect.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Source Identity: keep the real human in CloudTrail
&lt;/h2&gt;

&lt;p&gt;When you assume a Role, CloudTrail shows &lt;code&gt;arn:aws:sts::123456789012:assumed-role/MyRole/SomeSessionName&lt;/code&gt;. &lt;code&gt;SomeSessionName&lt;/code&gt; is the required &lt;code&gt;--role-session-name&lt;/code&gt; argument.&lt;/p&gt;

&lt;p&gt;In day-to-day use this ends up as &lt;code&gt;bob-cli-session&lt;/code&gt; or &lt;code&gt;1700000000&lt;/code&gt; or whatever string someone threw in. &lt;strong&gt;CloudTrail alone cannot tell you who actually called the API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source Identity solves this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Properties of Source Identity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pass it at AssumeRole with &lt;code&gt;--source-identity bob@example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once set, it is immutable for the session.&lt;/strong&gt; Cannot be tampered with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survives Role Chaining.&lt;/strong&gt; The value propagates to subsequent assumed Roles.&lt;/li&gt;
&lt;li&gt;It always appears at &lt;code&gt;userIdentity.sessionContext.sourceIdentity&lt;/code&gt; in CloudTrail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So stamping the "original human ID" at the first AssumeRole locks it in for the whole chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F06-source-identity-propagation.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F06-source-identity-propagation.png%3Fv%3D2" alt="Source Identity propagation across Role Chain" width="719" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce it in the Trust Policy
&lt;/h3&gt;

&lt;p&gt;Tighten the operation: refuse any AssumeRole that does not set Source Identity. Add this to the Role's Trust Policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111:root"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111:root"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:SetSourceIdentity"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111:root"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sts:SourceIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need &lt;code&gt;sts:SetSourceIdentity&lt;/code&gt; to be Allow-ed before SourceIdentity can be stamped. Stack a Deny that fires when the value is empty.&lt;/p&gt;

&lt;p&gt;One trap: &lt;strong&gt;to propagate Source Identity across Role Chain or Cross-account, you must write &lt;code&gt;sts:SetSourceIdentity&lt;/code&gt; in two places&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The caller Principal's Identity Policy (the IAM User / Role making the call)&lt;/li&gt;
&lt;li&gt;The target Role's Trust Policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Miss either and the chained AssumeRole fails.&lt;/p&gt;

&lt;p&gt;In Identity Center environments, mapping &lt;code&gt;https://aws.amazon.com/SAML/Attributes/SourceIdentity&lt;/code&gt; on the IdP side to the user's email gives you automatic Source Identity stamping on every Identity Center-mediated AssumeRole. That is the ideal setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Session Tag and Transitive Tag
&lt;/h2&gt;

&lt;p&gt;Session Tags are key=value pairs passed at AssumeRole. They attach to the temporary credential and become available in policy Conditions as &lt;code&gt;aws:PrincipalTag/Team&lt;/code&gt;. They are the engine that drives ABAC (attribute-based access control).&lt;/p&gt;

&lt;h3&gt;
  
  
  Plain Session Tag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts assume-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::111:role/DataEngineerRole &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-session-name&lt;/span&gt; alice-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Team,Value&lt;span class="o"&gt;=&lt;/span&gt;ml &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Project,Value&lt;span class="o"&gt;=&lt;/span&gt;recsys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Role's Identity Policy side, use the tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::data-*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/Team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3:ResourceTag/Team}"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This says "Allow only when the principal's Team matches the bucket's Team." Instead of creating a Role per Team, &lt;strong&gt;one Role with dynamic filtering by Tag&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transitive Tag: carry across Role Chain
&lt;/h3&gt;

&lt;p&gt;Plain Session Tags &lt;strong&gt;disappear at the next AssumeRole&lt;/strong&gt;. After Role A to Role B, the tag on Role A's session does not appear on Role B's session.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--transitive-tag-keys&lt;/code&gt; makes a Tag &lt;strong&gt;survive the chain&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts assume-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::111:role/RoleA &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-session-name&lt;/span&gt; session1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Team,Value&lt;span class="o"&gt;=&lt;/span&gt;ml &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Project,Value&lt;span class="o"&gt;=&lt;/span&gt;recsys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transitive-tag-keys&lt;/span&gt; Team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;aws:PrincipalTag/Team=ml&lt;/code&gt; stays alive across Role A to Role B to Role C. &lt;code&gt;Project&lt;/code&gt; drops at the first hop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F07-transitive-tag-chain.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F07-transitive-tag-chain.png%3Fv%3D2" alt="Transitive Tag survives Role Chain" width="800" height="1279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Transitive Tag also gives you a guarantee: &lt;strong&gt;an attribute stamped upstream cannot be overwritten downstream&lt;/strong&gt;. Even if a downstream call to AssumeRole sets &lt;code&gt;--tags Team=admin&lt;/code&gt; with the same key, the upstream Transitive Tag wins.&lt;/p&gt;

&lt;p&gt;Same shape as Source Identity: &lt;strong&gt;an attribute stamped at the upstream trust boundary survives intact downstream&lt;/strong&gt;. That is what makes ABAC trustworthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Session Policy: narrow at the call site
&lt;/h2&gt;

&lt;p&gt;Session Policy is an &lt;strong&gt;inline policy passed as an AssumeRole argument&lt;/strong&gt;. "Keep the Role's full permissions, but for this specific session, narrow further to just these." You are lowering the permission ceiling through the call argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts assume-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::111:role/AdminRole &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-session-name&lt;/span&gt; restricted-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::reports/*"
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Policy is AND (intersection) with the Role's Identity Policy.&lt;/strong&gt; It can only narrow, never widen.&lt;/li&gt;
&lt;li&gt;Even if the Role allows all of &lt;code&gt;s3:*&lt;/code&gt;, a Session Policy restricted to &lt;code&gt;s3:GetObject&lt;/code&gt; ends up at just &lt;code&gt;s3:GetObject&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Writing &lt;code&gt;ec2:*&lt;/code&gt; in a Session Policy when the Role does not allow it grants nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F08-session-policy-intersection.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F08-session-policy-intersection.png%3Fv%3D2" alt="Session Policy as intersection with Role Identity Policy" width="800" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use cases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral narrowing&lt;/strong&gt;: "I am inside Admin Role but for this 1 hour I only touch a specific S3 prefix." Self-imposed handcuffs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited delegation to a SaaS&lt;/strong&gt;: when a SaaS Vendor assumes your Role, layer a Session Policy at the call site so the Vendor can only hit the minimum it actually needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extra defense on cross-account&lt;/strong&gt;: even when a Role is open to assume from another account, the AssumeRole-side Session Policy enforces "what runs in our account is read-only."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can pass up to 10 managed policy ARNs plus 1 inline policy (the inline one has a 2 KB cap).&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Role Chaining: the 1-hour wall
&lt;/h2&gt;

&lt;p&gt;Role Chaining means "use Role A's temporary credential to AssumeRole into Role B."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F09-role-chaining-1h-wall.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F09-role-chaining-1h-wall.png%3Fv%3D2" alt="Role Chaining capped at 1 hour after first hop" width="760" height="881"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trap: &lt;strong&gt;once you chain, DurationSeconds is hard-capped at 1 hour (3600 s) for every hop after the first&lt;/strong&gt;. Even if the Role's MaxSessionDuration is 12 hours, a request beyond 1 hour is rejected.&lt;/p&gt;

&lt;p&gt;If you leave &lt;code&gt;DurationSeconds&lt;/code&gt; at the default (1 hour) you never see it. But "the first hop ran 12 hours fine, so the second should too" leads to Terraform / scripts breaking on &lt;code&gt;The requested DurationSeconds exceeds the 1 hour session limit for roles assumed by role chaining&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is simple: &lt;strong&gt;do not chain&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Within an account: usually you can skip the relay Role and assume the target directly.&lt;/li&gt;
&lt;li&gt;Cross-account: reconsider whether the relay Role is actually needed. Moving to Identity Center removes the relay.&lt;/li&gt;
&lt;li&gt;When chaining is genuinely required (some delegated-admin patterns): write a refresh loop that re-assumes every hour.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. DurationSeconds limits
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DurationSeconds&lt;/code&gt; for &lt;code&gt;AssumeRole&lt;/code&gt; and friends ranges from 900 (15 min) to 43200 (12 hours). What you actually get is the minimum of several limits.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API spec maximum&lt;/td&gt;
&lt;td&gt;43200 s (12 h)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role's &lt;code&gt;MaxSessionDuration&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3600 to 43200 s (set when the Role is created)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role Chaining (see section 9)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3600 s (fixed)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AssumeRoot&lt;/code&gt; (2024)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;900 s (fixed)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GetSessionToken&lt;/code&gt; (IAM User)&lt;/td&gt;
&lt;td&gt;129600 s (36 h). 3600 s when called by root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GetFederationToken&lt;/code&gt; (IAM User)&lt;/td&gt;
&lt;td&gt;43200 s (12 h). 3600 s when called by root&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Effective limit is &lt;strong&gt;API spec ∩ Role.MaxSessionDuration ∩ chain constraint&lt;/strong&gt;, take the minimum. "I asked for 43200 and got 3600 back" almost always means the Role's MaxSessionDuration is still at 1 hour, or you are chaining.&lt;/p&gt;

&lt;p&gt;Check with &lt;code&gt;aws iam get-role --role-name MyRole&lt;/code&gt; to see &lt;code&gt;MaxSessionDuration&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. GitHub Actions OIDC integration (a quick look)
&lt;/h2&gt;

&lt;p&gt;The most popular use of &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; is GitHub Actions OIDC. It ends the era of putting long-lived keys (&lt;code&gt;AKIA...&lt;/code&gt;) into GitHub Secrets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F10-github-actions-oidc.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F10-github-actions-oidc.png%3Fv%3D2" alt="GitHub Actions OIDC into AWS STS" width="720" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just the points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register &lt;strong&gt;&lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt; as an IAM OIDC Identity Provider&lt;/strong&gt; up front.&lt;/li&gt;
&lt;li&gt;In the receiving Role's Trust Policy, narrow the &lt;strong&gt;&lt;code&gt;sub&lt;/code&gt; StringLike condition&lt;/strong&gt; to &lt;strong&gt;repository + branch&lt;/strong&gt;. Loose conditions like &lt;code&gt;repo:my-org/*&lt;/code&gt; open the door for other repos in the org to assume.&lt;/li&gt;
&lt;li&gt;The workflow side just uses &lt;code&gt;aws-actions/configure-aws-credentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The reason the Job can get an id_token without long-lived secrets: GitHub's control plane injects a short-lived internal token (&lt;code&gt;ACTIONS_ID_TOKEN_REQUEST_TOKEN&lt;/code&gt;) into env vars when the Runner starts. The long-lived secret exists on GitHub's side, not on yours. "Zero long-lived credentials" is a user-side statement, not a literal one (typical workload identity pattern).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this, zero long-lived keys live in GitHub Secrets. This is a different path from Roles Anywhere, and for CI it is far easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. How it shows up in CloudTrail
&lt;/h2&gt;

&lt;p&gt;Once you have assumed a Role, CloudTrail's &lt;code&gt;userIdentity&lt;/code&gt; block tells you "who is this calling as."&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;userIdentity&lt;/code&gt; right after AssumeRole
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AssumedRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"principalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AROAEXAMPLEID:alice-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sts::123456789012:assumed-role/DataEngineerRole/alice-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accessKeyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASIAEXAMPLEKEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sessionContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sessionIssuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"principalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AROAEXAMPLEID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::123456789012:role/DataEngineerRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"accountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DataEngineerRole"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"creationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-17T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mfaAuthenticated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type: "AssumedRole"&lt;/code&gt; tells you "this call used a temporary credential."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;arn&lt;/code&gt; follows &lt;strong&gt;&lt;code&gt;arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME&lt;/code&gt;&lt;/strong&gt;, an unusual format. Note the &lt;code&gt;sts::&lt;/code&gt;, not the regular Role ARN (&lt;code&gt;arn:aws:iam::...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;principalId&lt;/code&gt; is &lt;code&gt;AROA...&lt;/code&gt; (the Role ID) plus &lt;code&gt;:&lt;/code&gt; plus the session name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accessKeyId&lt;/code&gt; starts with &lt;code&gt;ASIA...&lt;/code&gt; (the temporary credential marker).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sessionContext.sourceIdentity&lt;/code&gt;&lt;/strong&gt; carries the Source Identity. Empty here means you cannot tell who, which is why stamping is worth enforcing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionIssuer&lt;/code&gt; is the Role this session came from. In a chain, the nearest Role.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Athena: aggregate "by human"
&lt;/h3&gt;

&lt;p&gt;If CloudTrail is landed in S3, this Athena query gives you "API calls per human."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;useridentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessioncontext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sourceidentity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;useridentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;who&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;eventname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cloudtrail_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;eventtime&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-05-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-05-17'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Source Identity enforced, the first column is &lt;code&gt;alice@example.com&lt;/code&gt;. Without it, you get a tasteless &lt;code&gt;arn:aws:sts::.../session-1700000000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the most concrete reason to enforce Source Identity: &lt;strong&gt;audit aggregation becomes mechanical&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. AssumeRoot: the 2024 re:Invent addition
&lt;/h2&gt;

&lt;p&gt;In November 2024, just before re:Invent, AWS released a new STS API: &lt;code&gt;AssumeRoot&lt;/code&gt;. Its purpose: &lt;strong&gt;"From the Management Account, take Root-equivalent permissions on a Member Account on behalf of that account."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it exists
&lt;/h3&gt;

&lt;p&gt;Member Accounts in AWS Organizations all have a Root User. Some operations require the Root User. Examples.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unsticking an S3 bucket whose Bucket Policy is Deny for every Principal&lt;/strong&gt;: no IAM permission can fix it, only Root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsticking an SQS queue whose Queue Policy is Deny for every Principal&lt;/strong&gt;: same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resetting Root User MFA when lost&lt;/strong&gt;: only the Root itself can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting / enabling Root credentials&lt;/strong&gt;: same.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing these required logging into the Member Account's Root, and maintaining MFA and password-recovery email for every Root across the organization. &lt;strong&gt;In organizations with hundreds of accounts, this was an incident factory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AssumeRoot&lt;/code&gt; fixes it. &lt;strong&gt;From the Management Account (or the IAM delegated admin Account), take Root-equivalent permissions on a Member Account, narrowed to a specific task, for 15 minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F11-assumeroot-flow.png%3Fv%3D2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-sts-deep-dive%2Fdiagrams%2F11-assumeroot-flow.png%3Fv%3D2" alt="AssumeRoot flow for Member Account root tasks" width="719" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoping via TaskPolicy
&lt;/h3&gt;

&lt;p&gt;The key constraint of &lt;code&gt;AssumeRoot&lt;/code&gt; is that &lt;strong&gt;TaskPolicy is required&lt;/strong&gt;. You cannot mint a session that says "Root, anything goes." You must pick one of the AWS-managed task-scoped policies. Representative ones.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task Policy (managed)&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S3UnlockBucketPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read / Delete on S3 Bucket Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SQSUnlockQueuePolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read / Delete on SQS Queue Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IAMDeleteRootUserCredentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete the Member Account's Root credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IAMCreateRootUserPassword&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regenerate the Member Account's Root password (recovery)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IAMAuditRootUserCredentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read-only inspection of Root credential state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So you cannot "do anything because you are Root." You get a "Root session scoped to unlock S3 bucket policy" or "Root session scoped to unlock SQS queue policy." Roles are narrow by design, and audits stay readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Two Organizations-side settings are required to call &lt;code&gt;AssumeRoot&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable Centralized root access for member accounts&lt;/strong&gt; (Organizations console / API).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete or disable each Member Account's Root credentials&lt;/strong&gt; (or use SCP to forbid direct Root login).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The caller's IAM Identity Policy needs &lt;code&gt;sts:AssumeRoot&lt;/code&gt; Allow. Only the Management Account or a delegated admin account can hold it. You cannot grant it on a Member Account directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit side
&lt;/h3&gt;

&lt;p&gt;If your org has monitored only &lt;code&gt;ConsoleLogin&lt;/code&gt; events for Root, take note. With Root credentials deleted, Root console logins stop happening, so &lt;strong&gt;add &lt;code&gt;sts:AssumeRoot&lt;/code&gt; to your monitored event set&lt;/strong&gt;. Vendors like Elastic Security Labs already publish detection rules for "rare user plus Member Account AssumeRoot."&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Do this / avoid that
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Avoid&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reaching for the Global STS endpoint (&lt;code&gt;sts.amazonaws.com&lt;/code&gt;) on a new project.&lt;/li&gt;
&lt;li&gt;Putting only &lt;code&gt;*&lt;/code&gt; in Trust Policy's Principal without a Condition.&lt;/li&gt;
&lt;li&gt;Forgetting ExternalId on a Role for an external vendor.&lt;/li&gt;
&lt;li&gt;Stacking 3 or more Role Chain hops (the trust boundary blurs).&lt;/li&gt;
&lt;li&gt;Random session names with an empty Source Identity.&lt;/li&gt;
&lt;li&gt;Granting &lt;code&gt;AssumeRoot&lt;/code&gt; to a Member Account Principal. Keep it on Management / delegated admin only.&lt;/li&gt;
&lt;li&gt;Maxing &lt;code&gt;DurationSeconds&lt;/code&gt; to 12 hours everywhere. You lose the short-lived benefit.&lt;/li&gt;
&lt;li&gt;Continuing to use long-lived keys (&lt;code&gt;AKIA...&lt;/code&gt;) without MFA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default to Regional STS endpoints.&lt;/li&gt;
&lt;li&gt;Require ExternalId on external vendor Roles.&lt;/li&gt;
&lt;li&gt;Route through Identity Center so SAML/OIDC stamps SourceIdentity automatically.&lt;/li&gt;
&lt;li&gt;Design Team / Project ABAC with Transitive Tags surviving the chain.&lt;/li&gt;
&lt;li&gt;Layer Session Policy at AssumeRole call sites: "this session only touches this bucket."&lt;/li&gt;
&lt;li&gt;Write a refresh loop aware of the 1-hour cap after Role Chain.&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;sts:AssumeRoot&lt;/code&gt; in CloudTrail and route to Slack.&lt;/li&gt;
&lt;li&gt;Aggregate CloudTrail by &lt;code&gt;sourceIdentity&lt;/code&gt; so "who did what" becomes mechanical.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;STS is not a single function. It is 6 issuance APIs (&lt;code&gt;AssumeRole&lt;/code&gt;, &lt;code&gt;AssumeRoleWithSAML&lt;/code&gt;, &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;, &lt;code&gt;AssumeRoot&lt;/code&gt;, &lt;code&gt;GetSessionToken&lt;/code&gt;, &lt;code&gt;GetFederationToken&lt;/code&gt;) plus helpers (&lt;code&gt;DecodeAuthorizationMessage&lt;/code&gt;, &lt;code&gt;GetCallerIdentity&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Global and Regional both exist. New work picks Regional.&lt;/li&gt;
&lt;li&gt;A temporary credential is &lt;code&gt;ASIA...&lt;/code&gt; + Secret + SessionToken, sent with &lt;code&gt;X-Amz-Security-Token&lt;/code&gt; riding on SigV4.&lt;/li&gt;
&lt;li&gt;Trust Policy answers "who is allowed to assume." Identity Policy answers "what can the assumed Role do."&lt;/li&gt;
&lt;li&gt;ExternalId stops Confused Deputy through a value the customer and vendor agreed on, enforced in the Trust Policy.&lt;/li&gt;
&lt;li&gt;Source Identity carries the original human ID across the chain. Required for mechanical CloudTrail aggregation.&lt;/li&gt;
&lt;li&gt;Transitive Session Tag is the ABAC engine. Upstream stamping survives downstream.&lt;/li&gt;
&lt;li&gt;Session Policy is intersection at the call site. It narrows, never widens.&lt;/li&gt;
&lt;li&gt;DurationSeconds is hard-capped at 1 hour after Role Chaining.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AssumeRoot&lt;/code&gt; from 2024 re:Invent is the core of Centralized Root Access. Take Member Root from Management for 15 minutes, scoped by TaskPolicy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html" rel="noopener noreferrer"&gt;AWS Security Token Service API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_region-endpoints.html" rel="noopener noreferrer"&gt;AWS STS Regions and endpoints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html" rel="noopener noreferrer"&gt;AWS STS Regional endpoints (SDKs and Tools)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/developer/updating-aws-sdk-defaults-aws-sts-service-endpoint-and-retry-strategy/" rel="noopener noreferrer"&gt;Updating AWS SDK defaults: STS endpoint and Retry Strategy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html" rel="noopener noreferrer"&gt;How to use external ID when granting access to your AWS resources to a third party&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" rel="noopener noreferrer"&gt;The confused deputy problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_monitor.html" rel="noopener noreferrer"&gt;Monitor and control actions taken with assumed roles (Source Identity)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html" rel="noopener noreferrer"&gt;Pass session tags in AWS STS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session" rel="noopener noreferrer"&gt;Session policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html" rel="noopener noreferrer"&gt;AssumeRole API: DurationSeconds and role chaining&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoot.html" rel="noopener noreferrer"&gt;AssumeRoot API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user-privileged-task.html" rel="noopener noreferrer"&gt;Perform a privileged task on an AWS Organizations member account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/security/secure-root-user-access-for-member-accounts-in-aws-organizations/" rel="noopener noreferrer"&gt;Secure root user access for member accounts in AWS Organizations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.elastic.co/security-labs/exploring-aws-sts-assumeroot" rel="noopener noreferrer"&gt;Exploring AWS STS AssumeRoot (Elastic Security Labs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services" rel="noopener noreferrer"&gt;Configuring OpenID Connect in Amazon Web Services (GitHub Docs)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>sts</category>
      <category>iam</category>
      <category>security</category>
    </item>
    <item>
      <title>WORM (Write Once Read Many) Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Mon, 18 May 2026 15:49:44 +0000</pubDate>
      <link>https://dev.to/kanywst/worm-write-once-read-many-deep-dive-2l7k</link>
      <guid>https://dev.to/kanywst/worm-write-once-read-many-deep-dive-2l7k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;"Data can be deleted." It looks obvious, but it is actually a &lt;strong&gt;fatal default from a legal and ransomware perspective&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When an auditor asks for 7 years of email, the moment a single person can type &lt;code&gt;DELETE&lt;/code&gt;, evidentiary value collapses&lt;/li&gt;
&lt;li&gt;When ransomware encrypts your backups too, recovery is impossible and you end up paying the ransom&lt;/li&gt;
&lt;li&gt;When someone suspects "that trade record was tampered with, wasn't it?", you lose in court unless you can show that tampering is technically impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer to all of these is &lt;strong&gt;WORM (Write Once Read Many)&lt;/strong&gt;. A storage mode where once you write data, &lt;strong&gt;nobody&lt;/strong&gt; can delete or rewrite it during the prescribed period. Not the root admin, not the attacker, not the operations team at the service provider.&lt;/p&gt;

&lt;p&gt;This article dissects "undeletable storage" in the following order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What WORM is (definition)&lt;/li&gt;
&lt;li&gt;History of WORM (from optical discs to cloud)&lt;/li&gt;
&lt;li&gt;The 3 worlds that need WORM (regulation / ransomware / insider threat)&lt;/li&gt;
&lt;li&gt;The 3 ways to implement WORM (physical / on-prem / cloud)&lt;/li&gt;
&lt;li&gt;The main event of cloud WORM: dissecting S3 Object Lock&lt;/li&gt;
&lt;li&gt;Side-by-side comparison: AWS / Azure / GCP / on-prem NetApp&lt;/li&gt;
&lt;li&gt;Why regulators demand WORM (SEC 17a-4 and the 2022 amendment)&lt;/li&gt;
&lt;li&gt;The clash with GDPR "right to be forgotten" and how to solve it&lt;/li&gt;
&lt;li&gt;Famous companies and actual fine cases&lt;/li&gt;
&lt;li&gt;Design pitfalls and how to avoid them&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is long, but read top to bottom and you will see what WORM is, why it is needed, how to build it, and how to use it as one connected story.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. What WORM is
&lt;/h2&gt;

&lt;p&gt;WORM stands for &lt;strong&gt;Write Once Read Many&lt;/strong&gt;. It refers to a storage mode in which data, once written, &lt;strong&gt;cannot be rewritten or deleted&lt;/strong&gt; until a defined retention period has elapsed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F01-worm-overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F01-worm-overview.png" alt="WORM overview" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The definition has three cores.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write Once&lt;/strong&gt;: the object &lt;strong&gt;cannot be overwritten&lt;/strong&gt; with new content from the start of protection until the retention expires&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read Many&lt;/strong&gt;: reads are unlimited, by anyone (who is authorized)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention&lt;/strong&gt;: configurable "until when can it not be deleted". Specified in days, or indefinite (Legal Hold)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key thing to remember: in modern implementations, WORM is &lt;strong&gt;not a property of the whole storage but a flag attached to an object&lt;/strong&gt;. Inside one bucket it is normal to have object A locked for 7 years and object B not locked at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Once" strictly means "that version"
&lt;/h3&gt;

&lt;p&gt;In modern implementations like S3 Object Lock, WORM applies at the &lt;strong&gt;version level&lt;/strong&gt;. A new PUT to the same key (path) is written as a &lt;strong&gt;new version&lt;/strong&gt;, and the old version remains untouched. So it is "appending to history", not "overwriting content".&lt;/p&gt;

&lt;p&gt;This is an important premise for the GDPR "right to be forgotten" conflict discussion later (Section 8).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. History of WORM: why it has existed since the 1980s
&lt;/h2&gt;

&lt;p&gt;WORM is not a new concept. It is a technology that &lt;strong&gt;has existed at the physical media level since the 1980s&lt;/strong&gt;, moved up to the software layer, and finally became a cloud API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F02-history-timeline.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F02-history-timeline.png" alt="WORM technology timeline" width="800" height="3274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Physical WORM era (1984 to 2000)
&lt;/h3&gt;

&lt;p&gt;The first WORM implementations were physically un-rewritable at the media itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optical disc WORM&lt;/strong&gt;: a laser physically burns pits into the recording layer (ablative). Burnt pits cannot be undone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magneto-Optical (MO)&lt;/strong&gt;: the first MO drives in 1985 were &lt;strong&gt;WORM only&lt;/strong&gt;. They ran a verification pass after writing, so writing took 3x as long as reading, but reliability was extremely high&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CD-R / DVD-R&lt;/strong&gt;: consumer-grade WORM popular in the 1990s. Burning legal documents, medical images, and broadcaster master tapes onto discs and locking them in a safe was the standard workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WORM in this era was complete once you "&lt;strong&gt;put the media in a safe&lt;/strong&gt;". A simple guarantee model: tampering is impossible unless an attacker physically reaches the safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software WORM era (2002 to 2017)
&lt;/h3&gt;

&lt;p&gt;The problem was that optical discs do not scale to TB-class data retention. So the idea emerged: "&lt;strong&gt;provide WORM properties in software on top of ordinary hard disks&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;The representatives were &lt;strong&gt;EMC Centera&lt;/strong&gt; (2002) and &lt;strong&gt;NetApp SnapLock&lt;/strong&gt; (2003). They put logic at the OS / firmware level on top of a normal HDD array that "rejects delete / overwrite during the retention period".&lt;/p&gt;

&lt;p&gt;This solved the scale problem but raised a new issue: &lt;strong&gt;"what if you turn the OS clock back?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NetApp killed this with a solution called &lt;strong&gt;ComplianceClock&lt;/strong&gt; (detailed in Section 4). An internal hardware-based clock that is independent of the OS clock and only moves forward, used as the basis for retention. This makes the "turn the clock back to expire retention" attack impossible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud WORM era (2018 onward)
&lt;/h3&gt;

&lt;p&gt;Since AWS released &lt;strong&gt;S3 Object Lock&lt;/strong&gt; in 2018, WORM has become a standard feature of cloud object storage. Azure and GCP followed, and now if you are cloud-native you can get regulation-grade WORM by just "&lt;strong&gt;creating one bucket and flipping a flag&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;Unlike physical media it scales to TB instantly, and replication handles redundancy. The "put media in a safe" workflow of optical discs is now only seen at regional banks, municipalities, and old hospitals.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The 3 worlds that need WORM
&lt;/h2&gt;

&lt;p&gt;Many people think "we are not a regulated industry, so we do not need WORM", but in modern times there are &lt;strong&gt;2 reasons besides regulation&lt;/strong&gt; that demand WORM.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;What to protect&lt;/th&gt;
&lt;th&gt;Attack / failure scenario&lt;/th&gt;
&lt;th&gt;Main industries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;Regulation&lt;/td&gt;
&lt;td&gt;Trade / communication records (5 to 30y)&lt;/td&gt;
&lt;td&gt;Legal violation, huge fines, suspension order&lt;/td&gt;
&lt;td&gt;Securities, banking, healthcare, pharma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;②&lt;/td&gt;
&lt;td&gt;Ransomware&lt;/td&gt;
&lt;td&gt;Backups themselves&lt;/td&gt;
&lt;td&gt;Attacker takes admin rights and encrypts backups along with the rest&lt;/td&gt;
&lt;td&gt;All industries (exploded after 2020)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③&lt;/td&gt;
&lt;td&gt;Insider threat&lt;/td&gt;
&lt;td&gt;Audit logs, auth logs, operation history&lt;/td&gt;
&lt;td&gt;Admin or DBA deletes logs to hide evidence of wrongdoing&lt;/td&gt;
&lt;td&gt;SOC, SaaS operators, finance in general&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ① Regulation
&lt;/h3&gt;

&lt;p&gt;Historically this is where WORM was born. The US &lt;strong&gt;SEC Rule 17a-4&lt;/strong&gt; is the representative: it requires securities firms to retain trade records in a "&lt;strong&gt;non-rewriteable, non-erasable&lt;/strong&gt;" form. Equivalents exist in Japan (FIEA), the EU (MiFID II), healthcare (HIPAA), and pharma (FDA 21 CFR Part 11).&lt;/p&gt;

&lt;h3&gt;
  
  
  ② Ransomware
&lt;/h3&gt;

&lt;p&gt;This is the biggest reason WORM use exploded after 2020. Ransomware attackers have established a playbook of &lt;strong&gt;going for the backups first&lt;/strong&gt;. Once backups are gone, paying the ransom is the only option.&lt;/p&gt;

&lt;p&gt;If "the backup storage is on WORM", attackers cannot delete backups even after taking admin. In fact, backup products like Veeam / Rubrik / Commvault / Cohesity integrate with S3 Object Lock and Azure Immutable Blob and market a feature called "&lt;strong&gt;Immutable Backup&lt;/strong&gt;".&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ Insider threat
&lt;/h3&gt;

&lt;p&gt;Use audit logs, trade history, and security events for &lt;strong&gt;making sure admins themselves cannot delete them&lt;/strong&gt;. For example, if DB access logs flow into WORM storage, a DBA cannot "go delete the logs" after doing something bad.&lt;/p&gt;

&lt;p&gt;Especially &lt;strong&gt;SOC (Security Operation Center) logs&lt;/strong&gt; and &lt;strong&gt;FIDO / authentication logs&lt;/strong&gt; as evidence stores are becoming best practice to flow into WORM.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The 3 ways to implement WORM
&lt;/h2&gt;

&lt;p&gt;From here, the "how to build it" story. In modern times, WORM can be implemented at three large layers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F03-three-layers.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F03-three-layers.png" alt="Three layers of WORM implementation" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ① Physical media WORM
&lt;/h3&gt;

&lt;p&gt;Optical discs or WORM tape. Sony Optical Disc Archive and HPE LTO WORM tapes are the representatives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: pull the media and put it in a safe and it truly cannot be written (air gap). 50 to 100 year retention track records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: beyond TB-scale you need robotic libraries; cost and ops get heavy. Reading needs a dedicated drive; if you retain longer than the drive lifetime, you need to preserve the drive too&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it is used&lt;/strong&gt;: movie studio masters, national archives, broadcaster archives, medical imaging (legal 30-year retention)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ② On-prem software WORM
&lt;/h3&gt;

&lt;p&gt;Provides WORM at the filesystem or OS level on top of normal HDD / SSD arrays.&lt;/p&gt;

&lt;p&gt;The representatives are &lt;strong&gt;NetApp SnapLock&lt;/strong&gt; (still active), &lt;strong&gt;Dell EMC Centera&lt;/strong&gt; (EOL in 2019, replaced by a tool called CTA), &lt;strong&gt;HPE StoreOnce&lt;/strong&gt;, and &lt;strong&gt;Hitachi HCP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Taking NetApp SnapLock as an example, internally it works like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F04-snaplock-internals.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F04-snaplock-internals.png" alt="NetApp SnapLock internals" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WORM commit&lt;/strong&gt;: the moment a normal file gets the "read-only attribute" set, SnapLock promotes it to WORM file status. The retention date is recorded at the same time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ComplianceClock&lt;/strong&gt;: the core of SnapLock. A dedicated clock that &lt;strong&gt;is not affected by &lt;code&gt;ntpdate&lt;/code&gt; on the OS turning the clock back&lt;/strong&gt;. A hardware-based one-way clock initialized once, used as the basis for retention. This makes the "turn the clock back to expire retention" attack impossible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two modes&lt;/strong&gt;: Compliance (not deletable even by root) and Enterprise (deletable with privileged delete, but logged)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On-prem WORM is firmly rooted in old banks and hospitals. The reason is simple: it &lt;strong&gt;integrates easily with existing mainframe / NAS operations&lt;/strong&gt;, and &lt;strong&gt;"the data lives in our own rack" is easy to explain to auditors&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ Cloud WORM
&lt;/h3&gt;

&lt;p&gt;The main event since 2018. Starting with AWS S3 Object Lock, almost all major object storage services support WORM: Azure, GCP, Backblaze B2, Wasabi, Cloudflare R2.&lt;/p&gt;

&lt;p&gt;We dig deeper in the next section.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The main event of cloud WORM: dissecting S3 Object Lock
&lt;/h2&gt;

&lt;p&gt;S3 Object Lock went GA in November 2018. It is the de facto standard for WORM features, and Azure / GCP designed their APIs with it in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Big picture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F05-s3-object-lock-overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F05-s3-object-lock-overview.png" alt="S3 Object Lock overview" width="800" height="695"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are three elements.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retention Mode&lt;/strong&gt;: period-based (&lt;code&gt;Days&lt;/code&gt; or &lt;code&gt;Years&lt;/code&gt;). Two modes: Governance (loose) and Compliance (strict)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal Hold&lt;/strong&gt;: no period; locked indefinitely until explicitly released&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both can apply at the same time&lt;/strong&gt;: if the normal retention has expired but Legal Hold is on, it still cannot be deleted&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Governance vs Compliance: get this wrong and you have an incident
&lt;/h3&gt;

&lt;p&gt;The difference between them is "&lt;strong&gt;whether even the root user can delete or not&lt;/strong&gt;".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Governance&lt;/th&gt;
&lt;th&gt;Compliance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normal user delete&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileged delete (&lt;code&gt;s3:BypassGovernanceRetention&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ yes&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root user delete&lt;/td&gt;
&lt;td&gt;✅ yes (effectively)&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shortening retention period&lt;/td&gt;
&lt;td&gt;yes with privilege&lt;/td&gt;
&lt;td&gt;❌ absolutely no (extend only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changing retention mode&lt;/td&gt;
&lt;td&gt;can upgrade to Compliance with privilege&lt;/td&gt;
&lt;td&gt;❌ absolutely no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only way to release&lt;/td&gt;
&lt;td&gt;release with privilege&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;close the entire AWS account&lt;/strong&gt; (may still survive retention even then)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use case&lt;/td&gt;
&lt;td&gt;"accident prevention", test, internal policy&lt;/td&gt;
&lt;td&gt;regulatory requirements like SEC 17a-4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Once you put it in Compliance mode, even AWS Support cannot release it&lt;/strong&gt;. If you set 7 years, it absolutely stays for 7 years. Use Compliance in a test environment by mistake and the bucket fills with undeletable test data while billing climbs. Compliance is for production regulatory compliance only.&lt;/p&gt;

&lt;h3&gt;
  
  
  API: actually applying the lock
&lt;/h3&gt;

&lt;p&gt;The lock is specified by HTTP headers on PUT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-object &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-immutable-records &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; 2026/q1/trades.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; trades.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-lock-mode&lt;/span&gt; COMPLIANCE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-lock-retain-until-date&lt;/span&gt; &lt;span class="s2"&gt;"2033-05-17T00:00:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The essence is what gets sent under the hood.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;PUT&lt;/span&gt; &lt;span class="nn"&gt;/2026/q1/trades.csv&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-immutable-records.s3.amazonaws.com&lt;/span&gt;
&lt;span class="na"&gt;x-amz-object-lock-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COMPLIANCE&lt;/span&gt;
&lt;span class="na"&gt;x-amz-object-lock-retain-until-date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2033-05-17T00:00:00Z&lt;/span&gt;
&lt;span class="na"&gt;Content-MD5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;base64-md5&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;Content-Length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;12345&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you specify &lt;code&gt;x-amz-object-lock-mode&lt;/code&gt;, &lt;code&gt;x-amz-object-lock-retain-until-date&lt;/code&gt; is required&lt;/strong&gt;. One alone is not allowed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Content-MD5&lt;/code&gt; header is required&lt;/strong&gt;: a PUT with object-lock related headers forces MD5 verification. This is to prevent "tampering during PUT"&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dates are ISO 8601 (UTC, millisecond precision)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Object Lock operation sequence
&lt;/h3&gt;

&lt;p&gt;Following the actual flow of "lock → delete attempt → retention expires → delete" as a sequence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F06-object-lock-sequence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F06-object-lock-sequence.png" alt="S3 Object Lock operation sequence" width="800" height="1037"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the S3 server side, a check of &lt;code&gt;retain-until-date &amp;gt; current time&lt;/code&gt; runs, and if true it &lt;strong&gt;rejects at the HTTP layer&lt;/strong&gt;. Even if a DELETE flies due to an app bug or admin mistake, it never reaches the storage layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three gotchas when using Object Lock
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Versioning is a prerequisite&lt;/strong&gt;: to enable Object Lock you need S3 Versioning ON first. Easy if you flip both on at bucket creation, but watch the order when retrofitting an existing bucket (self-service retrofit on existing buckets became possible in November 2023; details in Section 10)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-PUT to the same key becomes a new version&lt;/strong&gt;: a version that has the lock applied is not removed even by Lifecycle. With Object Lock + Versioning, old versions pile up, so always use &lt;code&gt;NoncurrentVersionExpiration&lt;/code&gt; in Lifecycle together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal Hold uses different IAM Actions&lt;/strong&gt;: controlled by &lt;code&gt;s3:PutObjectLegalHold&lt;/code&gt; / &lt;code&gt;s3:GetObjectLegalHold&lt;/code&gt;. Manage who can apply or remove Legal Hold under a separate policy from retention&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Cloud WORM comparison: AWS / Azure / GCP / on-prem
&lt;/h2&gt;

&lt;p&gt;All three major clouds have WORM features, but the API and terminology differ. Side-by-side comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;AWS S3 Object Lock&lt;/th&gt;
&lt;th&gt;Azure Immutable Blob&lt;/th&gt;
&lt;th&gt;GCS Bucket Lock&lt;/th&gt;
&lt;th&gt;NetApp SnapLock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Granularity&lt;/td&gt;
&lt;td&gt;Object (Version)&lt;/td&gt;
&lt;td&gt;Container / Version&lt;/td&gt;
&lt;td&gt;Bucket / Object&lt;/td&gt;
&lt;td&gt;Volume + File&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Period unit&lt;/td&gt;
&lt;td&gt;Days / Years&lt;/td&gt;
&lt;td&gt;Days&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;td&gt;Days / Years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Not deletable by root" mode&lt;/td&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;Locked Policy&lt;/td&gt;
&lt;td&gt;Locked Retention Policy&lt;/td&gt;
&lt;td&gt;Compliance Mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Loose" mode&lt;/td&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Unlocked Policy&lt;/td&gt;
&lt;td&gt;Unlocked Policy&lt;/td&gt;
&lt;td&gt;Enterprise Mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legal Hold&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (Object Hold)&lt;/td&gt;
&lt;td&gt;yes (Event-Based Hold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-delete after period&lt;/td&gt;
&lt;td&gt;via Lifecycle&lt;/td&gt;
&lt;td&gt;via Lifecycle&lt;/td&gt;
&lt;td&gt;via Lifecycle&lt;/td&gt;
&lt;td&gt;configurable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory attestations&lt;/td&gt;
&lt;td&gt;SEC 17a-4, FINRA 4511, CFTC 1.31, HIPAA&lt;/td&gt;
&lt;td&gt;SEC 17a-4, FINRA 4511, CFTC 1.31&lt;/td&gt;
&lt;td&gt;SEC 17a-4, FINRA, CFTC&lt;/td&gt;
&lt;td&gt;SEC 17a-4 (longest)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock tampering defense&lt;/td&gt;
&lt;td&gt;AWS internal clock (root cannot touch)&lt;/td&gt;
&lt;td&gt;Azure internal clock&lt;/td&gt;
&lt;td&gt;GCP internal clock&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ComplianceClock&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First GA&lt;/td&gt;
&lt;td&gt;Nov 2018&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;2018 (Bucket Lock) / 2023 (Object Hold)&lt;/td&gt;
&lt;td&gt;2003&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The notable fact here is "&lt;strong&gt;cloud WORMs all converged to a similar shape&lt;/strong&gt;". S3 Object Lock effectively became the reference implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure peculiarity: Container vs Version
&lt;/h3&gt;

&lt;p&gt;Azure has &lt;strong&gt;two scopes&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container-level WORM&lt;/strong&gt;: WORM policy applied to the entire container. All blobs in the same container share the same retention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-level WORM&lt;/strong&gt;: WORM applied to individual blob versions. Feels almost identical to S3 Object Lock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommend Version-level for new designs. Container-level is an older design with less operational flexibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  GCP Bucket Lock has a brutal "no going back"
&lt;/h3&gt;

&lt;p&gt;GCS &lt;strong&gt;Retention Policy + Bucket Lock&lt;/strong&gt;, once locked, permanently disables the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shortening&lt;/strong&gt; retention period&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting&lt;/strong&gt; retention policy&lt;/li&gt;
&lt;li&gt;Releasing the bucket retention setting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extension is allowed. The bucket itself cannot be deleted unless all contents have completed retention and the bucket is empty. &lt;strong&gt;"Oops, my bad" does not work at all&lt;/strong&gt;, so always test it in dev / staging before production.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Why regulators demand WORM
&lt;/h2&gt;

&lt;p&gt;Worth understanding "why are the requirements this strict".&lt;/p&gt;

&lt;h3&gt;
  
  
  SEC Rule 17a-4: the original
&lt;/h3&gt;

&lt;p&gt;SEC Rule 17a-4 requires US &lt;strong&gt;broker-dealers (securities firms)&lt;/strong&gt; to retain trade records and communications (including email and chat) in a "&lt;strong&gt;non-rewriteable, non-erasable form&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;Established in 1997. Originally written assuming paper and microfilm, when electronic records came in, it was interpreted as "if storing electronically, in &lt;strong&gt;WORM format&lt;/strong&gt;" and operated that way ever since.&lt;/p&gt;

&lt;p&gt;Why so strict? The answer is simple: &lt;strong&gt;lying in securities transactions has huge instant upside&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete an insider trading evidence email → escape civil sanctions&lt;/li&gt;
&lt;li&gt;Rewrite a customer explanation → win the lawsuit&lt;/li&gt;
&lt;li&gt;Tamper with trade timestamps → push losses onto the customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these become technically possible, trust in the whole market collapses. So the logic is to mandate "technically impossible" mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  2022 amendment: the WORM-only era ended
&lt;/h3&gt;

&lt;p&gt;For years 17a-4 allowed only WORM, but &lt;strong&gt;on October 12, 2022 the SEC adopted an amendment&lt;/strong&gt; (Effective January 3, 2023; broker-dealer compliance deadline May 3, 2023) adding an alternative called "Audit Trail".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F07-rule-17a4-amendment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F07-rule-17a4-amendment.png" alt="SEC Rule 17a-4 amendment" width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a fairly big change: now you can meet the requirement with modern cloud-native designs (for example, streaming DB CDC logs into a separate WORM store). That said, "the most obviously sufficient option is still WORM", so conservative operations continue to choose WORM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other regulations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Regulation&lt;/th&gt;
&lt;th&gt;Industry&lt;/th&gt;
&lt;th&gt;Role of WORM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FINRA Rule 4511&lt;/td&gt;
&lt;td&gt;US securities&lt;/td&gt;
&lt;td&gt;extends 17a-4 to all FINRA member firms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CFTC Rule 1.31(c)-(d)&lt;/td&gt;
&lt;td&gt;US commodities futures&lt;/td&gt;
&lt;td&gt;retain trade records in WORM form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIPAA&lt;/td&gt;
&lt;td&gt;US healthcare&lt;/td&gt;
&lt;td&gt;prevent tampering of PHI (Protected Health Information)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FDA 21 CFR Part 11&lt;/td&gt;
&lt;td&gt;US pharma / medical dev&lt;/td&gt;
&lt;td&gt;authenticity of electronic records and signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIEA&lt;/td&gt;
&lt;td&gt;Japan finance&lt;/td&gt;
&lt;td&gt;"store in tamper-proof method for 7 years"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiFID II&lt;/td&gt;
&lt;td&gt;EU finance&lt;/td&gt;
&lt;td&gt;retain communications 5 years, prevent tampering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR Art. 32&lt;/td&gt;
&lt;td&gt;EU all industries&lt;/td&gt;
&lt;td&gt;integrity assurance (WORM is one concrete option)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  8. Clash with GDPR: "right to be forgotten" vs "WORM"
&lt;/h2&gt;

&lt;p&gt;Unavoidable when talking about WORM: the relationship with &lt;strong&gt;GDPR Article 17 (right to be forgotten)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The problem is simple.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GDPR: when a user requests, &lt;strong&gt;delete&lt;/strong&gt; their personal data&lt;/li&gt;
&lt;li&gt;WORM: the moment a record containing personal data lands, it &lt;strong&gt;cannot be deleted&lt;/strong&gt; during retention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looks irreconcilable. In fact, this was a long-running headache for cloud WORM design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 1: Crypto-Shredding (destroy the encryption key)
&lt;/h3&gt;

&lt;p&gt;The most widespread solution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F08-crypto-shredding.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F08-crypto-shredding.png" alt="Crypto-Shredding flow" width="800" height="927"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data itself &lt;strong&gt;remains in the WORM S3 still encrypted&lt;/strong&gt; (not deleted)&lt;/li&gt;
&lt;li&gt;Generate a separate encryption key per user (envelope encryption)&lt;/li&gt;
&lt;li&gt;When a deletion request comes, &lt;strong&gt;just delete the key&lt;/strong&gt;. Data can no longer be decrypted = effectively deleted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This satisfies both "WORM immutability" and "GDPR right to delete". Easy to assemble with AWS KMS plus S3 Object Lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: Off-chain / Hybrid
&lt;/h3&gt;

&lt;p&gt;A solution from the blockchain context. Put &lt;strong&gt;only the hash or reference&lt;/strong&gt; in WORM storage, and put the actual content in normal storage. When a deletion request comes, delete the content. The hash remains in WORM, so "data of this kind existed at this time" can be proven, but the content itself cannot be reconstructed.&lt;/p&gt;

&lt;p&gt;Useful for audit contexts where it is sufficient to prove "data existed at that timestamp".&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Famous companies and real cases
&lt;/h2&gt;

&lt;p&gt;Now the reality. What happens without WORM, and how companies that have it use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case ①: FINRA fines 12 major firms 14.4M USD (2016)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;December 21, 2016&lt;/strong&gt;, FINRA fined 12 firms a total of &lt;strong&gt;14.4 million USD&lt;/strong&gt;. The reason was that they &lt;strong&gt;"failed to store hundreds of millions of electronic records in WORM format"&lt;/strong&gt;. One of the largest WORM-violation fine cases ever.&lt;/p&gt;

&lt;p&gt;The top 4 breakdown.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Fine&lt;/th&gt;
&lt;th&gt;Firm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$4.0M&lt;/td&gt;
&lt;td&gt;Wells Fargo Securities + Wells Fargo Prime Services (combined)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$3.5M&lt;/td&gt;
&lt;td&gt;RBC Capital Markets + RBC Capital Markets Arbitrage S.A. (combined)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$2.0M&lt;/td&gt;
&lt;td&gt;RBS Securities, Inc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$1.5M&lt;/td&gt;
&lt;td&gt;Wells Fargo Advisors + Advisors Financial Network + First Clearing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rest&lt;/td&gt;
&lt;td&gt;3.4M&lt;/td&gt;
&lt;td&gt;8 firms including PNC Capital Markets ($0.5M)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After this incident, US major securities firms &lt;strong&gt;audited existing systems for WORM compliance all at once&lt;/strong&gt;, and migration to cloud WORM accelerated. The general retrospective: the post-fine regulatory remediation cost and reputational risk were far larger than the fine itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case ②: Continuity Centers ransomware defense (2020)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Continuity Centers&lt;/strong&gt; is a disaster recovery specialist DRaaS provider. In 2020, against the surge of ransomware, they adopted &lt;strong&gt;Veeam + Backblaze B2 Cloud Storage Object Lock&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The configuration is simple.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F09-continuity-centers.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fworm-mode-deep-dive%2Fdiagrams%2F09-continuity-centers.png" alt="Continuity Centers backup architecture" width="800" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CEO Gregory Tellone's comment captures the essence: "Attackers try to &lt;strong&gt;wipe the customer's data center and backups in one go&lt;/strong&gt;. Immutability is another line of defense."&lt;/p&gt;

&lt;p&gt;A notable point: setup completed &lt;strong&gt;within an hour&lt;/strong&gt; of Backblaze's Object Lock announcement. A symbolic case of the era when cloud WORM setup is one API call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case ③: Hospital + Veeam + StoneFly DR365V
&lt;/h3&gt;

&lt;p&gt;A US public hospital with tens of thousands of patients adopted the following to meet HIPAA's long-term medical record retention requirement plus ransomware defense.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 VMware VMs + TB-scale medical records&lt;/li&gt;
&lt;li&gt;Backup product: Veeam&lt;/li&gt;
&lt;li&gt;Storage destination: &lt;strong&gt;StoneFly DR365V air-gap + WORM volume + S3 Object Lockdown&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three layers of immutability, with tamper-proof copies in local, cloud, and offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case ④: Snowflake GA's WORM Backup in 2025
&lt;/h3&gt;

&lt;p&gt;The data warehouse &lt;strong&gt;Snowflake&lt;/strong&gt; &lt;strong&gt;made WORM Backup generally available in December 2025&lt;/strong&gt;. In addition to Time Travel, you can use external S3 / Azure Blob as a retention-lock-capable WORM store to take tamper-proof backups.&lt;/p&gt;

&lt;p&gt;As Snowflake usage in financial institutions grew, Snowflake itself moved to the side of providing WORM features to meet SEC 17a-4 / FINRA 4511. A clear trend of platform vendors building WORM in as a "standard feature".&lt;/p&gt;

&lt;h3&gt;
  
  
  Case ⑤: Broadcasters / archives (optical disc is still alive)
&lt;/h3&gt;

&lt;p&gt;Even in the cloud era, there is a domain where &lt;strong&gt;optical disc WORM is still alive for long-term archive&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sony's &lt;strong&gt;Optical Disc Archive (ODA)&lt;/strong&gt; is the representative. A cartridge bundles multiple dedicated Blu-ray discs; capacity varies by generation from 1.5 TB to 5.5 TB. &lt;strong&gt;The vendor claims 100-year retention&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Adopted for master storage in broadcasting, video production, and medical imaging&lt;/li&gt;
&lt;li&gt;The decisive properties: "&lt;strong&gt;no power needed once the media is removed&lt;/strong&gt;" and "&lt;strong&gt;not subject to the convenience of firmware or cloud providers&lt;/strong&gt;"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even when cloud WORM is one API call away, the reality is that "&lt;strong&gt;no operator guarantees access for 100 years&lt;/strong&gt;" (no cloud provider signs a 100-year contract), so for ultra-long-term retention, optical disc or LTO tape WORM is still chosen.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Design pitfalls and how to avoid them
&lt;/h2&gt;

&lt;p&gt;We have seen the mechanics and usage of WORM, so let me close with the landmines easy to step on in implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall ①: using Compliance mode lightly
&lt;/h3&gt;

&lt;p&gt;If you use &lt;code&gt;Compliance&lt;/code&gt; mode in dev / staging / learning buckets, the bucket keeps &lt;strong&gt;bloating with undeletable data while billing climbs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For verification, always use &lt;strong&gt;Governance mode&lt;/strong&gt; or &lt;strong&gt;short retention (1 day)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Before production, set an IAM policy / SCP (Service Control Policy) at the organization level that Denies PUT in Compliance mode except for specific IAM Roles&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfall ②: combined with versioning, capacity explodes
&lt;/h3&gt;

&lt;p&gt;Once you enable S3 Object Lock, Versioning is forced ON. If you design with frequent PUTs to the same key, all old versions stay under WORM and cannot be removed.&lt;/p&gt;

&lt;p&gt;Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;Lifecycle &lt;code&gt;NoncurrentVersionExpiration&lt;/code&gt;&lt;/strong&gt; to delete old versions after retention&lt;/li&gt;
&lt;li&gt;Make it an operational rule that &lt;strong&gt;immutable buckets are "PUT once, never overwrite"&lt;/strong&gt; (use date-suffixed keys)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfall ③: deleting the KMS encryption key
&lt;/h3&gt;

&lt;p&gt;If you encrypted WORM objects with SSE-KMS and delete the KMS Key, you land in &lt;strong&gt;data remains, but cannot decrypt&lt;/strong&gt; hell.&lt;/p&gt;

&lt;p&gt;Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operate KMS Keys for WORM buckets as &lt;strong&gt;never delete&lt;/strong&gt; (even with &lt;code&gt;PendingWindowInDays&lt;/code&gt; set to the longest 30 days, operational mistakes happen)&lt;/li&gt;
&lt;li&gt;SCP at the organization level that Denies Key deletion&lt;/li&gt;
&lt;li&gt;If using Multi-Region Keys, consistent management across both regions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfall ④: Legal Hold release permission held by everyone
&lt;/h3&gt;

&lt;p&gt;If retention is strict but &lt;code&gt;s3:PutObjectLegalHold&lt;/code&gt; (release permission) is granted to all IAM Roles, Legal Hold is effectively meaningless.&lt;/p&gt;

&lt;p&gt;Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grant Legal Hold &lt;code&gt;Put&lt;/code&gt; / &lt;code&gt;Delete&lt;/code&gt; permission &lt;strong&gt;only to a dedicated IAM Role for legal / compliance&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Log every &lt;code&gt;PutObjectLegalHold&lt;/code&gt; API call in CloudTrail and stream into SIEM&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfall ⑤: acting on old knowledge that "Object Lock can only be enabled at bucket creation"
&lt;/h3&gt;

&lt;p&gt;For a long time, "&lt;strong&gt;Object Lock can only be turned ON at bucket creation&lt;/strong&gt;" was a constraint. There may still be old project plans in your org that gave up with "cannot change anymore" and migrated to a new bucket.&lt;/p&gt;

&lt;p&gt;But the &lt;strong&gt;November 20, 2023 update&lt;/strong&gt; made it possible to &lt;strong&gt;enable Object Lock self-service on existing buckets&lt;/strong&gt; from the console / API. You do not even need to contact AWS Support. To apply retention to existing objects, &lt;strong&gt;S3 Batch Operations&lt;/strong&gt; can apply in bulk to billions of objects.&lt;/p&gt;

&lt;p&gt;But two caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 Versioning must be ON before enabling&lt;/strong&gt; (Versioning is a prerequisite)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once Object Lock is enabled, it cannot be disabled&lt;/strong&gt;. Versioning can no longer be Suspended either. No going back, so do not enable "just to try"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;WORM is a feature that looks simple on the surface as "data cannot be deleted", but to actually realize it requires the &lt;strong&gt;physical layer / OS layer / firmware layer / app layer / legal layer&lt;/strong&gt; to all click together.&lt;/p&gt;

&lt;p&gt;The takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WORM is an old concept that has existed since the 1980s&lt;/strong&gt;. The implementation evolved from physical media to software to cloud&lt;/li&gt;
&lt;li&gt;Modern cloud WORM is effectively referenced against &lt;strong&gt;S3 Object Lock&lt;/strong&gt;. Azure / GCP provide almost equivalent features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance mode = not deletable even by root&lt;/strong&gt;, &lt;strong&gt;Governance mode = removable with privilege&lt;/strong&gt;. Use Compliance for regulatory production, Governance for internal policy or accident prevention&lt;/li&gt;
&lt;li&gt;WORM demand comes from three sources: &lt;strong&gt;regulation / ransomware defense / insider threat&lt;/strong&gt;. Even non-regulated industries should WORM-ify backups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NetApp SnapLock's ComplianceClock&lt;/strong&gt; is a historic device to defend against clock tampering. In the cloud it is transparently solved internally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conflict with GDPR's right to delete is practically resolved by Crypto-Shredding (destroying the encryption key)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The classic three design pitfalls: "do not use Compliance lightly", "watch out for version explosion", "do not delete KMS keys"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time you design a backup system, always ask first: "&lt;strong&gt;Is this destination immutable?&lt;/strong&gt;". That is the most basic line of defense against both ransomware and audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sec.gov/investment/amendments-electronic-recordkeeping-requirements-broker-dealers" rel="noopener noreferrer"&gt;SEC Rule 17a-4 / amendment notice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html" rel="noopener noreferrer"&gt;AWS S3 Object Lock official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/storage/blobs/immutable-storage-overview" rel="noopener noreferrer"&gt;Azure Immutable Storage Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/storage/docs/bucket-lock" rel="noopener noreferrer"&gt;GCS Bucket Lock official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netapp.com/media/6158-tr4526.pdf" rel="noopener noreferrer"&gt;NetApp SnapLock TR-4526&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.finra.org/media-center/news-releases/2016/finra-fines-12-firms-total-144-million-failing-protect-records" rel="noopener noreferrer"&gt;FINRA Fines 12 Firms a Total of $14.4 Million (2016)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.backblaze.com/cloud-storage/case-studies/continuity-centers" rel="noopener noreferrer"&gt;Continuity Centers + Backblaze Case Study&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.snowflake.com/en/release-notes/2025/other/2025-12-10-worm-backups" rel="noopener noreferrer"&gt;Snowflake WORM Backups GA (2025-12)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>storage</category>
      <category>security</category>
      <category>compliance</category>
      <category>aws</category>
    </item>
    <item>
      <title>AWS S3 Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Mon, 18 May 2026 10:45:59 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-s3-deep-dive-17f3</link>
      <guid>https://dev.to/kanywst/aws-s3-deep-dive-17f3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;S3 (Simple Storage Service) is one of the oldest (launched in 2006) and most-used services in AWS. "Near-infinite storage where you can put and get files over an HTTPS API" sounds simple, but in real operation it keeps showing up in incident news because of the &lt;strong&gt;complexity of its access control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every year you see headlines like "S3 bucket left public, customer PII leaked." That isn't because S3 is broken. It's because &lt;strong&gt;almost nobody fully understands all 4 kinds of access control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article dissects S3 in the following order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The S3 object model: the true structure of buckets, objects, and keys&lt;/li&gt;
&lt;li&gt;Following a single round-trip: what happens inside S3 between receiving and returning&lt;/li&gt;
&lt;li&gt;The 4 layers of access control: IAM / Bucket Policy / ACL / Block Public Access&lt;/li&gt;
&lt;li&gt;The evaluation flowchart: who actually gets access in the end&lt;/li&gt;
&lt;li&gt;The 4 forms of encryption: SSE-S3 / SSE-KMS / SSE-C / CSE&lt;/li&gt;
&lt;li&gt;Features you lose by not knowing: Versioning / MFA Delete / Object Lock / Access Point&lt;/li&gt;
&lt;li&gt;A list of configurations you must never use&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. The S3 Object Model
&lt;/h2&gt;

&lt;p&gt;S3 is not a filesystem. It is &lt;strong&gt;object storage&lt;/strong&gt;. This is the first source of confusion.&lt;/p&gt;

&lt;p&gt;Object storage is a mechanism where data + a unique ID (the key) + metadata are bundled into one unit (an object) and &lt;strong&gt;put and got from a flat namespace&lt;/strong&gt;. There is no directory hierarchy, no file permission bits, no blocks. If you think of it not as a "file" but as "an item retrievable by ID," it starts to make sense.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fis1u5urb0jh1r8wyjlp2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fis1u5urb0jh1r8wyjlp2.png" alt="S3 object model" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Bucket&lt;/strong&gt; is a "container" scoped to an account + Region. Its name is &lt;strong&gt;globally unique across all of AWS&lt;/strong&gt;, so the name &lt;code&gt;my-app-logs&lt;/code&gt; exists in exactly one place in the world.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Object&lt;/strong&gt; is a single piece of data inside a bucket. It is identified by a &lt;strong&gt;Key&lt;/strong&gt; like &lt;code&gt;2026/05/14/app.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The key "looks like" a file path, but &lt;strong&gt;it is just a string&lt;/strong&gt;. The &lt;code&gt;/&lt;/code&gt; is just a character. Directories do not exist.&lt;/li&gt;
&lt;li&gt;Each object consists of &lt;strong&gt;data + metadata + (optional) tags&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A bucket is tied to a Region, but &lt;strong&gt;only the bucket name namespace is global&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  "Folders" Are an Illusion
&lt;/h3&gt;

&lt;p&gt;When you click &lt;code&gt;2026/05/&lt;/code&gt; in the S3 console and "see its contents," that is &lt;strong&gt;just a prefix search on keys&lt;/strong&gt;, not a folder. To fetch the contents of &lt;code&gt;2026/05/14/app.log&lt;/code&gt;, you do not need to first fetch a parent object called &lt;code&gt;2026/&lt;/code&gt; or &lt;code&gt;2026/05/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you do not know this, you end up doing things like uploading a 0-byte object named &lt;code&gt;2026/06/&lt;/code&gt; because "I want to create an empty folder in S3," which is a console-driven misconception.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memorize the ARN Format
&lt;/h3&gt;

&lt;p&gt;You always need this when pointing to S3 (or any AWS resource) from an IAM policy. ARN stands for &lt;strong&gt;Amazon Resource Name&lt;/strong&gt; and is the scheme AWS uses to refer to any resource by a globally unique string. The base form is this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:partition:service:region:account-id:resource
       │        │       │        │          │
       │        │       │        │          └─ Resource ID (bucket name or object key)
       │        │       │        └────────── 12-digit AWS account number
       │        │       └─────────────────── Region (e.g. ap-northeast-1)
       │        └─────────────────────────── Service name (e.g. s3, ec2, iam)
       └──────────────────────────────────── Partition (usually aws, China is aws-cn)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 takes the special form of &lt;strong&gt;leaving &lt;code&gt;region&lt;/code&gt; and &lt;code&gt;account-id&lt;/code&gt; blank&lt;/strong&gt; (which is why you see three consecutive colons &lt;code&gt;:::&lt;/code&gt;), due to "globally unique bucket names" and a "Region-independent billing model."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;ARN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The whole bucket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::my-app-logs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All objects in the bucket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::my-app-logs/*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A specific object&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::my-app-logs/2026/05/14/app.log&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything under a specific prefix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::my-app-logs/users/alice/*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point is that &lt;strong&gt;a bucket and its objects have different ARNs&lt;/strong&gt;. &lt;code&gt;s3:ListBucket&lt;/code&gt; applies to the bucket ARN and &lt;code&gt;s3:GetObject&lt;/code&gt; applies to the object ARN, so you need to write both to get listing and reading working together.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Following a Single Round-Trip
&lt;/h2&gt;

&lt;p&gt;When a client runs &lt;code&gt;aws s3 cp ./image.png s3://my-app-uploads/users/alice/avatar.png&lt;/code&gt;, what happens inside S3?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-s3-deep-dive%2Fdiagrams%2F02-request-roundtrip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Faws-s3-deep-dive%2Fdiagrams%2F02-request-roundtrip.png" alt="S3 request round-trip" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you need to recognize here is that &lt;strong&gt;the inputs S3 uses to decide authorization are all 4 of IAM / Bucket Policy / ACL / Block Public Access&lt;/strong&gt;. Looking at just one of them does not tell you the real permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The 4 Layers of Access Control
&lt;/h2&gt;

&lt;p&gt;This is the single biggest source of confusion in S3. Access control is decided by 4 layers simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx1hbrk56zy37mftmkc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx1hbrk56zy37mftmkc8.png" alt="Access control layers" width="800" height="1346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer ①: Block Public Access (BPA)
&lt;/h3&gt;

&lt;p&gt;The newest, strongest safety net, and &lt;strong&gt;the first thing evaluated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since April 2023, AWS has &lt;strong&gt;enabled it by default on new buckets&lt;/strong&gt; and also disabled ACL by default. BPA consists of 4 switches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BPA setting&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BlockPublicAcls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevent &lt;strong&gt;newly attaching&lt;/strong&gt; a public ACL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IgnorePublicAcls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ignore all&lt;/strong&gt; public ACLs (including existing ones)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BlockPublicPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevent &lt;strong&gt;attaching&lt;/strong&gt; a public bucket policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RestrictPublicBuckets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Completely block unauthenticated access&lt;/strong&gt; to public buckets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;All 4 ON is the rule&lt;/strong&gt;. As long as you do this, "I accidentally made it public" incidents are almost entirely prevented.&lt;/p&gt;

&lt;p&gt;You can also &lt;strong&gt;enforce BPA at the Organizations level&lt;/strong&gt; (Organization-level BPA). Once set, individual accounts can no longer disable BPA themselves. If you use AWS at a company, turn this on without exception.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer ②: IAM Identity Policy
&lt;/h3&gt;

&lt;p&gt;The IAM policy type that is &lt;strong&gt;attached to the operating side (user / role)&lt;/strong&gt;. It writes "what this principal can do."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-app-uploads/users/${aws:username}/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an IAM policy that says "you can only read the subfolder named after your own username." Dynamic variables like &lt;code&gt;${aws:username}&lt;/code&gt; are useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer ③: Bucket Policy
&lt;/h3&gt;

&lt;p&gt;A Resource Policy attached to the &lt;strong&gt;bucket side&lt;/strong&gt;. It writes "who can access this bucket."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyInsecureTransport"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-app-uploads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-app-uploads/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aws:SecureTransport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowFromMyOrg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-app-uploads/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalOrgID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"o-abc1234567"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Denying HTTP (non-TLS) access&lt;/strong&gt; is a standard template.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; to Condition lets you ensure &lt;strong&gt;only people/Roles in your own AWS Organizations can access&lt;/strong&gt;. This is a structural defense that prevents public-exposure incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-account sharing is done with the bucket policy&lt;/strong&gt;. IAM alone will not allow cross-account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer ④: ACL (Access Control List)
&lt;/h3&gt;

&lt;p&gt;The oldest access control in S3. Today, &lt;strong&gt;you do not need to use it&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Since April 2023, &lt;strong&gt;ACL is disabled by default on new buckets&lt;/strong&gt; (Object Ownership = "Bucket owner enforced").&lt;/li&gt;
&lt;li&gt;Some legacy integrations (such as CloudFront logs) still require it, but even then keep it minimal.&lt;/li&gt;
&lt;li&gt;For existing buckets too, &lt;strong&gt;prefer disabling ACL&lt;/strong&gt; when possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of "S3 public-exposure incidents" were caused by giving Read to the ACL's &lt;code&gt;AllUsers&lt;/code&gt; group (= the entire internet). BPA + disabled ACL is the mechanism that closes that old wound.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Who Actually Gets Access: The Evaluation Flowchart
&lt;/h2&gt;

&lt;p&gt;Given the 4 layers, here is the full flow of whether S3 lets a request through.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuplch1rh3te9l4bw08bd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuplch1rh3te9l4bw08bd.png" alt="Authorization evaluation flowchart" width="800" height="950"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The principles to internalize.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;BPA wins everything&lt;/strong&gt;: if it judges the request as public, it is over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An explicit Deny wherever it appears wins&lt;/strong&gt;: if there is a Deny in IAM, Bucket Policy, or SCP, you are done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same account: Allow from either IAM or Bucket Policy is enough&lt;/strong&gt; (OR).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-account: Allow is required in both IAM and Bucket Policy&lt;/strong&gt; (AND).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cross-account sharing often gets stuck on point 4. When someone says "I can't access our bucket from a Role in another account," the first thing to check is &lt;strong&gt;whether Allow is written on both sides&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The 4 Forms of Encryption
&lt;/h2&gt;

&lt;p&gt;As of 2026, &lt;strong&gt;all objects in S3 are automatically encrypted&lt;/strong&gt; by default. That said, there are 4 variations on whose key does the encryption.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j6z81pwbhk0g8cwljvy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j6z81pwbhk0g8cwljvy.png" alt="Encryption forms" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Their characteristics.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Key management&lt;/th&gt;
&lt;th&gt;Key-use log&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSE-S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully managed by S3&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;You want forced encryption but want to avoid KMS cost and management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSE-KMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS KMS (Customer Managed Key)&lt;/td&gt;
&lt;td&gt;Recorded in CloudTrail&lt;/td&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;Required when you need auditing, separation of duties, or compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSE-C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client sends a key on each request&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;Only for special cases where you cannot hand the key to AWS. Disabled by default on new buckets from April 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully managed by the client&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;For ultra-high requirements where you cannot let AWS see plaintext at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How to Choose in Practice
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8aqa5owng97lzv9yp3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8aqa5owng97lzv9yp3t.png" alt="Encryption choice flowchart" width="800" height="1115"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you do not want to think about it, pick &lt;strong&gt;SSE-KMS with a Customer Managed Key (CMK)&lt;/strong&gt;. Key-use logs go into CloudTrail, KMS key policy lets you enforce "only this role can decrypt," and you can replicate keys to other Regions. The only downside is that the KMS API call charges slowly add up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce Default Encryption on the Bucket
&lt;/h3&gt;

&lt;p&gt;When you want to enforce encryption on new objects, set the bucket-level &lt;strong&gt;Default encryption&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ApplyServerSideEncryptionByDefault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"SSEAlgorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"KMSMasterKeyID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:kms:ap-northeast-1:123456789012:key/abc-..."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"BucketKeyEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BucketKeyEnabled: true&lt;/code&gt; dramatically reduces the number of KMS API calls and saves cost, so turn it on whenever you use KMS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket Policy That Rejects "Unencrypted PUT"
&lt;/h3&gt;

&lt;p&gt;Putting this in as a guardrail rescues you with default encryption even when someone misuses the SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyUnEncryptedObjectUploads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-app-uploads/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"s3:x-amz-server-side-encryption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Features You Lose by Not Knowing
&lt;/h2&gt;

&lt;p&gt;Features that elevate S3 beyond "just an object store."&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioning and MFA Delete
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzigrtcpbbvavl5wl2cj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzigrtcpbbvavl5wl2cj2.png" alt="Versioning and delete markers" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turning &lt;strong&gt;Versioning&lt;/strong&gt; on keeps older versions even when you overwrite the same key.&lt;/li&gt;
&lt;li&gt;Deletion just stacks a &lt;strong&gt;Delete Marker&lt;/strong&gt;, leaving the actual data intact.&lt;/li&gt;
&lt;li&gt;Combining &lt;strong&gt;MFA Delete&lt;/strong&gt; requires the Root account's MFA for permanent deletion. Even if ransomware encrypts your bucket, you can roll back from an older version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Object Lock
&lt;/h3&gt;

&lt;p&gt;A WORM (Write Once Read Many) mode that disallows deletion and overwriting for a fixed period after a write.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Governance mode&lt;/strong&gt;: privileged users can still delete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance mode&lt;/strong&gt;: nobody (not even Root) can delete until expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it for data that must not be deleted, such as legal records (SEC, FINRA, HIPAA).&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Point
&lt;/h3&gt;

&lt;p&gt;A feature that "grows multiple purpose-specific entry points on a single bucket." For one bucket &lt;code&gt;company-data&lt;/code&gt; you can create Access Points like &lt;code&gt;marketing-readonly&lt;/code&gt; (only the marketing IAM can Get), &lt;code&gt;finance-readwrite&lt;/code&gt; (the finance IAM can Get/Put), &lt;code&gt;public-cdn&lt;/code&gt; (only via CloudFront), each with its own independent Access Point Policy.&lt;/p&gt;

&lt;p&gt;Before your Bucket Policy bloats into something unreadable, splitting it into Access Points keeps things organized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-signed URL
&lt;/h3&gt;

&lt;p&gt;A feature that issues a short-lived signed URL. Handy when you want to &lt;strong&gt;let an unauthenticated user temporarily download or upload&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example: "let a customer download an invoice PDF for just 1 hour" → issue a Pre-signed URL and email it.&lt;/li&gt;
&lt;li&gt;The bucket stays completely closed via BPA. Per-request access is allowed by the URL's expiration and signature.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. List of Configurations You Must Never Use
&lt;/h2&gt;

&lt;p&gt;Given everything above, here are anti-patterns and best practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;❌ Do not do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn BPA OFF&lt;/li&gt;
&lt;li&gt;Grant Read to &lt;code&gt;AllUsers&lt;/code&gt; / &lt;code&gt;AuthenticatedUsers&lt;/code&gt; via ACL&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Principal: *&lt;/code&gt; in Bucket Policy without any Condition&lt;/li&gt;
&lt;li&gt;Store important data without Versioning&lt;/li&gt;
&lt;li&gt;Allow HTTP (non-TLS) access&lt;/li&gt;
&lt;li&gt;Embed long-lived IAM User credentials in an application&lt;/li&gt;
&lt;li&gt;Casually grab a globally unique bucket name and operate it with public settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;✅ Do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All 4 BPA switches ON / enforced at the organization level&lt;/li&gt;
&lt;li&gt;Disable ACL (Object Ownership = Bucket owner enforced)&lt;/li&gt;
&lt;li&gt;Enforce &lt;code&gt;aws:SecureTransport&lt;/code&gt; in Bucket Policy&lt;/li&gt;
&lt;li&gt;Fence in with &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; in Bucket Policy&lt;/li&gt;
&lt;li&gt;Versioning + MFA Delete&lt;/li&gt;
&lt;li&gt;SSE-KMS + Bucket Key for both auditability and low cost&lt;/li&gt;
&lt;li&gt;Use Pre-signed URLs as a substitute for temporary public exposure&lt;/li&gt;
&lt;li&gt;For distribution, completely privatize the bucket with CloudFront + OAC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one operational pattern worth emphasizing especially.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"I want to expose this to the internet" does not mean "make the bucket public."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to publish a static site or image distribution, keep the bucket fully private and serve it through a CDN with &lt;strong&gt;CloudFront + OAC (Origin Access Control)&lt;/strong&gt;. This achieves "direct bucket access is blocked, only the CDN can fetch from it." There is zero need to disable BPA.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;S3 is an object store of Bucket + Object + Key. Folders are an illusion, &lt;code&gt;/&lt;/code&gt; is just a character.&lt;/li&gt;
&lt;li&gt;Bucket names are globally unique. The ARN of the bucket and of its objects are different.&lt;/li&gt;
&lt;li&gt;Access control has 4 layers: BPA / IAM / Bucket Policy / ACL.&lt;/li&gt;
&lt;li&gt;Evaluation principles: BPA wins everything, explicit Deny wins, same account is OR, cross-account is AND.&lt;/li&gt;
&lt;li&gt;Encryption comes in 4 flavors: SSE-S3 / SSE-KMS / SSE-C / CSE. Today, SSE-KMS + Bucket Key is the standard.&lt;/li&gt;
&lt;li&gt;All 4 BPA switches ON, ACL disabled, HTTPS enforced, scoped by &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt;: public-exposure incidents drop dramatically.&lt;/li&gt;
&lt;li&gt;For public distribution use CloudFront + OAC. For temporary sharing use Pre-signed URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/features/block-public-access/" rel="noopener noreferrer"&gt;Amazon S3 Block Public Access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" rel="noopener noreferrer"&gt;Blocking public access to your Amazon S3 storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html" rel="noopener noreferrer"&gt;Examples of Amazon S3 bucket policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html" rel="noopener noreferrer"&gt;Protecting data with server-side encryption&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/storage/advanced-notice-amazon-s3-to-disable-the-use-of-sse-c-encryption-by-default-for-all-new-buckets-and-select-existing-buckets-in-april-2026/" rel="noopener noreferrer"&gt;Advanced notice: Amazon S3 to disable SSE-C by default for new buckets in April 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>security</category>
      <category>storage</category>
    </item>
    <item>
      <title>AWS IAM Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Sun, 17 May 2026 07:10:19 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-iam-deep-dive-2b81</link>
      <guid>https://dev.to/kanywst/aws-iam-deep-dive-2b81</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every action on AWS goes through an HTTPS API, and &lt;strong&gt;IAM (Identity and Access Management) sits in front of every single one of them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once you actually run things on AWS, you notice IAM is where you get stuck. "It says AccessDenied." "The policy says Allow, why is it still rejected?" "I assumed the role but my credentials are still the old ones." The patterns are predictable.&lt;/p&gt;

&lt;p&gt;This article takes IAM apart in the order auth happens: authentication, then authorization, then operations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What IAM actually solves: authentication vs authorization&lt;/li&gt;
&lt;li&gt;Principals: who is making the call&lt;/li&gt;
&lt;li&gt;SigV4: how AWS verifies the caller is real&lt;/li&gt;
&lt;li&gt;The six policy types and what each one does&lt;/li&gt;
&lt;li&gt;The shape of a policy JSON&lt;/li&gt;
&lt;li&gt;Policy evaluation: why Deny beats Allow&lt;/li&gt;
&lt;li&gt;IAM Identity Center and short-lived credentials&lt;/li&gt;
&lt;li&gt;The do / don't list&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. What IAM Solves: Authentication and Authorization
&lt;/h2&gt;

&lt;p&gt;Start by separating authentication (AuthN) from authorization (AuthZ).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4gr6ja98juqpw2fvn2kn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4gr6ja98juqpw2fvn2kn.png" alt="AuthN and AuthZ overview" width="800" height="1518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication (AuthN)&lt;/strong&gt;: pins down "who are you". On AWS, the caller is &lt;strong&gt;whoever owns the credentials that produced this SigV4 signature&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization (AuthZ)&lt;/strong&gt;: decides "can they do this". AWS answers it by &lt;strong&gt;combining multiple policies&lt;/strong&gt; and evaluating them together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the article follows that same order: AuthN first, then AuthZ.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Principals: Who Is Calling AWS
&lt;/h2&gt;

&lt;p&gt;In AWS, the entity making a call is called a &lt;strong&gt;Principal&lt;/strong&gt;. Anything that can execute against a resource.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;th&gt;Authenticated by&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Root User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Account email + password&lt;/td&gt;
&lt;td&gt;AWS itself&lt;/td&gt;
&lt;td&gt;Almost never. Billing changes and a few special tasks only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Access key (long-lived) or password&lt;/td&gt;
&lt;td&gt;IAM&lt;/td&gt;
&lt;td&gt;One per human or system. &lt;strong&gt;Long-lived keys are a leak risk.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short-lived credentials issued by STS&lt;/td&gt;
&lt;td&gt;AssumeRole&lt;/td&gt;
&lt;td&gt;The default. Roles over Users in modern setups.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Federated Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;External IdP token, swapped for AWS creds via STS&lt;/td&gt;
&lt;td&gt;Identity Center / SAML / OIDC&lt;/td&gt;
&lt;td&gt;The modern way humans log in.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An IAM Group is not a principal&lt;/strong&gt;. It is just a container for attaching policies to a set of users. The group itself never authenticates and you cannot put it in the &lt;code&gt;Principal&lt;/code&gt; field of a policy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Don't Use the Root User
&lt;/h3&gt;

&lt;p&gt;Root has &lt;strong&gt;god-mode on the whole account&lt;/strong&gt;. Only Root can change billing or close the account, and that is exactly why everything else should not be done as Root.&lt;/p&gt;

&lt;p&gt;The AWS-recommended setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After creating the account, &lt;strong&gt;set up MFA on Root immediately&lt;/strong&gt; (basically required now).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never create an access key for Root.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;For day-to-day work, log in through IAM Identity Center or assume an IAM Role.&lt;/li&gt;
&lt;li&gt;Lock the Root credentials in a safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For MFA, &lt;strong&gt;TOTP&lt;/strong&gt; (the 6-digit code from Google Authenticator, 1Password, etc.) works, but AWS recommends &lt;strong&gt;FIDO2 / WebAuthn&lt;/strong&gt; passkeys or hardware keys like a YubiKey, because they resist phishing. AWS also recommends &lt;strong&gt;registering at least two MFA methods on Root&lt;/strong&gt; so you don't get locked out if one is lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use IAM Roles Instead of IAM Users
&lt;/h3&gt;

&lt;p&gt;The access keys on an IAM User (the ones that start with &lt;code&gt;AKIA...&lt;/code&gt;) are &lt;strong&gt;long-lived&lt;/strong&gt;. Once leaked, they stay valid until you rotate them. The stream of "committed an access key to GitHub, got abused within hours" stories isn't slowing down.&lt;/p&gt;

&lt;p&gt;IAM Roles solve this by &lt;strong&gt;handing out short-lived credentials via AssumeRole&lt;/strong&gt; that expire after 1 hour by default (12 hours max).&lt;/p&gt;

&lt;p&gt;Three pieces to know before reading the diagram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STS (Security Token Service)&lt;/strong&gt;: the AWS service that issues temporary credentials. &lt;code&gt;sts:AssumeRole&lt;/code&gt; and friends call into this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust Policy&lt;/strong&gt;: a policy attached to a Role that says &lt;strong&gt;"who is allowed to assume this role"&lt;/strong&gt; (e.g. "only this specific IAM User", "only this GitHub Actions repo").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary credentials&lt;/strong&gt;: a triple of &lt;code&gt;AccessKeyId&lt;/code&gt;, &lt;code&gt;SecretAccessKey&lt;/code&gt;, and &lt;code&gt;SessionToken&lt;/code&gt;, with an expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4odlimzz6b9s8osn7qi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4odlimzz6b9s8osn7qi.png" alt="AssumeRole sequence" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. SigV4: How AWS Verifies a Request Is Real
&lt;/h2&gt;

&lt;p&gt;Once we know who the principal is, the next question is whether they actually signed the request. That is what &lt;strong&gt;SigV4 (Signature Version 4)&lt;/strong&gt; does.&lt;/p&gt;

&lt;p&gt;A typical REST API sends something like &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; on every call. AWS does not. It &lt;strong&gt;never sends the Secret Key on the wire&lt;/strong&gt;. It sends a signature computed from the Secret Key, every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Big Picture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F588b9fyb8eete1kdv34a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F588b9fyb8eete1kdv34a.png" alt="SigV4 signing sequence" width="800" height="914"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the 4 Steps Actually Do
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c3ke30c2jzqh4df8mgy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c3ke30c2jzqh4df8mgy.png" alt="SigV4 four steps" width="800" height="1154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things to internalize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Secret Key never leaves your machine.&lt;/strong&gt; Only an HMAC derived from it goes on the wire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clock skew will kill you.&lt;/strong&gt; The timestamp is baked into the signature scope. &lt;strong&gt;If your laptop's clock is off, you get InvalidSignature immediately.&lt;/strong&gt; AWS tolerates roughly 15 minutes of skew.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signatures have a short validity window.&lt;/strong&gt; To shrink the replay window, API calls only accept signatures from the last 15 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In practice the SDK and CLI do all of this for you, so you almost never compute it by hand. But when you see "calling another region from inside Lambda fails with a signature error" or "every S3 call from this Docker container returns 403", the answer is almost always clock sync.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Six Policy Types and Who Does What
&lt;/h2&gt;

&lt;p&gt;Now for authorization. The reason AWS authorization feels complicated is that &lt;strong&gt;six different policy types are evaluated together&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Attached to&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;How often used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Identity Policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User / Group / Role&lt;/td&gt;
&lt;td&gt;"What can this principal do?"&lt;/td&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource Policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3 bucket, KMS Key, SNS, etc.&lt;/td&gt;
&lt;td&gt;"Who can touch this resource?"&lt;/td&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SCP (Service Control Policy)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OU / Account (Organizations)&lt;/td&gt;
&lt;td&gt;Per-account ceiling (a guardrail)&lt;/td&gt;
&lt;td&gt;★★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Permissions Boundary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User / Role&lt;/td&gt;
&lt;td&gt;Per-principal ceiling&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session Policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Args to AssumeRole / GetFederationToken&lt;/td&gt;
&lt;td&gt;Extra narrowing that only applies in a session&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RCP (Resource Control Policy)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Organizations&lt;/td&gt;
&lt;td&gt;Org-wide guardrail on resources&lt;/td&gt;
&lt;td&gt;Newer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trick is to keep "ceiling" and "permission" separate in your head. &lt;strong&gt;Identity Policy is the "what you may do" list. SCP and Permissions Boundary are the "the most you will ever be allowed to do" list.&lt;/strong&gt; No Allow anywhere means no access; a Deny in the ceiling kills it for sure.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Shape of a Policy JSON
&lt;/h2&gt;

&lt;p&gt;Every policy uses the same JSON structure, with five elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowReadOnlyMyBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"IpAddress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceIp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:MultiFactorAuthPresent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Effect&lt;/strong&gt;: only &lt;code&gt;Allow&lt;/code&gt; or &lt;code&gt;Deny&lt;/code&gt;. Default is &lt;code&gt;Deny&lt;/code&gt; (if nothing says Allow, you get denied).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: &lt;code&gt;s3:GetObject&lt;/code&gt; style, in &lt;code&gt;&amp;lt;service prefix&amp;gt;:&amp;lt;API name&amp;gt;&lt;/code&gt; form. Wildcards (&lt;code&gt;s3:*&lt;/code&gt;, &lt;code&gt;s3:Get*&lt;/code&gt;) work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource&lt;/strong&gt;: the target, expressed as an &lt;strong&gt;ARN (Amazon Resource Name)&lt;/strong&gt;, e.g. &lt;code&gt;arn:aws:s3:::my-bucket/*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Principal&lt;/strong&gt;: "who". Not needed in an Identity Policy because the attachment target is implied. &lt;strong&gt;Required in a Resource Policy.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition&lt;/strong&gt;: extra constraints like IP, MFA, tags, time, VPC endpoint, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Condition Is Where Real IAM Lives
&lt;/h3&gt;

&lt;p&gt;Once you start writing IAM seriously, you live inside &lt;code&gt;Condition&lt;/code&gt;. The ones I reach for most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:SourceIp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source IP&lt;/td&gt;
&lt;td&gt;Allow only from the office IP block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Did the caller use MFA?&lt;/td&gt;
&lt;td&gt;Require MFA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:PrincipalTag/Team&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tag on the principal&lt;/td&gt;
&lt;td&gt;If Team = ml, allow only ml-bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s3:prefix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prefix being listed in S3&lt;/td&gt;
&lt;td&gt;A user can only &lt;code&gt;ls&lt;/code&gt; their own folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:RequestedRegion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target region&lt;/td&gt;
&lt;td&gt;Deny anything outside ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws:SecureTransport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTPS?&lt;/td&gt;
&lt;td&gt;Block plaintext HTTP access to S3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Policy Evaluation: Deny Beats Allow
&lt;/h2&gt;

&lt;p&gt;AWS evaluates the six policy types &lt;strong&gt;in this order&lt;/strong&gt; to reach a final decision.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcualbitro7o2gb24lmvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcualbitro7o2gb24lmvt.png" alt="Policy evaluation flow" width="800" height="1096"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Note: this is the simplified, same-account version. Cross-account access has extra rules, e.g. the Resource Policy Allow is required, but the picture above is enough to grasp the principle.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Iron Rules
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default is Deny.&lt;/strong&gt; If nothing says Allow, the answer is no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An explicit Deny overrides an explicit Allow.&lt;/strong&gt; A single Deny anywhere ends the discussion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need at least one Allow.&lt;/strong&gt; No Allow = denied.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without these three, you cannot debug "the policy says Allow but I still get AccessDenied".&lt;/p&gt;

&lt;h3&gt;
  
  
  Common "Why Am I Denied?" Patterns
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity Policy says Allow, still Deny&lt;/td&gt;
&lt;td&gt;An SCP is blocking that service at the org level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't access your own bucket&lt;/td&gt;
&lt;td&gt;The bucket policy names a different Principal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost permissions after AssumeRole&lt;/td&gt;
&lt;td&gt;A Session Policy was passed as an argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only some actions get Deny'd&lt;/td&gt;
&lt;td&gt;A Permissions Boundary is narrowing via wildcards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Denied in only one region&lt;/td&gt;
&lt;td&gt;An SCP or IAM Policy has a Region condition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why SCPs Are Powerful
&lt;/h3&gt;

&lt;p&gt;An SCP (Service Control Policy) is an AWS Organizations feature that &lt;strong&gt;caps what an OU or account is allowed to do&lt;/strong&gt;. Attach this SCP to a Sandbox OU and accounts under it cannot launch EC2 outside Tokyo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyOutsideTokyo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RunInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:RequestedRegion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-northeast-1"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if an IAM policy inside the account allows RunInstances in every region, &lt;strong&gt;the SCP Deny wins&lt;/strong&gt;. That is what a guardrail buys you.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. IAM Identity Center and Short-Lived Credentials
&lt;/h2&gt;

&lt;p&gt;The era of humans logging in as IAM Users is over. The standard now is &lt;strong&gt;IAM Identity Center&lt;/strong&gt; (formerly AWS SSO).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffheltelfck24t9oqacdy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffheltelfck24t9oqacdy.png" alt="IAM Identity Center topology" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user logs in &lt;strong&gt;once via the company IdP (Google / Okta / Entra)&lt;/strong&gt;, and &lt;strong&gt;AssumeRole gives them short-lived credentials&lt;/strong&gt; into every AWS account under it.&lt;/li&gt;
&lt;li&gt;The contents of the role are defined as a &lt;strong&gt;Permission Set&lt;/strong&gt;. Build a few of them ("ReadOnly is this, dev is that") and assign per user × account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No access keys are ever issued&lt;/strong&gt;, so leak risk goes away structurally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws sso login&lt;/code&gt; covers the CLI side, dropping a temp token in &lt;code&gt;~/.aws/sso/cache&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For EC2 / Lambda / GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Non-human callers (apps, CI) should also &lt;strong&gt;not use long-lived keys&lt;/strong&gt;. The current playbook:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Authentication&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;td&gt;Instance Profile (attach a Role directly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;td&gt;Execution Role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS / EKS&lt;/td&gt;
&lt;td&gt;Task Role / IRSA / Pod Identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;OIDC, then AssumeRoleWithWebIdentity into an AWS Role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab CI&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External Kubernetes&lt;/td&gt;
&lt;td&gt;IAM Roles for Service Accounts (IRSA)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions OIDC&lt;/strong&gt; is the strongest of the bunch: you can scope AssumeRole by repository or branch. Long-lived keys in GitHub Secrets become entirely unnecessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. The Do / Don't List
&lt;/h2&gt;

&lt;p&gt;Tying the theory back to practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;❌ Don't&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an access key for the Root user&lt;/li&gt;
&lt;li&gt;Do day-to-day work as Root&lt;/li&gt;
&lt;li&gt;Commit IAM User long-lived keys to GitHub&lt;/li&gt;
&lt;li&gt;Hand out wildcards like &lt;code&gt;s3:*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Principal: *&lt;/code&gt; in a Resource Policy without a Condition&lt;/li&gt;
&lt;li&gt;Mix production and staging in the same account&lt;/li&gt;
&lt;li&gt;Skip MFA entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;✅ Do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put MFA on Root and lock the credentials away&lt;/li&gt;
&lt;li&gt;Move humans onto SSO via IAM Identity Center&lt;/li&gt;
&lt;li&gt;Have GitHub Actions assume a Role via OIDC&lt;/li&gt;
&lt;li&gt;Cap developer permissions with a Permissions Boundary&lt;/li&gt;
&lt;li&gt;Set an org-wide guardrail with an SCP&lt;/li&gt;
&lt;li&gt;Apply least privilege and audit with IAM Access Analyzer&lt;/li&gt;
&lt;li&gt;Turn CloudTrail on in every account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three of these are worth calling out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat long-lived access keys as something you will eliminate in the next few years.&lt;/strong&gt; Roles + STS + Identity Center can cover the same ground.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be suspicious of &lt;code&gt;*&lt;/code&gt;.&lt;/strong&gt; Action &lt;code&gt;*&lt;/code&gt;, Resource &lt;code&gt;*&lt;/code&gt;, Principal &lt;code&gt;*&lt;/code&gt;, no Condition: that combination is how incidents happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail and IAM Access Analyzer must be on.&lt;/strong&gt; If you can't reconstruct "who did what" after the fact, an incident becomes unsolvable.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IAM handles both authentication (who?) and authorization (allowed to?).&lt;/li&gt;
&lt;li&gt;Principals come in four kinds: Root, User, Role, Federated. Groups are containers, not principals.&lt;/li&gt;
&lt;li&gt;AuthN uses SigV4: the Secret Key never leaves you, clock drift is fatal, signatures live for 15 minutes.&lt;/li&gt;
&lt;li&gt;There are six policy types (Identity / Resource / SCP / Permissions Boundary / Session / RCP) and they are all combined.&lt;/li&gt;
&lt;li&gt;Evaluation rules: default Deny, explicit Deny beats explicit Allow, you need at least one Allow.&lt;/li&gt;
&lt;li&gt;Modern best practice is Identity Center + Permission Sets + OIDC-based role assumption.&lt;/li&gt;
&lt;li&gt;Long-lived keys are game over once leaked. Replace them with Role + STS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html" rel="noopener noreferrer"&gt;Policy evaluation logic: AWS IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic_AccessPolicyLanguage_Interplay.html" rel="noopener noreferrer"&gt;The difference between explicit and implicit denies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html" rel="noopener noreferrer"&gt;AWS Signature Version 4 for API requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-troubleshooting.html" rel="noopener noreferrer"&gt;Troubleshoot Signature Version 4 signing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/root-user-best-practices.html" rel="noopener noreferrer"&gt;Root user best practices for your AWS account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html" rel="noopener noreferrer"&gt;Service control policies (SCPs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html" rel="noopener noreferrer"&gt;Security best practices in IAM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>iam</category>
      <category>security</category>
      <category>authentication</category>
    </item>
    <item>
      <title>AWS Free Hands-On</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Fri, 15 May 2026 14:38:54 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-free-hands-on-build-s3-lambda-dynamodb-on-the-always-free-tier-plus-iam-cloudwatch-and-34le</link>
      <guid>https://dev.to/kanywst/aws-free-hands-on-build-s3-lambda-dynamodb-on-the-always-free-tier-plus-iam-cloudwatch-and-34le</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is for the "I want to try AWS but I'm scared of the bill" and "I made an account once and never did anything with it" crowd. By the end of the next 30 minutes you'll have something running, and you'll have actually touched the parts of AWS that matter.&lt;/p&gt;

&lt;p&gt;The finished pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub7qur5j0um48o4hwgqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub7qur5j0um48o4hwgqp.png" alt="finished pipeline" width="800" height="99"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you touch by building it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt;: the cloud object store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: a serverless runtime where you just put code and it runs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt;: managed NoSQL database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM&lt;/strong&gt;: permission management for everything (the hidden main character of this article)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch&lt;/strong&gt;: logs, metrics, alarms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Budgets&lt;/strong&gt;: monthly cost alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CLI&lt;/strong&gt;: the command-line interface for talking to AWS from your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And &lt;strong&gt;the core stays inside the Always Free tier&lt;/strong&gt;. The S3 usage in this hands-on is tiny (a few dozen bytes, a handful of requests), so even if S3 isn't strictly "Always Free" for your account type, the credit consumption is effectively $0. No surprise charges 6 months from now either.&lt;/p&gt;

&lt;p&gt;The plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The 2026 AWS free tier, what it actually is (misread this and you get charged)&lt;/li&gt;
&lt;li&gt;Account creation and four safety setups for the first 10 minutes&lt;/li&gt;
&lt;li&gt;Install AWS CLI locally&lt;/li&gt;
&lt;li&gt;Create an S3 bucket and put something in it&lt;/li&gt;
&lt;li&gt;Create a DynamoDB table&lt;/li&gt;
&lt;li&gt;Prepare an IAM Role for Lambda&lt;/li&gt;
&lt;li&gt;Write a Lambda function and say hello world&lt;/li&gt;
&lt;li&gt;Wire S3 to invoke Lambda automatically&lt;/li&gt;
&lt;li&gt;Watch logs and metrics in CloudWatch&lt;/li&gt;
&lt;li&gt;Tear it all down (this matters the most)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The finished version is on GitHub
&lt;/h2&gt;

&lt;p&gt;The article walks through every command by hand. The same setup is also on GitHub as a repo where &lt;strong&gt;&lt;code&gt;scripts/setup.sh&lt;/code&gt; brings everything up in one shot, and &lt;code&gt;scripts/cleanup.sh&lt;/code&gt; tears it down in one shot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/0-draft/awscape" rel="noopener noreferrer"&gt;0-draft/awscape&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Escape AWS bills. A 30-minute hands-on that builds S3 → Lambda → DynamoDB inside the AWS Always Free tier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What's in it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lambda-src/handler.py&lt;/code&gt;: the same Lambda code you'll write in Step 7&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policies/trust-policy.json&lt;/code&gt;: the trust policy from Step 6&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/setup.sh&lt;/code&gt;: runs Step 4 through Step 9 end-to-end (after you edit &lt;code&gt;config.env&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/cleanup.sh&lt;/code&gt;: idempotent teardown for Step 10 (safe to re-run if something errors midway)&lt;/li&gt;
&lt;li&gt;README is English by default, Japanese version is included&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read it through and type each command, or clone and run the scripts and then read the code. Either way works.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The 2026 AWS free tier, what it actually is
&lt;/h2&gt;

&lt;p&gt;Get this part wrong and you're going to be unhappy later. Read it carefully.&lt;/p&gt;

&lt;p&gt;The AWS free tier was &lt;strong&gt;overhauled on 2025-07-15&lt;/strong&gt;. Accounts created on or after that date follow the new rules. The word "free" now covers three different things.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ssj8uwioy1vche900b0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ssj8uwioy1vche900b0.png" alt="aws free tier" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Breakdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Free Plan (new accounts, on/after 2025-07-15)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$100 in credits&lt;/strong&gt; at signup, plus up to &lt;strong&gt;another $100&lt;/strong&gt; through &lt;strong&gt;5 specific activities (launch EC2, configure RDS, build a Lambda, prompt Bedrock, set up Budgets) at $20 each&lt;/strong&gt;. Maximum total: &lt;strong&gt;$200&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Expires at &lt;strong&gt;6 months&lt;/strong&gt; or when credits are gone, whichever comes first&lt;/li&gt;
&lt;li&gt;When it expires the &lt;strong&gt;account is automatically closed&lt;/strong&gt;. You then have &lt;strong&gt;90 days to upgrade to Paid Plan&lt;/strong&gt; to keep the account and data; otherwise data is permanently deleted&lt;/li&gt;
&lt;li&gt;Services that "would burn through the credits in a heartbeat" (Savings Plans, Reserved Instances, certain AWS Marketplace offers, etc.) aren't available on the Free Plan. The core services this hands-on uses (Lambda / DynamoDB / S3 / CloudWatch) work fine&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Paid Plan (the other option on/after 2025-07-15)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Same up to &lt;strong&gt;$200 in credits&lt;/strong&gt; at signup&lt;/li&gt;
&lt;li&gt;No expiry, full access to all services, pay-as-you-go after credits are gone&lt;/li&gt;
&lt;li&gt;For production workloads or anyone who wants to keep learning past 6 months&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  C. Legacy Free Tier (accounts created before 2025-07-15)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;"12 months free" applies for &lt;strong&gt;12 months from account creation&lt;/strong&gt;: EC2 &lt;strong&gt;t2.micro or t3.micro (region-dependent)&lt;/strong&gt; 750h/mo, S3 5 GB + 20,000 GET + 2,000 PUT, RDS 750h, etc.&lt;/li&gt;
&lt;li&gt;After 12 months only Always Free remains (the account stays open)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  D. Always Free (every plan, no expiry)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No clock, resets monthly, never goes away&lt;/li&gt;
&lt;li&gt;This is what this hands-on relies on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Always Free monthly quotas worth knowing about in 2026 (per each service's official pricing page).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Always Free per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Lambda&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M requests + 400,000 GB-seconds of compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DynamoDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25 GB storage + 25 RCU + 25 WCU (&lt;strong&gt;provisioned, Standard table class only&lt;/strong&gt;, on-demand is not covered)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudWatch metrics + alarms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 custom metrics, 10 alarms (Standard Resolution), 1M API requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudWatch Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ingestion + archive + Logs Insights scan &lt;strong&gt;share a single 5 GB quota&lt;/strong&gt; (not 5 GB each)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SNS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M publishes (commonly cited figure; not on the official pricing page directly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A note on S3.&lt;/strong&gt; The "5 GB + 20,000 GET + 2,000 PUT" figure is documented as the &lt;strong&gt;Legacy 12-month free tier&lt;/strong&gt; (for accounts created before 2025-07-15). For new Free Plan accounts, S3 usage draws from the $200 credit pool. AWS says "over 30 always-free services" but doesn't explicitly clarify whether S3 is in that list for new plans. Either way, this hands-on uses only a few dozen bytes and a handful of requests, so the practical cost is $0.&lt;/p&gt;

&lt;p&gt;That's enough room to put a small prototype together with effectively no cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which plan does this hands-on use
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brand new&lt;/strong&gt; to AWS: pick the &lt;strong&gt;Free Plan&lt;/strong&gt;. The core stays in Always Free and the S3 use is so small that credit consumption is effectively zero&lt;/li&gt;
&lt;li&gt;Already have a legacy account: use that&lt;/li&gt;
&lt;li&gt;Worried about the 6-month auto-close: switch to &lt;strong&gt;Paid Plan&lt;/strong&gt; before the expiry, and the account stays&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Prerequisites
&lt;/h2&gt;

&lt;p&gt;What you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An email address&lt;/strong&gt; (ideally separate from your daily one, for the root user)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A credit or debit card&lt;/strong&gt; (the Free Plan still wants one for identity verification; just registering won't charge it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A phone number&lt;/strong&gt; (for SMS or voice OTP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A notebook and a password manager&lt;/strong&gt; (for storing everything you set up)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set aside 30 to 60 minutes of focused time.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Step 1: create an account
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://aws.amazon.com/" rel="noopener noreferrer"&gt;https://aws.amazon.com/&lt;/a&gt; and click "Create an AWS Account." The flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvg7oaks7p3a53up42z96.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvg7oaks7p3a53up42z96.png" alt="aws signup" width="471" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account alias&lt;/strong&gt;: shows up in the IAM sign-in URL later. Pick something easy to remember and unrelated to anything sensitive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt;: pick &lt;strong&gt;Free Plan&lt;/strong&gt; the first time. You can switch to Paid Plan later&lt;/li&gt;
&lt;li&gt;Wait for the &lt;strong&gt;"Your account is ready"&lt;/strong&gt; email before moving on&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Step 2: four safety setups for the first 10 minutes
&lt;/h2&gt;

&lt;p&gt;The instant the account is live, do these four things. Skip them and you're one leaked root credential away from the kind of bill that ends up on Twitter.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add MFA to the root user&lt;/li&gt;
&lt;li&gt;Set up an AWS Budgets alert&lt;/li&gt;
&lt;li&gt;Create an IAM admin user&lt;/li&gt;
&lt;li&gt;Log out of root, log in as the IAM user&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  a) Add MFA to the root user
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Top-right of the console, click your account name, then "Security credentials"&lt;/li&gt;
&lt;li&gt;"Multi-factor authentication (MFA)" then "Assign MFA device"&lt;/li&gt;
&lt;li&gt;Scan the QR code with an &lt;strong&gt;Authenticator app&lt;/strong&gt; (Google Authenticator / 1Password / Authy etc.)&lt;/li&gt;
&lt;li&gt;Type two consecutive 6-digit codes to confirm&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the root user somehow has any access keys, delete them now. &lt;strong&gt;The root user must never own access keys.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  b) AWS Budgets alert
&lt;/h3&gt;

&lt;p&gt;For catching "I did something I shouldn't have" early.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search "Budgets" in the console&lt;/li&gt;
&lt;li&gt;Create budget, pick the &lt;strong&gt;Zero spend budget&lt;/strong&gt; template ("notify if any charge appears")&lt;/li&gt;
&lt;li&gt;Enter your email and save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you ever step outside Always Free, you get notified &lt;strong&gt;the next day or so&lt;/strong&gt; (Budgets' billing data aggregates with an 8 to 24 hour lag, so it's not literally instant, just very prompt).&lt;/p&gt;

&lt;h3&gt;
  
  
  c) Create an IAM admin user
&lt;/h3&gt;

&lt;p&gt;The root user is not for daily work. Create one IAM user for that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search "IAM" in the console, then "Users", then "Create user"&lt;/li&gt;
&lt;li&gt;Username: &lt;code&gt;admin-cli&lt;/code&gt; or similar&lt;/li&gt;
&lt;li&gt;Check "Provide user access to the console", auto-generate password, force change on first login

&lt;ol&gt;
&lt;li&gt;"Attach policies directly", pick &lt;code&gt;AdministratorAccess&lt;/code&gt; (full permissions, for learning; never do this in production)&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;On the success screen, download the .csv and bookmark the sign-in URL&lt;/li&gt;

&lt;li&gt;Add &lt;strong&gt;MFA&lt;/strong&gt; to this user too (same flow as root, separate entry in your authenticator)&lt;/li&gt;

&lt;/ol&gt;

&lt;h3&gt;
  
  
  d) Log out of root, sign in as the IAM user
&lt;/h3&gt;

&lt;p&gt;The sign-in URL is &lt;code&gt;https://&amp;lt;account-id&amp;gt;.signin.aws.amazon.com/console&lt;/code&gt;. Use that going forward. Only switch back to root for special tasks like changing billing info.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Step 3: AWS CLI setup
&lt;/h2&gt;

&lt;p&gt;You'll drive AWS from your local machine. Install the CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;awscli

&lt;span class="c"&gt;# Linux (official)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"awscliv2.zip"&lt;/span&gt;
unzip awscliv2.zip &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo&lt;/span&gt; ./aws/install

&lt;span class="c"&gt;# Windows: grab the official MSI&lt;/span&gt;
&lt;span class="c"&gt;# https://awscli.amazonaws.com/AWSCLIV2.msi&lt;/span&gt;

&lt;span class="c"&gt;# Verify&lt;/span&gt;
aws &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# aws-cli/2.x.x Python/3.x.x ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue an access key for the CLI
&lt;/h3&gt;

&lt;p&gt;The IAM user needs its own access key for CLI use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;IAM, then user &lt;code&gt;admin-cli&lt;/code&gt;, then the "Security credentials" tab&lt;/li&gt;
&lt;li&gt;"Create access key"&lt;/li&gt;
&lt;li&gt;For the use case, pick &lt;strong&gt;"Command Line Interface (CLI)"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy both the Access Key ID and the Secret Access Key (the secret is shown &lt;strong&gt;once and never again&lt;/strong&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;aws configure&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;span class="c"&gt;# AWS Access Key ID: AKIA...&lt;/span&gt;
&lt;span class="c"&gt;# AWS Secret Access Key: ...&lt;/span&gt;
&lt;span class="c"&gt;# Default region name: ap-northeast-1&lt;/span&gt;
&lt;span class="c"&gt;# Default output format: json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#     "UserId": "AIDA...",&lt;/span&gt;
&lt;span class="c"&gt;#     "Account": "123456789012",&lt;/span&gt;
&lt;span class="c"&gt;#     "Arn": "arn:aws:iam::123456789012:user/admin-cli"&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your username should show up in &lt;code&gt;Arn&lt;/code&gt;. That's the green light.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Step 4: create an S3 bucket and put something in it
&lt;/h2&gt;

&lt;p&gt;S3 (Simple Storage Service) is one of the oldest AWS services (2006). For now, think of it as "near-infinite cloud storage that you talk to over HTTPS."&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick refresher
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucket&lt;/strong&gt;: the container for objects. Names are &lt;strong&gt;globally unique across all of AWS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object&lt;/strong&gt;: an individual file in a bucket. Identified by a &lt;strong&gt;key&lt;/strong&gt; (the filename, roughly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region&lt;/strong&gt;: a bucket lives in one Region. This hands-on uses &lt;code&gt;ap-northeast-1&lt;/code&gt; (Tokyo) everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create the bucket
&lt;/h3&gt;

&lt;p&gt;Bucket names are globally unique, so add some entropy. Replace &lt;code&gt;&amp;lt;your-name&amp;gt;&lt;/code&gt; with your handle or a date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"aws-handson-&amp;lt;your-name&amp;gt;-2026"&lt;/span&gt;
aws s3 mb &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;span class="c"&gt;# make_bucket: aws-handson-...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check Block Public Access
&lt;/h3&gt;

&lt;p&gt;New buckets should have all public access blocked by default. Verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api get-public-access-block &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "PublicAccessBlockConfiguration": {&lt;/span&gt;
&lt;span class="c"&gt;#     "BlockPublicAcls": true,&lt;/span&gt;
&lt;span class="c"&gt;#     "IgnorePublicAcls": true,&lt;/span&gt;
&lt;span class="c"&gt;#     "BlockPublicPolicy": true,&lt;/span&gt;
&lt;span class="c"&gt;#     "RestrictPublicBuckets": true&lt;/span&gt;
&lt;span class="c"&gt;#   }&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four &lt;code&gt;true&lt;/code&gt; is the safe state. If any are &lt;code&gt;false&lt;/code&gt;, force them all on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-public-access-block &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--public-access-block-configuration&lt;/span&gt; &lt;span class="s2"&gt;"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Drop a file in
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello AWS"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hello.txt
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;hello.txt &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/uploads/hello.txt"&lt;/span&gt;
&lt;span class="c"&gt;# upload: ./hello.txt to s3://aws-handson-.../uploads/hello.txt&lt;/span&gt;

aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/uploads/"&lt;/span&gt;
&lt;span class="c"&gt;# 2026-05-14 12:34:56         10 hello.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's "putting a file in cloud storage." Always Free covers 5 GB of standard storage plus 2,000 PUT and 20,000 GET per month.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Step 5: create a DynamoDB table
&lt;/h2&gt;

&lt;p&gt;DynamoDB is the managed NoSQL database. Create one table, then later Lambda will write to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vocab
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table&lt;/strong&gt;: a collection of schemaless items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition key&lt;/strong&gt;: the unique key for an item (close to a primary key in RDBMS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity mode&lt;/strong&gt;: "Provisioned" (reserve capacity in RCU/WCU) or "On-demand" (&lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;, per-request billing)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick provisioned mode to stay on Always Free
&lt;/h3&gt;

&lt;p&gt;This part trips people up. DynamoDB's Always Free covers &lt;strong&gt;provisioned (Standard class), 25 WCU + 25 RCU + 25 GB only&lt;/strong&gt;. On-demand (&lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;) is &lt;strong&gt;outside the free tier&lt;/strong&gt; and charges per request.&lt;/p&gt;

&lt;p&gt;For learning, reserve 25 WCU / 25 RCU on provisioned mode (provisioning that much still costs nothing).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws dynamodb create-table &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--table-name&lt;/span&gt; UploadLog &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--attribute-definitions&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ObjectKey,AttributeType&lt;span class="o"&gt;=&lt;/span&gt;S &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--key-schema&lt;/span&gt; &lt;span class="nv"&gt;AttributeName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ObjectKey,KeyType&lt;span class="o"&gt;=&lt;/span&gt;HASH &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--billing-mode&lt;/span&gt; PROVISIONED &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--provisioned-throughput&lt;/span&gt; &lt;span class="nv"&gt;ReadCapacityUnits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;25,WriteCapacityUnits&lt;span class="o"&gt;=&lt;/span&gt;25 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--table-class&lt;/span&gt; STANDARD &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1

&lt;span class="c"&gt;# Give it a moment&lt;/span&gt;
aws dynamodb describe-table &lt;span class="nt"&gt;--table-name&lt;/span&gt; UploadLog &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Table.TableStatus'&lt;/span&gt;
&lt;span class="c"&gt;# "ACTIVE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;25 WCU lets you write 25 items per second (1 KB each), roughly 65 million writes per month. You'll never come close in a hands-on.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Step 6: prepare an IAM Role for Lambda
&lt;/h2&gt;

&lt;p&gt;Time for the IAM piece. The two-line version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lambda doesn't carry credentials of its own.&lt;/strong&gt; When you create a function you specify "the IAM Role this function runs as"&lt;/li&gt;
&lt;li&gt;Every time the function runs, the Lambda service &lt;strong&gt;assumes&lt;/strong&gt; that Role to get temporary credentials, and your code (&lt;code&gt;boto3&lt;/code&gt; etc.) uses those to call other AWS services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the Role needs two policies. A &lt;strong&gt;trust policy&lt;/strong&gt; that says "the Lambda service is allowed to assume me," and &lt;strong&gt;permission policies&lt;/strong&gt; describing what the function can do once it has.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4opktlna3b859fwm9j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4opktlna3b859fwm9j4.png" alt="prepare an IAM Role for lambda" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust policy file
&lt;/h3&gt;

&lt;p&gt;Lets the Lambda service assume the Role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; trust-policy.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Role
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-role &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; file://trust-policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Attach the AWS managed policy
&lt;/h3&gt;

&lt;p&gt;The standard one for log writes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam attach-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add DynamoDB and S3 permissions (inline policy)
&lt;/h3&gt;

&lt;p&gt;Following least privilege, scope it to exactly this table and this bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; Account &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; inline-policy.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "dynamodb:PutItem",
      "Resource": "arn:aws:dynamodb:ap-northeast-1:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;:table/UploadLog"
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;/*"
    }
  ]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;aws iam put-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--policy-name&lt;/span&gt; HandleUploadInline &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://inline-policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Step 7: write and deploy the Lambda function
&lt;/h2&gt;

&lt;p&gt;Lambda is the serverless runtime where you upload code plus a runtime spec, and AWS handles starting and stopping the process for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Python code
&lt;/h3&gt;

&lt;p&gt;A heads-up on the libraries used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;boto3&lt;/code&gt;&lt;/strong&gt;: the AWS SDK for Python. &lt;strong&gt;Already bundled with the Lambda Python runtime&lt;/strong&gt;, so no pip install needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;urllib.parse.unquote_plus&lt;/code&gt;&lt;/strong&gt;: object keys in S3 events come URL-encoded (spaces become &lt;code&gt;+&lt;/code&gt;, Japanese characters become &lt;code&gt;%E3%83%...&lt;/code&gt;). This function decodes them back
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; lambda-src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; lambda-src/handler.py &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
import json
import urllib.parse
from datetime import datetime, timezone

import boto3

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("UploadLog")


def lambda_handler(event, context):
    # Pull info out of the S3 event
    record = event["Records"][0]
    bucket = record["s3"]["bucket"]["name"]
    # S3 URL-encodes the object key in events; decode it
    key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
    size = record["s3"]["object"]["size"]

    print(f"Got upload: bucket={bucket} key={key} size={size}")

    # Write to DynamoDB
    table.put_item(Item={
        "ObjectKey": key,
        "Bucket": bucket,
        "Size": size,
        "UploadedAt": datetime.now(timezone.utc).isoformat(),
    })

    return {"statusCode": 200, "body": json.dumps({"ok": True, "key": key})}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Zip it up&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;lambda-src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; zip ../function.zip handler.py &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Lambda function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws iam get-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Role.Arn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws lambda create-function &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--function-name&lt;/span&gt; HandleUpload &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--runtime&lt;/span&gt; python3.13 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--handler&lt;/span&gt; handler.lambda_handler &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--zip-file&lt;/span&gt; fileb://function.zip &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you just created the Role, you might get an "IAM Role hasn't propagated yet" error. Wait 10 seconds and retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smoke test (manually invoke with a fake S3 event)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test-event.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "Records": [{
    "s3": {
      "bucket": {"name": "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"},
      "object": {"key": "uploads/hello.txt", "size": 10}
    }
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;aws lambda invoke &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--function-name&lt;/span&gt; HandleUpload &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--payload&lt;/span&gt; fileb://test-event.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cli-binary-format&lt;/span&gt; raw-in-base64-out &lt;span class="se"&gt;\&lt;/span&gt;
    response.json

&lt;span class="nb"&gt;cat &lt;/span&gt;response.json
&lt;span class="c"&gt;# {"statusCode": 200, "body": "{\"ok\": true, \"key\": \"uploads/hello.txt\"}"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that DynamoDB picked it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws dynamodb scan &lt;span class="nt"&gt;--table-name&lt;/span&gt; UploadLog
&lt;span class="c"&gt;# Items should include a hello.txt record&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. Step 8: have S3 invoke Lambda automatically
&lt;/h2&gt;

&lt;p&gt;Up to here we've been calling the function explicitly. Next, wire it so that &lt;strong&gt;a file landing in S3 triggers the Lambda by itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpxocint1jnxr90ele7tp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpxocint1jnxr90ele7tp.png" alt="s3 invoke lambda automatically" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Allow S3 to invoke the function
&lt;/h3&gt;

&lt;p&gt;For S3 to call Lambda, Lambda needs a resource-based policy granting that permission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda add-permission &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--function-name&lt;/span&gt; HandleUpload &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--statement-id&lt;/span&gt; AllowS3Invoke &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--action&lt;/span&gt; lambda:InvokeFunction &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--principal&lt;/span&gt; s3.amazonaws.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--source-arn&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:s3:::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure the S3 event notification
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LAMBDA_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws lambda get-function &lt;span class="nt"&gt;--function-name&lt;/span&gt; HandleUpload &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Configuration.FunctionArn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; notification.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "LambdaFunctionConfigurations": [{
    "LambdaFunctionArn": "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LAMBDA_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
    "Events": ["s3:ObjectCreated:*"],
    "Filter": {
      "Key": {
        "FilterRules": [{ "Name": "prefix", "Value": "uploads/" }]
      }
    }
  }]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;aws s3api put-bucket-notification-configuration &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--notification-configuration&lt;/span&gt; file://notification.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Upload a file and watch it fire
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"auto-trigger test"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; memo.txt
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;memo.txt &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/uploads/memo.txt"&lt;/span&gt;

&lt;span class="c"&gt;# Wait a few seconds, then check DynamoDB&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;5
aws dynamodb scan &lt;span class="nt"&gt;--table-name&lt;/span&gt; UploadLog &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Items[?ObjectKey.S == `uploads/memo.txt`]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;memo.txt&lt;/code&gt; shows up in the scan, the S3 → Lambda → DynamoDB pipeline is wired up.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Step 9: logs and metrics in CloudWatch
&lt;/h2&gt;

&lt;p&gt;Lambda automatically pushes stdout (&lt;code&gt;print&lt;/code&gt; or &lt;code&gt;console.log&lt;/code&gt;) to &lt;strong&gt;CloudWatch Logs&lt;/strong&gt;. Execution count, error count, duration, all of it lands in &lt;strong&gt;CloudWatch Metrics&lt;/strong&gt; automatically too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws logs &lt;span class="nb"&gt;tail&lt;/span&gt; /aws/lambda/HandleUpload &lt;span class="nt"&gt;--since&lt;/span&gt; 10m
&lt;span class="c"&gt;# 2026-05-14T12:34:56 START RequestId: ...&lt;/span&gt;
&lt;span class="c"&gt;# 2026-05-14T12:34:56 Got upload: bucket=aws-handson-... key=uploads/memo.txt size=18&lt;/span&gt;
&lt;span class="c"&gt;# 2026-05-14T12:34:56 END RequestId: ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  View metrics in the console
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Console → CloudWatch → Metrics → Lambda&lt;/li&gt;
&lt;li&gt;Look at &lt;code&gt;Invocations&lt;/code&gt; (run count), &lt;code&gt;Errors&lt;/code&gt; (failures), &lt;code&gt;Duration&lt;/code&gt; (how long it took)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Set up an alarm
&lt;/h3&gt;

&lt;p&gt;Email yourself when Lambda errors. The alarm itself just holds state; for actual email delivery you need an &lt;strong&gt;SNS (Simple Notification Service) topic with your email subscribed.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create an SNS topic&lt;/span&gt;
&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sns create-topic &lt;span class="nt"&gt;--name&lt;/span&gt; lambda-alarms &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'TopicArn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Subscribe your email&lt;/span&gt;
aws sns subscribe &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--protocol&lt;/span&gt; email &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--notification-endpoint&lt;/span&gt; your-email@example.com
&lt;span class="c"&gt;# AWS sends you a confirmation email shortly. Click the Confirm link in it.&lt;/span&gt;

&lt;span class="c"&gt;# 3. Create the CloudWatch alarm and point it at the SNS topic&lt;/span&gt;
aws cloudwatch put-metric-alarm &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--alarm-name&lt;/span&gt; HandleUpload-Errors &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--metric-name&lt;/span&gt; Errors &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/Lambda &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--statistic&lt;/span&gt; Sum &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--period&lt;/span&gt; 60 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--evaluation-periods&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--threshold&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--comparison-operator&lt;/span&gt; GreaterThanOrEqualToThreshold &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;FunctionName,Value&lt;span class="o"&gt;=&lt;/span&gt;HandleUpload &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--treat-missing-data&lt;/span&gt; notBreaching &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--alarm-actions&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Click the Subscribe confirmation email.&lt;/strong&gt; Without it the topic accepts messages but nothing reaches your inbox.&lt;/p&gt;

&lt;p&gt;Everything stays inside the Always Free quota of 10 alarms and 1M SNS publishes. To test it, raise an exception in the Lambda code and within about a minute the alarm goes ALARM and you get the email.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Step 10: tear it all down (this matters the most)
&lt;/h2&gt;

&lt;p&gt;When you're done, &lt;strong&gt;delete every resource you created.&lt;/strong&gt; Anything left over is one slip away from surprise billing. Everything here should fit inside Always Free, but &lt;strong&gt;build the habit of deleting&lt;/strong&gt; anyway.&lt;/p&gt;

&lt;p&gt;Order of teardown:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Empty the S3 bucket, then delete it&lt;/li&gt;
&lt;li&gt;Delete the Lambda function&lt;/li&gt;
&lt;li&gt;Delete the DynamoDB table&lt;/li&gt;
&lt;li&gt;Delete the CloudWatch alarm&lt;/li&gt;
&lt;li&gt;Delete the CloudWatch log group&lt;/li&gt;
&lt;li&gt;Delete the SNS topic&lt;/li&gt;
&lt;li&gt;Delete the IAM Role&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Leave AWS Budgets in place&lt;/strong&gt; (you want it to keep watching going forward).&lt;/p&gt;

&lt;h3&gt;
  
  
  One-shot teardown
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# S3&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt;
aws s3 rb &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Lambda&lt;/span&gt;
aws lambda delete-function &lt;span class="nt"&gt;--function-name&lt;/span&gt; HandleUpload

&lt;span class="c"&gt;# DynamoDB&lt;/span&gt;
aws dynamodb delete-table &lt;span class="nt"&gt;--table-name&lt;/span&gt; UploadLog

&lt;span class="c"&gt;# CloudWatch&lt;/span&gt;
aws cloudwatch delete-alarms &lt;span class="nt"&gt;--alarm-names&lt;/span&gt; HandleUpload-Errors
aws logs delete-log-group &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; /aws/lambda/HandleUpload

&lt;span class="c"&gt;# SNS&lt;/span&gt;
aws sns delete-topic &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# IAM Role&lt;/span&gt;
aws iam delete-role-policy &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole &lt;span class="nt"&gt;--policy-name&lt;/span&gt; HandleUploadInline
aws iam detach-role-policy &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; HandleUploadRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last step: open Billing → Bills in the console and confirm the current month shows &lt;code&gt;$0.00&lt;/code&gt;. Done.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you cloned &lt;a href="https://github.com/0-draft/awscape" rel="noopener noreferrer"&gt;0-draft/awscape&lt;/a&gt;, &lt;code&gt;./scripts/cleanup.sh&lt;/code&gt; does all of the above in one go. It's idempotent, so re-running after a partial failure is safe.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  13. What you actually touched
&lt;/h2&gt;

&lt;p&gt;The seven services here cover most of the fundamental building blocks of AWS.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What you used&lt;/th&gt;
&lt;th&gt;What you got out of it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IAM User / Role / Policy / Trust Policy&lt;/td&gt;
&lt;td&gt;The basics of separating "who can access what"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3 + Block Public Access&lt;/td&gt;
&lt;td&gt;Cloud object storage and how access control works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lambda + IAM Role wiring&lt;/td&gt;
&lt;td&gt;Serverless execution, and how one AWS service calls another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DynamoDB (provisioned)&lt;/td&gt;
&lt;td&gt;The feel of NoSQL and reading/writing from Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event-driven&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3 → Lambda notifications&lt;/td&gt;
&lt;td&gt;How AWS services trigger each other loosely-coupled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CloudWatch Logs / Metrics / Alarms&lt;/td&gt;
&lt;td&gt;"Is it actually running" plus paging when it isn't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS Budgets + Always Free&lt;/td&gt;
&lt;td&gt;The baseline for not getting a surprise bill&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shape (S3 catches input, Lambda processes, DynamoDB stores, CloudWatch watches) shows up everywhere in real systems. From here, the natural next moves are adding API Gateway for a REST API, fanning out with SQS or EventBridge, or rolling all of it into CloudFormation / CDK as infrastructure-as-code.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Common mistakes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AccessDenied&lt;/code&gt; everywhere&lt;/td&gt;
&lt;td&gt;Logged out of root but the IAM user has too-narrow permissions&lt;/td&gt;
&lt;td&gt;Confirm you attached &lt;code&gt;AdministratorAccess&lt;/code&gt; in Step 2c (only for learning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;s3 mb&lt;/code&gt; fails with name conflict&lt;/td&gt;
&lt;td&gt;S3 bucket names are global&lt;/td&gt;
&lt;td&gt;Add more entropy (date, random suffix)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;InvalidParameterValueException&lt;/code&gt; creating Lambda&lt;/td&gt;
&lt;td&gt;IAM Role hasn't propagated yet&lt;/td&gt;
&lt;td&gt;Wait 10 seconds and retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 upload works but Lambda doesn't fire&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;add-permission&lt;/code&gt;, or wrong prefix in the notification&lt;/td&gt;
&lt;td&gt;Re-run Step 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda runs but DynamoDB write fails&lt;/td&gt;
&lt;td&gt;Inline policy is missing &lt;code&gt;dynamodb:PutItem&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Re-apply inline-policy.json&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surprise charge next month&lt;/td&gt;
&lt;td&gt;Forgot to clean up&lt;/td&gt;
&lt;td&gt;Open Cost Explorer to find what's still running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account vanished at the 6-month mark&lt;/td&gt;
&lt;td&gt;Free Plan expired&lt;/td&gt;
&lt;td&gt;Upgrade to Paid Plan before that date, or start a new account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 2026 AWS free tier is four things: &lt;strong&gt;Free Plan (6-month, new), Paid Plan (new), Legacy 12-month (old), Always Free (forever)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Always Free alone (Lambda 1M req / DynamoDB 25 GB / S3 5 GB / CloudWatch 10 metrics) is enough to actually build something&lt;/li&gt;
&lt;li&gt;Four things to do &lt;strong&gt;immediately&lt;/strong&gt; after account creation: root MFA, Budgets, IAM admin user, log out of root&lt;/li&gt;
&lt;li&gt;S3 + Lambda + DynamoDB + CloudWatch + IAM is the foundational pattern; once you've wired it once by hand, the rest of AWS reads as variations on this&lt;/li&gt;
&lt;li&gt;When you're done, &lt;strong&gt;delete the resources.&lt;/strong&gt; Keep Budgets&lt;/li&gt;
&lt;li&gt;Watch the Free Plan 6-month clock. Switch to Paid Plan before the expiry if you want to keep the account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/free/" rel="noopener noreferrer"&gt;AWS Free Tier (official)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/aws-free-tier-update-new-customers-can-get-started-and-explore-aws-with-up-to-200-in-credits/" rel="noopener noreferrer"&gt;AWS Free Tier update: $200 credits and 6-month plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/free/free-tier-faqs/" rel="noopener noreferrer"&gt;AWS Free Tier FAQs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier-plans.html" rel="noopener noreferrer"&gt;Choosing an AWS Free Tier plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/getting-started/hands-on/run-serverless-code/" rel="noopener noreferrer"&gt;Getting Started with AWS Lambda (Hands-On)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html" rel="noopener noreferrer"&gt;Tutorial: Using an Amazon S3 trigger to invoke a Lambda function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStartedDynamoDB.html" rel="noopener noreferrer"&gt;Amazon DynamoDB Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>tutorial</category>
      <category>serverless</category>
      <category>beginners</category>
    </item>
    <item>
      <title>AWS Deep Dive</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Thu, 14 May 2026 12:46:07 +0000</pubDate>
      <link>https://dev.to/kanywst/aws-deep-dive-what-it-actually-is-how-regions-and-accounts-fit-together-and-where-auth-lives-107k</link>
      <guid>https://dev.to/kanywst/aws-deep-dive-what-it-actually-is-how-regions-and-accounts-fit-together-and-where-auth-lives-107k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Before you can honestly say you "get AWS," you need a plain-language answer to one question: what is it, really? &lt;strong&gt;Where does it physically run, how does the bill actually arrive, and how do you talk to it?&lt;/strong&gt; Skip that and every IAM policy you write is just vibes.&lt;/p&gt;

&lt;p&gt;By the end of this you should be able to answer all of these without hesitation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's the actual difference between a Region and an Availability Zone?&lt;/li&gt;
&lt;li&gt;An AWS account is just an email address, right?&lt;/li&gt;
&lt;li&gt;Why does every senior engineer insist on splitting production and staging into separate accounts?&lt;/li&gt;
&lt;li&gt;When you click around in the console vs. typing into the CLI, what's actually happening underneath?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The 30-second version of "the world before the cloud"
&lt;/h2&gt;

&lt;p&gt;The fastest way to appreciate the cloud is to remember what it replaced.&lt;/p&gt;

&lt;p&gt;You buy a physical server. You rent rack space in a data center. You sign contracts for power and network. You install an OS. Finally, your app runs. That's on-premises, "on-prem" for short.&lt;/p&gt;

&lt;p&gt;The problem with all of that: &lt;strong&gt;it's slow, paid up front, and a pain to scale&lt;/strong&gt;. If marketing tells you traffic is going to be 10x next week, you can't just go buy 10x the hardware.&lt;/p&gt;

&lt;p&gt;The cloud rents you everything from "physical box up to just-below-your-app." All of it. &lt;strong&gt;One API call, available right now.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjjrddy7hxe7kcv0qh0c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjjrddy7hxe7kcv0qh0c.png" alt="aws vs onprem" width="800" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The green box is the only thing you actually wrote. With the cloud, that's the only part you have to care about.&lt;/p&gt;

&lt;p&gt;AWS (Amazon Web Services) is the biggest cloud provider out there with 200+ services. EC2 (virtual machines), S3 (object storage), RDS (managed databases), Lambda (serverless runtime), IAM (auth). Those are just AWS's internal code names for things you probably already know.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The physical layout: Region, AZ, data center
&lt;/h2&gt;

&lt;p&gt;The cloud isn't magic. It runs on physical servers somewhere on Earth, and AWS arranges those servers in a strict hierarchy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqmh1xz96pypc0bovvtk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqmh1xz96pypc0bovvtk.png" alt="Region AZ DC" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three terms, three different scopes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A geographic chunk of the planet&lt;/td&gt;
&lt;td&gt;ap-northeast-1 (Tokyo)&lt;/td&gt;
&lt;td&gt;Regions are fully independent. An S3 bucket in Tokyo is invisible from Virginia.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability Zone (AZ)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A physically isolated set of DCs inside one Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1a&lt;/td&gt;
&lt;td&gt;Independent power, cooling, and network. One AZ failing doesn't take down the others.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data center (DC)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The actual building&lt;/td&gt;
&lt;td&gt;(omitted)&lt;/td&gt;
&lt;td&gt;AWS officially says "an AZ is &lt;strong&gt;one or more&lt;/strong&gt; discrete DCs." In any serious Region the AZs are multiple buildings (the diagram above is conceptual).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three classic beginner mistakes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Putting EC2 in a single AZ&lt;/strong&gt;: that AZ goes down, your service goes down. Production should always span at least 2 AZs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working in the wrong Region&lt;/strong&gt;: thinking you're in the US, racking up bills in Tokyo. Happens more than you'd expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We're using Tokyo, so the data stays in Japan"&lt;/strong&gt;: the AWS control plane (IAM user info and the like) is global. Data residency and governance need explicit design, not a Region picker.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  "Same services everywhere" is a myth
&lt;/h3&gt;

&lt;p&gt;New services almost always launch in us-east-1 first and roll out to other Regions over months or years. There are always features Tokyo doesn't have yet. If you just want to play with the latest thing, us-east-1 is the fast lane.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The logical layout: AWS accounts and Organizations
&lt;/h2&gt;

&lt;p&gt;This part is harder to picture than the physical layout. An AWS &lt;strong&gt;account&lt;/strong&gt; is not a Twitter account or a GitHub account.&lt;/p&gt;

&lt;p&gt;An AWS account is &lt;strong&gt;a container for resources and money&lt;/strong&gt;. Every EC2 instance, every S3 bucket, every resource you create lives inside exactly one account. Billing is also per account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmezsiirxt6tr42gld2e7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmezsiirxt6tr42gld2e7.png" alt="aws logical layout" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The relationships in one table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What to remember&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The unit of resources and billing&lt;/td&gt;
&lt;td&gt;One account is one "box." Everything inside is self-contained.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organizations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trees multiple accounts together&lt;/td&gt;
&lt;td&gt;If you're a company, use it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Management Account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The root of the tree&lt;/td&gt;
&lt;td&gt;Billing aggregation and governance only. &lt;strong&gt;Do not run workloads here.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OU (Organizational Unit)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A folder-like grouping for accounts&lt;/td&gt;
&lt;td&gt;Split by Production / Development / Security and similar lines.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why split accounts
&lt;/h3&gt;

&lt;p&gt;"We're one company, why do we need more than one account?" People say this until they've operated a real system. Then they stop saying it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg3nbakhak9jhmnicloc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg3nbakhak9jhmnicloc.png" alt="Split Account" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Account separation buys you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Physical isolation of permissions&lt;/strong&gt;: one botched IAM policy can't reach into a different account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visible costs&lt;/strong&gt;: glance at the bill and you know which environment is spending what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear ownership&lt;/strong&gt;: "this account belongs to Team A" is now a sentence you can say.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded blast radius&lt;/strong&gt;: when something blows up, it blows up inside one account, not across everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's why every senior engineer ends up saying "production and staging belong in different accounts."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Management Account is sacred
&lt;/h3&gt;

&lt;p&gt;The Management Account (the one at the top of the tree) is for &lt;strong&gt;billing and governance, period.&lt;/strong&gt; Don't run apps in it. If that account is compromised, every account below it is at risk by definition.&lt;/p&gt;

&lt;p&gt;In real organizations almost no human ever needs to log in to the management account.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Every path into AWS ends at the same HTTPS API
&lt;/h2&gt;

&lt;p&gt;Humans, CI runners, Lambda functions, Terraform: when they touch AWS, there's exactly one path under the hood. &lt;strong&gt;HTTPS API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdw5xzsssyc3r5zn8xf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdw5xzsssyc3r5zn8xf2.png" alt="HTTPS API" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth letting that sink in. Clicking around in the console? That's hitting the same HTTPS API. CLI? Same API. CloudFormation, CDK, Terraform? Same API.&lt;/p&gt;

&lt;p&gt;So when people talk about AWS security, the whole problem reduces to &lt;strong&gt;who can call which HTTPS API, and with what permissions&lt;/strong&gt;. The thing that answers both questions is &lt;strong&gt;IAM (Identity and Access Management)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every API request carries a signature
&lt;/h3&gt;

&lt;p&gt;Unlike most REST APIs, AWS doesn't use &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. It uses its own signing scheme: &lt;strong&gt;SigV4 (Signature Version 4)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The shape of it: hash the request (URL, headers, body), sign that hash with your secret key, attach the signature to the request. The server reproduces the same calculation. If the signatures match, the request is authentic and unmodified.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3hnqyjob5ijlby8dlxj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3hnqyjob5ijlby8dlxj.png" alt="Sigv4" width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing to take away: &lt;strong&gt;the secret key itself never goes over the wire.&lt;/strong&gt; Only the signature derived from it does. Even if someone snoops on the request, they can't recover the secret.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Authentication vs. authorization in AWS
&lt;/h2&gt;

&lt;p&gt;So far: everything is an API, and every request is signed. Once the request lands, AWS asks itself two separate questions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fslzm00o4fsrl6zk3ejva.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fslzm00o4fsrl6zk3ejva.png" alt="AuthN vs AuthZ" width="694" height="966"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;How AWS answers it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authentication (AuthN)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Who are you?&lt;/td&gt;
&lt;td&gt;SigV4 signature check + access key or temporary credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authorization (AuthZ)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Are you allowed to do this?&lt;/td&gt;
&lt;td&gt;IAM policy, SCP, resource policy, permissions boundary, and friends&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are not the same thing. Having a valid signature (you authenticated) doesn't mean the action is allowed. The reverse is also true: being broadly allowed to do something is meaningless if your signature is wrong.&lt;/p&gt;

&lt;p&gt;Authorization in AWS gets interesting because &lt;strong&gt;multiple policy types are combined&lt;/strong&gt; to produce the final yes-or-no. That deserves its own write-up, and AWS's IAM docs are the place to go for the details.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The money question: pay-as-you-go and guardrails
&lt;/h2&gt;

&lt;p&gt;AWS is &lt;strong&gt;pay-as-you-go, billed after the fact&lt;/strong&gt;. At the end of the month, you get a bill for what you used.&lt;/p&gt;

&lt;p&gt;This is where new users get burned. The classic blunders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spin up an EC2 instance for a test, forget to stop it, head into a long weekend, next month's bill is $400.&lt;/li&gt;
&lt;li&gt;Stuff a bucket full of logs and forget about them. Six months later you have tens of GB at a few dollars a month and never noticed.&lt;/li&gt;
&lt;li&gt;Assume data transfer (egress) is free. It is not. Big transfers add up fast.&lt;/li&gt;
&lt;li&gt;Leave a NAT Gateway running in a VPC you no longer use. That's about $30 a month for nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud pricing usually breaks down into &lt;strong&gt;compute, storage, data transfer, and request counts&lt;/strong&gt;. Services that look cheap on the sticker can get expensive once request charges or retrieval fees pile on. (Glacier's "cheap to store, expensive to retrieve" trap is famous for a reason.)&lt;/p&gt;

&lt;p&gt;Bare-minimum guardrails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set up AWS Budgets&lt;/strong&gt;: a monthly budget with email alerts at, say, $100. You can wire it up to actually disable IAM users at higher thresholds if you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Cost Explorer regularly&lt;/strong&gt;: a quick daily glance catches abnormal trends within a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep personal experiments in their own account&lt;/strong&gt;: mixed in with work stuff, weird charges go unnoticed.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Wrap-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS rents you everything from hardware up to just below your app, and bills you for what you used.&lt;/li&gt;
&lt;li&gt;Physical hierarchy: Region &amp;gt; AZ &amp;gt; DC. Always span multiple AZs in production.&lt;/li&gt;
&lt;li&gt;Logical hierarchy: Organizations &amp;gt; OU &amp;gt; account. Production and staging go in separate accounts.&lt;/li&gt;
&lt;li&gt;Every operation, no matter how it's triggered, ends up as an HTTPS API call signed with SigV4.&lt;/li&gt;
&lt;li&gt;Authentication (who?) and authorization (allowed?) are separate questions. IAM handles both.&lt;/li&gt;
&lt;li&gt;Pay-as-you-go means you need Budgets and Cost Explorer in place from day one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"AWS is a collection of HTTPS APIs, and IAM stands at the door."&lt;/strong&gt; Keep that one line in your head and you won't get lost when a new service shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/iam/resources/best-practices/" rel="noopener noreferrer"&gt;AWS Identity and Access Management Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html" rel="noopener noreferrer"&gt;AWS Signature Version 4 for API requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html" rel="noopener noreferrer"&gt;AWS Organizations: Service control policies (SCPs)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>security</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Introducing Zopa: a 60 KB authorization engine for proxy-wasm, written in Zig</title>
      <dc:creator>kt</dc:creator>
      <pubDate>Sun, 10 May 2026 07:37:50 +0000</pubDate>
      <link>https://dev.to/kanywst/introducing-zopa-a-60-kb-authorization-engine-for-proxy-wasm-written-in-zig-1l66</link>
      <guid>https://dev.to/kanywst/introducing-zopa-a-60-kb-authorization-engine-for-proxy-wasm-written-in-zig-1l66</guid>
      <description>&lt;p&gt;There are plenty of times you want to delegate "let this request through, or block it" to a wasm filter inside Envoy. API gateways, service mesh boundaries, L7 checkpoints. The default move is to use OPA's wasm build.&lt;/p&gt;

&lt;p&gt;The trouble is OPA-as-wasm is heavy. The Go runtime, the Rego parser, and the evaluator are all in there. You only want to return allow/deny at the edge, but you ship something many times the size of the evaluator. Cedar and Casbin don't ship official wasm builds (as of May 2026). The slot for "drop-in proxy-wasm authorization filter" is empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/0-draft/zopa" rel="noopener noreferrer"&gt;zopa&lt;/a&gt; is what I built to fill that slot. A Zig &lt;code&gt;wasm32-freestanding&lt;/code&gt; binary, ~60 KB at release. No GC; memory turns over on a per-request arena. It runs on any host that implements proxy-wasm 0.2.1 (Envoy / wasmtime / wamr / v8).&lt;/p&gt;

&lt;h2&gt;
  
  
  Big picture
&lt;/h2&gt;

&lt;p&gt;Zopa assumes you separate &lt;strong&gt;where you write policy&lt;/strong&gt; from &lt;strong&gt;where you evaluate it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5tld6tadgzo8cwx4f6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5tld6tadgzo8cwx4f6l.png" alt="zopa" width="514" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Policy authors write rules in Rego (OPA's policy language; a declarative DSL in the Datalog family). The CI converts that to AST (Abstract Syntax Tree) JSON. At Envoy startup the AST is handed to the wasm module as plugin config; on each incoming request, zopa evaluates and returns 1 or 0.&lt;/p&gt;

&lt;p&gt;There's exactly one design call here: &lt;strong&gt;don't ship a language compiler inside the wasm module&lt;/strong&gt;. OPA wasm is large because the Rego parser and evaluator are bundled together. Zopa pushes the parser out of the wasm (into a CI job) and keeps the wasm module focused on evaluation. That alone moves the binary size by orders of magnitude.&lt;/p&gt;

&lt;h2&gt;
  
  
  proxy-wasm refresher
&lt;/h2&gt;

&lt;p&gt;proxy-wasm is the ABI spec for "filters in wasm" used by Envoy and friends. Most famous in Envoy, but anything embedding wasmtime / wamr / v8 can host it.&lt;/p&gt;

&lt;p&gt;Three points cover the host/wasm relationship:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The host calls into wasm exports at request milestones (&lt;code&gt;proxy_on_request_headers&lt;/code&gt; etc.).&lt;/li&gt;
&lt;li&gt;The wasm pulls header values back through host imports (&lt;code&gt;proxy_get_header_map_value&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Allow does nothing (Envoy continues). Deny asks the host to call &lt;code&gt;proxy_send_local_response(403)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zopa implements proxy-wasm 0.2.1. Spec body: &lt;a href="https://github.com/proxy-wasm/spec" rel="noopener noreferrer"&gt;proxy-wasm/spec&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it fits in 60 KB
&lt;/h2&gt;

&lt;p&gt;The build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zig build &lt;span class="nt"&gt;--release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;small
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; zig-out/bin/zopa.wasm
&lt;span class="c"&gt;# -rw-r--r-- 1 you staff 60K  zopa.wasm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things drive the size:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wasm32-freestanding&lt;/code&gt; target.&lt;/strong&gt; No WASI (the wasm syscall spec). No OS, no syscalls, only a thin slice of stdlib. &lt;code&gt;freestanding&lt;/code&gt; drops every file I/O / network stub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No GC.&lt;/strong&gt; Zig has no garbage collector (like Rust, ownership is explicit and memory is hand-managed). The GC code and management metadata simply don't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero deps.&lt;/strong&gt; Nothing outside Zig stdlib. The JSON parser is hand-rolled (recursive descent) in &lt;code&gt;src/json.zig&lt;/code&gt;, surrogate-pair handling included, in a few hundred lines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OPA's wasm is large because it carries the Go runtime (with GC), the Rego parser, and the evaluator. Zopa took the opposite call on every point. The result is 60 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory model
&lt;/h2&gt;

&lt;p&gt;Zopa's heart is the memory layout. Two allocators, with different lifetimes and roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviuwqw6lqy5ld5c3z2e5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviuwqw6lqy5ld5c3z2e5.png" alt="memory model" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;host_allocator&lt;/code&gt; is Zig stdlib's &lt;code&gt;std.heap.wasm_allocator&lt;/code&gt;, a freelist-style allocator. It backs every buffer that crosses the host boundary. Lifetime: the whole module.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;request_arena&lt;/code&gt; is &lt;code&gt;std.heap.ArenaAllocator&lt;/code&gt;. Per-request scratch space. We call &lt;code&gt;reset(.retain_capacity)&lt;/code&gt; at the end of &lt;code&gt;evaluate()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An arena means "alloc as much as you want; everything goes away at the end". No individual &lt;code&gt;free&lt;/code&gt; calls. With &lt;code&gt;retain_capacity&lt;/code&gt;, the wasm linear memory pages aren't returned, so the next request reuses the existing capacity.&lt;/p&gt;

&lt;p&gt;Net effect: after warmup, &lt;code&gt;memory.grow&lt;/code&gt; (the wasm heap-grow instruction) stops firing. Throughput goes up; memory stays roughly flat. That's the source of that property.&lt;/p&gt;

&lt;p&gt;The single rule that ties it together: &lt;strong&gt;a pointer minted by one allocator must only be released by the matching free path&lt;/strong&gt;. The proxy-wasm shim returns host-malloc'd buffers via the host's free; the evaluator never calls &lt;code&gt;free&lt;/code&gt; directly and instead leans on the arena reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-phase target rules
&lt;/h2&gt;

&lt;p&gt;Zopa makes the decision independently in three HTTP phases. Each phase is bound to &lt;strong&gt;a different target rule name&lt;/strong&gt;; if your policy contains a rule with that name, the phase fires; if not, the phase passes through silently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Target rule&lt;/th&gt;
&lt;th&gt;Input shape&lt;/th&gt;
&lt;th&gt;On deny&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;proxy_on_request_headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{method, path, headers}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;proxy_on_request_body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allow_body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{body, body_raw}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;403 + Pause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;proxy_on_response_headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allow_response&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{response: {status, headers}}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point: a policy with only an &lt;code&gt;allow&lt;/code&gt; rule sails past the body and response phases untouched. At configure time we parse the policy JSON and remember whether &lt;code&gt;allow_body&lt;/code&gt; / &lt;code&gt;allow_response&lt;/code&gt; rules exist as bools; if they don't, the matching callbacks return &lt;code&gt;Continue&lt;/code&gt; directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens during one request
&lt;/h2&gt;

&lt;p&gt;When Envoy hands a single request to zopa, this is the timeline inside.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fintroducing-zopa%2Frequest-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0-draft%2Fdev.to%2Fmain%2Farticles%2Fassets%2Fintroducing-zopa%2Frequest-flow.png" alt="request flow" width="800" height="1798"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The policy AST is &lt;strong&gt;handed to us once at startup&lt;/strong&gt; and copied into host_allocator. We don't re-receive it per request.&lt;/li&gt;
&lt;li&gt;Every phase ends with &lt;code&gt;arena.reset&lt;/code&gt;. Zopa carries no data across phase or request boundaries.&lt;/li&gt;
&lt;li&gt;The body and response phases only run eval &lt;strong&gt;when the matching rule exists&lt;/strong&gt;. The policy opts each phase in by writing a rule with that name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;evaluate()&lt;/code&gt; returns a plain &lt;code&gt;i32&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Return&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;allow. The target rule fired with a truthy value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;deny. No rule fired and no truthy default rule was present.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;error. Parse failure, unknown node, recursion cap, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The proxy-wasm shim treats &lt;code&gt;-1&lt;/code&gt; the same as deny (broken policies block by default).&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy AST
&lt;/h2&gt;

&lt;p&gt;Zopa's input isn't Rego source; it's AST-shaped JSON. The supported nodes mirror a subset of Rego.&lt;/p&gt;

&lt;p&gt;"&lt;code&gt;role&lt;/code&gt; equals &lt;code&gt;admin&lt;/code&gt; → allow" looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"left"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"right"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The evaluation rule is "OR every rule whose &lt;code&gt;name&lt;/code&gt; is &lt;code&gt;"allow"&lt;/code&gt;". If any body holds, &lt;code&gt;allow=true&lt;/code&gt;. A &lt;code&gt;default=true&lt;/code&gt; rule's value is the fallback for when nothing else fires.&lt;/p&gt;

&lt;p&gt;Supported node types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Literal (any JSON value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ref&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path lookup, e.g. walking &lt;code&gt;input.user.role&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Binary compare. &lt;code&gt;eq&lt;/code&gt; / &lt;code&gt;neq&lt;/code&gt; / &lt;code&gt;lt&lt;/code&gt; / &lt;code&gt;lte&lt;/code&gt; / &lt;code&gt;gt&lt;/code&gt; / &lt;code&gt;gte&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logical negation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set literal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;some&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Existential quantifier: "some element x makes body true"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;every&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Universal quantifier: "every element x makes body true"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builtin function call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;module&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A set of rules. Optional &lt;code&gt;package&lt;/code&gt; field carries the package name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Module bundle: multiple packages co-resident in a single VM.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single rule with &lt;code&gt;body&lt;/code&gt; (AND) and &lt;code&gt;value&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;call&lt;/code&gt; ships with these four builtins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Args&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startswith&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(string, string)&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endswith&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(string, string)&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(string, string)&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(array / set / object / string)&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;some&lt;/code&gt; / &lt;code&gt;every&lt;/code&gt; also iterate over JSON objects: pick &lt;code&gt;kind: "keys"&lt;/code&gt; (default) or &lt;code&gt;kind: "values"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Zopa doesn't reach the full Rego (user-defined functions, &lt;code&gt;with&lt;/code&gt; clauses, partial evaluation, imports, etc.). The scope is "decide allow/deny at the edge". Full reference: &lt;a href="https://github.com/0-draft/zopa/blob/main/docs/ast.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/ast.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build
&lt;/h3&gt;

&lt;p&gt;You need Zig 0.16.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;zig
git clone https://github.com/0-draft/zopa
&lt;span class="nb"&gt;cd &lt;/span&gt;zopa
zig build &lt;span class="nt"&gt;--release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Drive it directly (Node.js)
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;evaluate(input, ast)&lt;/code&gt; without going through proxy-wasm. Useful as a smoke test before standing up Envoy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;WebAssembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zig-out/bin/zopa.wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;proxy_log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;proxy_get_buffer_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;proxy_get_header_map_pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;proxy_get_header_map_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;proxy_send_local_response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;free&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;il&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;al&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compare&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ref&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;il&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;al&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 1 (allow)&lt;/span&gt;
&lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;proxy_*&lt;/code&gt; stubs in &lt;code&gt;env&lt;/code&gt; are there because proxy-wasm imports must resolve before the wasm module instantiates. &lt;code&gt;evaluate&lt;/code&gt; itself doesn't call into them, so dummies are fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  As an Envoy proxy-wasm filter
&lt;/h3&gt;

&lt;p&gt;Drop the wasm into &lt;code&gt;http_filters&lt;/code&gt;. Two important fields: &lt;code&gt;vm_config&lt;/code&gt; and &lt;code&gt;configuration&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;http_filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;envoy.filters.http.wasm&lt;/span&gt;
    &lt;span class="na"&gt;typed_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@type"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm&lt;/span&gt;
      &lt;span class="s"&gt;config&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@type"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type.googleapis.com/google.protobuf.StringValue&lt;/span&gt;
          &lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{"type":"module","rules":[&lt;/span&gt;
              &lt;span class="s"&gt;{"type":"rule","name":"allow","default":true,&lt;/span&gt;
               &lt;span class="s"&gt;"value":{"type":"value","value":false}},&lt;/span&gt;
              &lt;span class="s"&gt;{"type":"rule","name":"allow","body":[&lt;/span&gt;
                &lt;span class="s"&gt;{"type":"eq",&lt;/span&gt;
                 &lt;span class="s"&gt;"left":{"type":"ref","path":["input","method"]},&lt;/span&gt;
                 &lt;span class="s"&gt;"right":{"type":"value","value":"GET"}}]}&lt;/span&gt;
            &lt;span class="s"&gt;]}&lt;/span&gt;
        &lt;span class="na"&gt;vm_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;envoy.wasm.runtime.v8&lt;/span&gt;
          &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/zopa/zopa.wasm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;configuration.value&lt;/code&gt; is the policy AST as JSON. The example reads "GET passes; everything else denies".&lt;/p&gt;

&lt;p&gt;A complete end-to-end sample lives in &lt;a href="https://github.com/0-draft/zopa/tree/main/examples/envoy" rel="noopener noreferrer"&gt;&lt;code&gt;examples/envoy/&lt;/code&gt;&lt;/a&gt;; &lt;code&gt;zig build test-envoy&lt;/code&gt; runs curl assertions against a real Envoy. CI exercises Node, wasmtime, and a real Envoy on every commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container image
&lt;/h3&gt;

&lt;p&gt;There's also a distroless OCI image. Multi-arch (amd64 / arm64) and cosign keyless-signed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/0-draft/zopa:v0.2.0
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;ghcr.io/0-draft/zopa:v0.2.0 &lt;span class="nt"&gt;-lh&lt;/span&gt; /zopa.wasm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intended use is staging &lt;code&gt;/zopa.wasm&lt;/code&gt; from an initContainer into the Envoy sidecar pod.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;zig build bench&lt;/code&gt; runs a zopa-only latency benchmark. On a local M-series Mac with &lt;code&gt;--release=small&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixture                 |    p50 |    p95 |    p99 |   mean
------------------------+--------+--------+--------+-------
01_static               |  1.79  |  2.96  |  3.46  |  1.73  (μs)
02_header_eq            |  4.42  |  4.96  |  5.17  |  4.48  (μs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Literal &lt;code&gt;true&lt;/code&gt; policy: 1.79 μs at p50. A simple &lt;code&gt;input.method == "GET"&lt;/code&gt; style compare: 4.42 μs at p50. Wall-clock direct measurement, 10 000 iterations after 1 000 warmup. No head-to-head against OPA / Cedar yet; cross-engine numbers wait until the conformance corpus is wide enough to honestly assert "same answer".&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits relative to alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;OPA&lt;/th&gt;
&lt;th&gt;Cedar&lt;/th&gt;
&lt;th&gt;Casbin&lt;/th&gt;
&lt;th&gt;zopa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Go (+ ports)&lt;/td&gt;
&lt;td&gt;Zig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm distribution&lt;/td&gt;
&lt;td&gt;Yes (heavy)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (~60KB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory model&lt;/td&gt;
&lt;td&gt;GC&lt;/td&gt;
&lt;td&gt;RC + arenas&lt;/td&gt;
&lt;td&gt;GC&lt;/td&gt;
&lt;td&gt;per-request arena&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;proxy-wasm&lt;/td&gt;
&lt;td&gt;Side project&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;First-class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy input&lt;/td&gt;
&lt;td&gt;Rego source&lt;/td&gt;
&lt;td&gt;Cedar source&lt;/td&gt;
&lt;td&gt;CSV / source&lt;/td&gt;
&lt;td&gt;Compiled AST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maturity&lt;/td&gt;
&lt;td&gt;CNCF Graduated&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;td&gt;Mature&lt;/td&gt;
&lt;td&gt;Alpha&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zopa isn't a replacement for OPA when you need full Rego, the management plane, bundle distribution, partial evaluation, or the state API. Use OPA for those. Zopa solves a narrow case: "I can compile the policy elsewhere and just want to evaluate at the edge". For that case, the wasm binary is two orders of magnitude smaller than OPA's.&lt;/p&gt;

&lt;p&gt;It fits when "Rego-ish syntax, but OPA is too heavy" and "Cedar / Casbin can't go to wasm" line up at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it and tell me
&lt;/h2&gt;

&lt;p&gt;Zopa's source is 8 files under &lt;code&gt;src/&lt;/code&gt;, no deps outside stdlib, readable top to bottom. Sized so that if you want to change something, you can fork and rewrite.&lt;/p&gt;

&lt;p&gt;Feedback I'd love:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"&lt;code&gt;tools/rego2ast.py&lt;/code&gt; rejects my policy with Unsupported on this node, please add it"&lt;/li&gt;
&lt;li&gt;"proxy-wasm host X (Istio / Kong / APISIX) worked / didn't work like this"&lt;/li&gt;
&lt;li&gt;"Cases I'd add to the conformance corpus"&lt;/li&gt;
&lt;li&gt;"The &lt;code&gt;allow_body&lt;/code&gt; 64 KiB cap is too small / too large"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/0-draft/zopa" rel="noopener noreferrer"&gt;https://github.com/0-draft/zopa&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issues: &lt;a href="https://github.com/0-draft/zopa/issues" rel="noopener noreferrer"&gt;https://github.com/0-draft/zopa/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;v0.2.0 release: &lt;a href="https://github.com/0-draft/zopa/releases/tag/v0.2.0" rel="noopener noreferrer"&gt;https://github.com/0-draft/zopa/releases/tag/v0.2.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OCI image: &lt;code&gt;ghcr.io/0-draft/zopa:v0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;proxy-wasm spec: &lt;a href="https://github.com/proxy-wasm/spec" rel="noopener noreferrer"&gt;https://github.com/proxy-wasm/spec&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webassembly</category>
      <category>authorization</category>
      <category>zig</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
