<?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: RuntimeStack</title>
    <description>The latest articles on DEV Community by RuntimeStack (@runtimestack).</description>
    <link>https://dev.to/runtimestack</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200951%2F145bcabe-7a5c-43c7-9178-ed3f15344cab.png</url>
      <title>DEV Community: RuntimeStack</title>
      <link>https://dev.to/runtimestack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/runtimestack"/>
    <language>en</language>
    <item>
      <title>How Passkeys Work: Why a Perfect Phishing Page Gets Nothing (Plus Lost Phones and the "Trap" Debate)</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Wed, 28 Oct 2026 17:02:30 +0000</pubDate>
      <link>https://dev.to/runtimestack/how-passkeys-work-why-a-perfect-phishing-page-gets-nothing-plus-lost-phones-and-the-trap-debate-40hj</link>
      <guid>https://dev.to/runtimestack/how-passkeys-work-why-a-perfect-phishing-page-gets-nothing-plus-lost-phones-and-the-trap-debate-40hj</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/5Yto8sCJi_g" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This login page is fake... and it's perfect. Your password would walk right in.&lt;/p&gt;

&lt;p&gt;So how do you log in, when the site never gets a secret?&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz405cn45yjoeu3h80krx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz405cn45yjoeu3h80krx.png" alt="A pixel-identical fake login page at runtimebank-secure.com: the typed password goes to the attacker, and the passkey button is crossed out" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One analogy makes it click: a signet ring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What passkeys replace
&lt;/h2&gt;

&lt;p&gt;First, what passkeys replace: the password. Whoever has your password logs in as you, from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ring and the impression
&lt;/h2&gt;

&lt;p&gt;Picture a signet ring and its wax impression. A passkey works the same way, with a pair of keys.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Passkey&lt;/th&gt;
&lt;th&gt;Signet ring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;private key&lt;/td&gt;
&lt;td&gt;the ring: it stays on your devices, never with the site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public key&lt;/td&gt;
&lt;td&gt;the impression: that's all the site stores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;signature&lt;/td&gt;
&lt;td&gt;the stamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;challenge&lt;/td&gt;
&lt;td&gt;a blank card with a random number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;site address (RP ID)&lt;/td&gt;
&lt;td&gt;the engraving on the ring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Where the analogy breaks: a real ring can be copied from wax.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfxgkdze4ncpenop1vvz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfxgkdze4ncpenop1vvz.png" alt="A pair of keys: the private key (the ring) stays on your device, the public key (the impression) goes to the website, and the stamp matches" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a passkey
&lt;/h2&gt;

&lt;p&gt;The site gets the impression, the public key, and that's ALL it stores.&lt;/p&gt;

&lt;p&gt;So if that server gets breached, the attacker gets a pile of impressions.&lt;/p&gt;

&lt;p&gt;But if the site never gets anything secret... how does it know it's really you?&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing in
&lt;/h2&gt;

&lt;p&gt;The site sends a challenge: a blank card with a random number it's never sent before.&lt;/p&gt;

&lt;p&gt;Your device stamps it with the ring. That stamp is the signature.&lt;/p&gt;

&lt;p&gt;The site checks the stamp against the impression on file. Match? You're in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runtimebank.com  --  blank card: 8f3a…c91  --&amp;gt;  your phone
runtimebank.com  &amp;lt;--  signature (the stamp)  --  your phone
checked against the public key  -&amp;gt;  you're in
secrets sent: 0
&lt;/code&gt;&lt;/pre&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysh8v365px908fvbwd6n.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysh8v365px908fvbwd6n.png" alt="Sign-in sequence: the site sends a challenge, the phone signs it with the private key, the site checks it against the public key, zero secrets sent" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the fake page gets nothing
&lt;/h2&gt;

&lt;p&gt;Every ring is engraved with the site it was made for: &lt;code&gt;runtimebank.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And your browser, not the ring, checks the real address before it brings one out.&lt;/p&gt;

&lt;p&gt;The fake site is &lt;code&gt;runtimebank-secure.com&lt;/code&gt;. One word off, so no ring matches... so there's nothing to offer. Your passkey can't be phished.&lt;/p&gt;

&lt;p&gt;And even if a stamp somehow got made there, the browser writes the address onto the card first... so it carries the fake address, and the real site rejects it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;origin:   runtimebank-secure.com
rings:    runtimebank.com ≠   shop.com ≠   mail.com ≠
passkey secrets sent: 0
&lt;/code&gt;&lt;/pre&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feya6xe6ubm4vfvw7gk1a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feya6xe6ubm4vfvw7gk1a.png" alt="The browser, the clerk, checks the origin runtimebank-secure.com against the ring engravings; none match, so the passkey can't be phished" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who shows the popup?
&lt;/h2&gt;

&lt;p&gt;So your browser picks the ring. But who's holding them, and who's showing you that popup?&lt;/p&gt;

&lt;p&gt;That popup is your browser or phone, not the website.&lt;/p&gt;

&lt;p&gt;The website asks your browser, and your browser asks your passkey provider. That's whatever holds your rings: iCloud Keychain, Google Password Manager, 1Password, Bitwarden, or a hardware key.&lt;/p&gt;

&lt;p&gt;Signing in on a laptop with your phone? You scan a QR code, and Bluetooth checks it's nearby.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F49vixq32jiqvnrg72ekd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F49vixq32jiqvnrg72ekd.png" alt="Website asks the browser or phone, which shows the passkey popup and asks the passkey provider; a laptop signs in with a phone by QR code and Bluetooth" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lost phone: synced vs device-bound
&lt;/h2&gt;

&lt;p&gt;But here's the catch with a ring that lives on one phone. Lose the phone, lose the ring.&lt;/p&gt;

&lt;p&gt;So passkeys made on a phone or in a password manager are usually synced. Your provider copies them, end-to-end encrypted, to your other devices.&lt;/p&gt;

&lt;p&gt;Phone gone? Sign in to your provider on a new one, and your rings come back.&lt;/p&gt;

&lt;p&gt;That makes your provider account the master key, so protect it well.&lt;/p&gt;

&lt;p&gt;A hardware key is device-bound, with no copies. So register two... and keep one in a drawer.&lt;/p&gt;

&lt;p&gt;![Lost phone: the provider restores end-to-end encrypted passkeys to a new phone; the provider account is the master key; a hardware key is device-bound]&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmj8sfsutbfbytby2klb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmj8sfsutbfbytby2klb.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Face, fingerprint, PIN
&lt;/h2&gt;

&lt;p&gt;Your face or PIN unlocks the ring box, right there on the device.&lt;/p&gt;

&lt;p&gt;Your fingerprint and face never leave the phone, and the site never sees them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stolen phone
&lt;/h2&gt;

&lt;p&gt;A leaked password works for anyone, from anywhere, without ever touching your phone. So the phone itself is what to protect.&lt;/p&gt;

&lt;p&gt;Here's what to actually do: a long passcode, not four digits, and remote lock and wipe turned on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The password fallback
&lt;/h2&gt;

&lt;p&gt;One more honest catch... most sites still keep a password or an email reset as a backup.&lt;/p&gt;

&lt;p&gt;That's a spare key under the mat, and attackers always check the mat.&lt;/p&gt;

&lt;p&gt;So if a site where you use a passkey suddenly asks for your password... stop. Assume it's the fake page, and type the address yourself.&lt;/p&gt;

&lt;p&gt;And if a site lets you remove the password, do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock-in
&lt;/h2&gt;

&lt;p&gt;And switching providers? The FIDO Alliance, the group behind passkeys, is rolling out Credential Exchange. It's a standard way to move passkeys between managers, encrypted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are passkeys a trap?
&lt;/h2&gt;

&lt;p&gt;The viral argument is about one optional feature, attestation.&lt;/p&gt;

&lt;p&gt;A site can ask for proof of what made your passkey, signed by the maker.&lt;/p&gt;

&lt;p&gt;Now the fact check. Synced passkeys from Apple, Google, 1Password and Bitwarden send no signed proof at all.&lt;/p&gt;

&lt;p&gt;So it's worth watching... but a consumer site that required it today would lock out almost everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Your private key is the ring. It stays on your devices, never with the site.&lt;/p&gt;

&lt;p&gt;The public key is the impression. That's all the site stores.&lt;/p&gt;

&lt;p&gt;The honest part: lock your phone down, and never type a password where your passkey should be.&lt;/p&gt;

&lt;p&gt;That fake page? Your password would've walked right in. Your passkey never even showed up.&lt;/p&gt;

&lt;p&gt;What's still stopping you from turning passkeys on? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;More in this series: &lt;a href="https://youtu.be/Kl3LZRcJsbM" rel="noopener noreferrer"&gt;JOSE and JWT&lt;/a&gt;, &lt;a href="https://youtu.be/R6RMOEuP2u4" rel="noopener noreferrer"&gt;PKCE&lt;/a&gt;, &lt;a href="https://youtu.be/nw-J8J6-p2A" rel="noopener noreferrer"&gt;Device Flow&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 7-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://passkeys.dev/docs/intro/what-are-passkeys/" rel="noopener noreferrer"&gt;passkeys.dev: What are passkeys?&lt;/a&gt;, &lt;a href="https://passkeys.dev/docs/reference/terms/" rel="noopener noreferrer"&gt;passkeys.dev: Terminology&lt;/a&gt;, &lt;a href="https://www.w3.org/TR/webauthn-3/" rel="noopener noreferrer"&gt;W3C Web Authentication Level 3&lt;/a&gt;, &lt;a href="https://fidoalliance.org/passkeys/" rel="noopener noreferrer"&gt;FIDO Alliance: Passkeys&lt;/a&gt;, &lt;a href="https://fidoalliance.org/fido-alliance-publishes-new-specifications-to-promote-user-choice-and-enhanced-ux-for-passkeys/" rel="noopener noreferrer"&gt;FIDO Alliance: Credential Exchange specifications&lt;/a&gt;, &lt;a href="https://nonfunctional.substack.com/p/passkeys-are-a-trap-do-not-use-them" rel="noopener noreferrer"&gt;"Passkeys are a trap" (nonfunctional)&lt;/a&gt;, &lt;a href="https://scotthelme.co.uk/passkeys-are-not-a-trap/" rel="noopener noreferrer"&gt;Scott Helme: "Passkeys are not a trap"&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>passkeys</category>
      <category>webauthn</category>
      <category>security</category>
      <category>authentication</category>
    </item>
    <item>
      <title>How AI Reverse Engineering Really Works: REA, Ghidra and angr on One 17-Byte Function</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:58:22 +0000</pubDate>
      <link>https://dev.to/runtimestack/how-ai-reverse-engineering-really-works-rea-ghidra-and-angr-on-one-17-byte-function-4h5b</link>
      <guid>https://dev.to/runtimestack/how-ai-reverse-engineering-really-works-rea-ghidra-and-angr-on-one-17-byte-function-4h5b</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/IF_1lFy7YM8" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;AI doesn't read machine code.&lt;/p&gt;

&lt;p&gt;So what's it actually reading? And those viral Adobe clones? Their builder says they never used REA.&lt;/p&gt;

&lt;p&gt;There are two ways to copy an app, like those Adobe clones: watch it from the outside, or open it up. We'll open one up, one real function at a time, and see exactly where the AI comes in. Plus the two places it quietly breaks.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwborp8jttkqxabxgl2pv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwborp8jttkqxabxgl2pv.png" alt="A hex dump of a compiled app in a terminal, with the rea command typed below it" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling is baking
&lt;/h2&gt;

&lt;p&gt;Think of compiling as baking.&lt;/p&gt;

&lt;p&gt;Your source code is the recipe. The compiler bakes it into a binary.&lt;/p&gt;

&lt;p&gt;Comments never make it into the cake. And for a release, most apps strip out the function names too.&lt;/p&gt;

&lt;p&gt;Here's the recipe we'll bake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&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;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&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;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc &lt;span class="nt"&gt;-O1&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; app main.c
strip app
nm app
&lt;span class="c"&gt;# nm: app: no symbols&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you ship the cake. Not the recipe. You can't get the recipe back. But a good food lab gets surprisingly close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the binary: seventeen bytes
&lt;/h2&gt;

&lt;p&gt;So let's open the cake. This is that exact function, from the stripped app.&lt;/p&gt;

&lt;p&gt;The whole thing is seventeen bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f3 0f 1e fa 83 ff 0a 7f 04 6b c7 64 c3 6b c7 5a c3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To you, it's noise. To the CPU, it's a list of instructions. The only question is: where does each instruction start?&lt;/p&gt;

&lt;h2&gt;
  
  
  The disassembler: bytes to assembly
&lt;/h2&gt;

&lt;p&gt;A disassembler maps them to instructions.&lt;/p&gt;

&lt;p&gt;The first four are a security marker. Skip those. &lt;code&gt;83 ff 0a&lt;/code&gt;: compare the input with 10. &lt;code&gt;7f 04&lt;/code&gt;: if it's bigger, jump. Otherwise multiply by a hundred, and after the jump, by ninety.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ objdump -d app
1169:  f3 0f 1e fa   endbr64                  # landing marker, skip
116d:  83 ff 0a      cmp    $0xa,%edi         # compare qty with 10
1170:  7f 04         jg     1176              # if bigger, jump
1172:  6b c7 64      imul   $0x64,%edi,%eax   # times 100
1175:  c3            ret
1176:  6b c7 5a      imul   $0x5a,%edi,%eax   # times 90
1179:  c3            ret
&lt;/code&gt;&lt;/pre&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8234ztiq2zsts53w2j3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8234ztiq2zsts53w2j3.png" alt="The 17 bytes split into instructions, with the objdump listing and a comment on each line" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The decompiler: assembly to C
&lt;/h2&gt;

&lt;p&gt;Assembly's still slow to read. So, tool two: a decompiler.&lt;/p&gt;

&lt;p&gt;The famous one is Ghidra, from the NSA. Here we'll use another free one, called angr. Same idea.&lt;/p&gt;

&lt;p&gt;First, each instruction becomes tiny steps, every side effect spelled out. Ghidra calls it &lt;strong&gt;P-code&lt;/strong&gt;. That one compare? Ten steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight syzprog"&gt;&lt;code&gt;&lt;span class="err"&gt;unique[78b00:4]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;EDI&lt;/span&gt;
&lt;span class="err"&gt;CF&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[78b00:4]&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;0xa&lt;/span&gt;
&lt;span class="err"&gt;OF&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sborrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="n"&gt;b00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mh"&gt;0xa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;unique[78d00:4]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[78b00:4]&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;0xa&lt;/span&gt;
&lt;span class="err"&gt;SF&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[78d00:4]&lt;/span&gt; &lt;span class="err"&gt;s&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;0x0&lt;/span&gt;
&lt;span class="err"&gt;ZF&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[78d00:4]&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;0x0&lt;/span&gt;
&lt;span class="err"&gt;unique[58300:4]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[78d00:4]&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;0xff&lt;/span&gt;
&lt;span class="err"&gt;unique[58400:1]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;popcount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;58300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="err"&gt;unique[58500:1]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[58400:1]&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;0x1&lt;/span&gt;
&lt;span class="err"&gt;PF&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;unique[58500:1]&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;0x0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it throws away the ones nothing uses. Then it spots the patterns. Compare plus jump is an &lt;code&gt;if&lt;/code&gt;, and out comes C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sub_401169&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a0&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&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;a0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&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;a0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What came back: the logic, but no names
&lt;/h2&gt;

&lt;p&gt;Look at what came back, though.&lt;/p&gt;

&lt;p&gt;The logic is exactly right. But angr calls the function &lt;code&gt;sub_401169&lt;/code&gt;. The input is called &lt;code&gt;a0&lt;/code&gt;. And nothing says why 90, and not 100.&lt;/p&gt;

&lt;p&gt;Every ingredient, every step. Not one note from the recipe.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdewgm78ipv6jp6vax0m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdewgm78ipv6jp6vax0m.png" alt="angr's decompiler output: the correct if/else logic, but the function is sub_401169 and the input is a0" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now picture thousands of functions like that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI adds: the chef's guesses
&lt;/h2&gt;

&lt;p&gt;So if the tools did the hard part, what's the AI for?&lt;/p&gt;

&lt;p&gt;Reading. The model has seen millions of functions that look a lot like this one. So it renames &lt;code&gt;a0&lt;/code&gt; to &lt;code&gt;qty&lt;/code&gt;, the function to &lt;code&gt;price&lt;/code&gt;, and 90 to a bulk discount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- unsigned int sub_401169(int a0)
&lt;/span&gt;&lt;span class="gi"&gt;+ int price(int qty)
&lt;/span&gt;  {
&lt;span class="gd"&gt;-     if (a0 &amp;gt; 10)
-         return a0 * 90;
-     return a0 * 100;
&lt;/span&gt;&lt;span class="gi"&gt;+     if (qty &amp;gt; 10)
+         return qty * 90;   // bulk discount?
+     return qty * 100;
&lt;/span&gt;  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those names are guesses. Good ones. But guesses.&lt;/p&gt;

&lt;p&gt;In the kitchen, that's the chef reading the lab report and saying: brown butter.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faezeltg7n8se3q1o5q1s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faezeltg7n8se3q1o5q1s.png" alt="The model's guesses as a diff over angr's output: price(int qty) and a bulk discount comment" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is REA?
&lt;/h2&gt;

&lt;p&gt;REA is an &lt;strong&gt;MCP server&lt;/strong&gt;: a standard plug that lets an agent like Claude Code drive those tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent  &amp;lt;-&amp;gt; MCP &amp;lt;-&amp;gt;  REA  &amp;lt;-&amp;gt;  Ghidra | Hopper | IDA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For native apps, it drives Ghidra, Hopper or IDA. It doesn't ship a decompiler of its own.&lt;/p&gt;

&lt;p&gt;The analysis runs locally. The model, cloud or local, only sees the findings REA sends back.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhs5lmn69utaldh4i6vmq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhs5lmn69utaldh4i6vmq.png" alt="How REA fits: the agent talks MCP to REA, which drives Ghidra, Hopper or IDA, all on your machine" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild, test, repeat
&lt;/h2&gt;

&lt;p&gt;But a guess isn't proof. So the agent rebuilds the function from its renamed copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// bulk = 10 or more?&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;90&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;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it runs both, the original and the rebuild, on the same inputs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Rebuild&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;500 ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;900 ✗&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bulk discount sounded like ten or more, so it wrote that. Its own guess leaked in.&lt;/p&gt;

&lt;p&gt;The test catches it. Decompile, guess, rebuild, test, repeat.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfuibj5y2z951zaqoy61.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfuibj5y2z951zaqoy61.png" alt="The agent's rebuild with qty &gt;= 10, run next to the original: input 10 gives 1000 vs 900" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks, part one: obfuscation
&lt;/h2&gt;

&lt;p&gt;Decompilers work because compilers write code in familiar patterns. Protectors break them on purpose.&lt;/p&gt;

&lt;p&gt;VMProtect turns your code into bytecode for a made-up CPU, and ships the interpreter inside the app.&lt;/p&gt;

&lt;p&gt;It's not a cake anymore. It's a cake baked inside another cake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks, part two: the agent's own report
&lt;/h2&gt;

&lt;p&gt;The second place is quieter. It's the agent's own report. Picture a summary that says: tested every edge case, matches the original. (The video labels this report as illustrative.)&lt;/p&gt;

&lt;p&gt;Did it? The log says five and eleven. That's it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat test.log
5  -&amp;gt; 500 ✓
11 -&amp;gt; 990 ✓
# no line for 10: it never ran
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A summary isn't evidence. The test log is. So you still steer, and you ask for the edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth the tokens?
&lt;/h2&gt;

&lt;p&gt;Not always. In one informal test, a plain agent working from screenshots finished first. And its copy was only slightly behind. Nobody's published a token count yet.&lt;/p&gt;

&lt;p&gt;If it's all visible on screen, watching is often enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Adobe clones: they watched
&lt;/h2&gt;

&lt;p&gt;According to their builder, that's exactly what the Adobe clones did. They watched. Public specs and the apps' visible behaviour. No decompiling, by their own rules.&lt;/p&gt;

&lt;p&gt;That's the idea behind &lt;strong&gt;clean-room design&lt;/strong&gt;: build from the spec, never from the original code.&lt;/p&gt;

&lt;p&gt;In kitchen terms? They tasted the cake in the shop, and never sent it to the lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it legal?
&lt;/h2&gt;

&lt;p&gt;Decompiling the original and shipping what you find is a much riskier legal story.&lt;/p&gt;

&lt;p&gt;REA's own readme limits it to lawful research, interoperability, and software you're authorized to examine.&lt;/p&gt;

&lt;p&gt;This isn't legal advice, but know which kitchen you're cooking in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kitchen&lt;/th&gt;
&lt;th&gt;Reverse engineering&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the cake&lt;/td&gt;
&lt;td&gt;the binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reading its ingredients, exactly&lt;/td&gt;
&lt;td&gt;the disassembler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rebuilding the steps, but not the names&lt;/td&gt;
&lt;td&gt;the decompiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the chef, guessing the names and the why&lt;/td&gt;
&lt;td&gt;the AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the taste test&lt;/td&gt;
&lt;td&gt;the tests: the only part you should really trust&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And for the Adobe clones: would you have watched, or opened it up? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;More in this series: &lt;a href="https://youtu.be/pvh4lbc0Ssc" rel="noopener noreferrer"&gt;AI agent sandboxing&lt;/a&gt;, &lt;a href="https://youtu.be/ciApeS1fyc8" rel="noopener noreferrer"&gt;reward hacking&lt;/a&gt;, &lt;a href="https://youtu.be/rIzbixCoSEs" rel="noopener noreferrer"&gt;Pi 1.0 and Pi Durable&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 7-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://github.com/morluto/rea" rel="noopener noreferrer"&gt;REA README&lt;/a&gt;, &lt;a href="https://www.mindstudio.ai/blog/reverse-engineer-anything-mcp/" rel="noopener noreferrer"&gt;MindStudio blog&lt;/a&gt;, &lt;a href="https://github.com/NationalSecurityAgency/ghidra" rel="noopener noreferrer"&gt;Ghidra README&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Ghidra" rel="noopener noreferrer"&gt;Ghidra (Wikipedia)&lt;/a&gt;, &lt;a href="https://ghidra.re/ghidra_docs/languages/html/pcoderef.html" rel="noopener noreferrer"&gt;P-code reference&lt;/a&gt;, &lt;a href="https://link.springer.com/chapter/10.1007/978-3-031-25803-9_7" rel="noopener noreferrer"&gt;Springer chapter&lt;/a&gt;, &lt;a href="https://github.com/angr/pypcode" rel="noopener noreferrer"&gt;pypcode&lt;/a&gt;, &lt;a href="https://docs.angr.io/en/latest/analyses/decompiler.html" rel="noopener noreferrer"&gt;angr decompiler&lt;/a&gt;, &lt;a href="https://www.kucoin.com/news/flash/artcraft-founder-refuses-to-use-rea-despite-its-popularity-in-reverse-engineering" rel="noopener noreferrer"&gt;KuCoin news: ArtCraft founder and REA&lt;/a&gt;, &lt;a href="https://innfactory.ai/en/blog/ai-rebuilt-photoshop-word-reverse-engineering/" rel="noopener noreferrer"&gt;innFactory: AI rebuilt Photoshop&lt;/a&gt;, &lt;a href="https://petapixel.com/2026/10/07/someone-rebuilt-free-open-source-versions-of-photoshop-premiere-and-lightroom-using-ai/" rel="noopener noreferrer"&gt;PetaPixel&lt;/a&gt;, &lt;a href="https://www.tweaktown.com/news/113989/developer-says-software-is-over-after-using-ai-to-recreate-adobes-entire-creative-cloud-suite-for-free/index.html" rel="noopener noreferrer"&gt;TweakTown&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Clean_room_design" rel="noopener noreferrer"&gt;Clean room design&lt;/a&gt;, &lt;a href="https://www.felixcloutier.com/x86/" rel="noopener noreferrer"&gt;x86 reference&lt;/a&gt;, &lt;a href="https://www.felixcloutier.com/x86/endbr64" rel="noopener noreferrer"&gt;ENDBR64&lt;/a&gt;, &lt;a href="https://back.engineering/blog/17/05/2021/" rel="noopener noreferrer"&gt;back.engineering blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI Agent Identity Explained: SPIFFE, SPIRE and OPA Instead of API Keys</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:07:00 +0000</pubDate>
      <link>https://dev.to/runtimestack/ai-agent-identity-explained-spiffe-spire-and-opa-instead-of-api-keys-5hb8</link>
      <guid>https://dev.to/runtimestack/ai-agent-identity-explained-spiffe-spire-and-opa-instead-of-api-keys-5hb8</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Q3wdpWt09UM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Many AI agents still run on a static API key: long-lived, and hard to rotate.&lt;/p&gt;

&lt;p&gt;It's a bearer secret, not bound to anything. Whoever copies it can act as your agent.&lt;/p&gt;

&lt;p&gt;The fix is a passport that expires within the hour.&lt;/p&gt;

&lt;p&gt;SPIFFE, SPIRE, SVIDs, OPA, Rego. Alphabet soup. One airport analogy makes it click, plus why a perfect ID won't stop your agent wiping a database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions: who are you, and may you board?
&lt;/h2&gt;

&lt;p&gt;An airport asks two questions: who are you, and may you board?&lt;/p&gt;

&lt;p&gt;SPIFFE and SPIRE issue the passport that answers the first. OPA runs the gate.&lt;/p&gt;

&lt;p&gt;SPIFFE is the standard. SPIRE implements it; meshes like Istio issue SPIFFE IDs too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SPIFFE ID: the name in the passport
&lt;/h2&gt;

&lt;p&gt;Your agent's name is a SPIFFE ID:&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.org/agents/billing
         └─────────┘└─────────────┘
         trust domain     path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, the &lt;strong&gt;trust domain&lt;/strong&gt;: the trust root, like the country that issues a passport. Then the &lt;strong&gt;path&lt;/strong&gt;, naming this workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SVID: the passport
&lt;/h2&gt;

&lt;p&gt;So what makes a name proof? A signature.&lt;/p&gt;

&lt;p&gt;Signed, it becomes an &lt;strong&gt;SVID&lt;/strong&gt;: a SPIFFE Verifiable Identity Document. It comes in two forms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SVID type&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;th&gt;Lifetime in the video&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X.509 certificate&lt;/td&gt;
&lt;td&gt;mTLS&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;bearer token&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A JWT-SVID looks like any other JWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJzcGlmZmU6Ly9....MEUCIQDx3k...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer the certificate: its private key never leaves the agent. Stolen JWTs replay until expiry.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPIRE and secret zero: the passport office
&lt;/h2&gt;

&lt;p&gt;But how does the agent get a passport without a password?&lt;/p&gt;

&lt;p&gt;You'd need a secret to get a secret: &lt;strong&gt;secret zero&lt;/strong&gt;. SPIRE solves it in two steps.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;SPIRE Server&lt;/strong&gt; is the passport office. The &lt;strong&gt;SPIRE Agent&lt;/strong&gt; is a desk on every node. Careful: that's not your AI agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step one:&lt;/strong&gt; the node proves itself with platform evidence, like a cloud instance document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workload attestation: the fingerprint check
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step two:&lt;/strong&gt; your AI agent asks its local desk for a passport. No credentials.&lt;/p&gt;

&lt;p&gt;The desk asks the kernel who's calling, then looks up that process: its container, its service account. Those are its &lt;strong&gt;selectors&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k8s:ns:agents
k8s:container-image:…/billing-agent
k8s:sa:billing-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those selectors match an entry you registered, like a fingerprint on file, and the SVID streams back. The entry is something you create up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spire-server entry create &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-parentID&lt;/span&gt; spiffe://example.org/spire/agent/k8s_psat/demo-cluster/node-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-spiffeID&lt;/span&gt; spiffe://example.org/agents/billing &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-selector&lt;/span&gt; k8s:ns:agents &lt;span class="nt"&gt;-selector&lt;/span&gt; k8s:sa:billing-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike a real passport, it's short-lived and auto-rotates before it expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  mTLS: passport control in both directions
&lt;/h2&gt;

&lt;p&gt;Now the agent calls a tool, a payments API, over mutual TLS (mTLS).&lt;/p&gt;

&lt;p&gt;Each side checks the other's stamp against its &lt;strong&gt;trust bundle&lt;/strong&gt;: passport control, in both directions.&lt;/p&gt;

&lt;p&gt;No API key anywhere, at least for internal calls. So you're safe, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: identity isn't permission
&lt;/h2&gt;

&lt;p&gt;A prompt injection tells the billing agent to clean up. It sends this:&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;DELETE&lt;/span&gt; &lt;span class="nn"&gt;/customers&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;

HTTP/1.1 200 OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the tool happily replies: 200 OK.&lt;/p&gt;

&lt;p&gt;SPIFFE proved who called. Deciding what it may do was never SPIFFE's job.&lt;/p&gt;

&lt;h2&gt;
  
  
  OPA: the gate
&lt;/h2&gt;

&lt;p&gt;That's the gate's job. A popular one: &lt;strong&gt;OPA&lt;/strong&gt;, the Open Policy Agent.&lt;/p&gt;

&lt;p&gt;OPA checks the request against a &lt;strong&gt;Rego&lt;/strong&gt; policy. Billing may read invoices. DELETE? Denied.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;default&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"spiffe://example.org/agents/billing"&lt;/span&gt;
  &lt;span class="n"&gt;input&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="s2"&gt;"GET"&lt;/span&gt;
  &lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/invoices"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default allow := false&lt;/code&gt; is the important line: anything not explicitly allowed is denied. OPA returns the decision. Envoy enforces it, usually with a &lt;code&gt;403 Forbidden&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: agents act for humans
&lt;/h2&gt;

&lt;p&gt;Agents usually act for a human, so the gate also checks a signed permission slip.&lt;/p&gt;

&lt;p&gt;The IETF's September 2026 AI-agent draft splits it the same way: SPIFFE-style IDs for agents, OAuth token exchange for users. Policy? Out of scope: that's a job for OPA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Airport&lt;/th&gt;
&lt;th&gt;Real thing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the name in the passport&lt;/td&gt;
&lt;td&gt;SPIFFE ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the passport&lt;/td&gt;
&lt;td&gt;SVID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the passport office&lt;/td&gt;
&lt;td&gt;SPIRE Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a desk on every node&lt;/td&gt;
&lt;td&gt;SPIRE Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the fingerprint check&lt;/td&gt;
&lt;td&gt;attestation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;passport control, both ways&lt;/td&gt;
&lt;td&gt;mTLS + trust bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the gate, on every single call&lt;/td&gt;
&lt;td&gt;OPA + Rego&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your agent never needed a forever key. It needed a passport, and a gate that denies by default.&lt;/p&gt;

&lt;p&gt;What does your agent use: API key, OAuth, or SPIFFE? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;More in this series: &lt;a href="https://youtu.be/pvh4lbc0Ssc" rel="noopener noreferrer"&gt;AI agent sandboxing&lt;/a&gt;, &lt;a href="https://youtu.be/rIzbixCoSEs" rel="noopener noreferrer"&gt;Pi 1.0 and Pi Durable&lt;/a&gt;, &lt;a href="https://youtu.be/IF_1lFy7YM8" rel="noopener noreferrer"&gt;AI reverse engineering&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 4-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md" rel="noopener noreferrer"&gt;SPIFFE ID spec&lt;/a&gt;, &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md" rel="noopener noreferrer"&gt;X509-SVID spec&lt;/a&gt;, &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md" rel="noopener noreferrer"&gt;JWT-SVID spec&lt;/a&gt;, &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md" rel="noopener noreferrer"&gt;SPIFFE Workload API&lt;/a&gt;, &lt;a href="https://spiffe.io/docs/latest/spire-about/spire-concepts/" rel="noopener noreferrer"&gt;SPIRE concepts&lt;/a&gt;, &lt;a href="https://spiffe.io/docs/latest/deploying/spire_server/" rel="noopener noreferrer"&gt;SPIRE Server&lt;/a&gt;, &lt;a href="https://www.cncf.io/announcements/2022/09/20/spiffe-and-spire-projects-graduate-from-cloud-native-computing-foundation-incubator/" rel="noopener noreferrer"&gt;CNCF: SPIFFE and SPIRE graduate&lt;/a&gt;, &lt;a href="https://istio.io/latest/docs/concepts/security/" rel="noopener noreferrer"&gt;Istio security&lt;/a&gt;, &lt;a href="https://www.openpolicyagent.org/docs/latest/" rel="noopener noreferrer"&gt;OPA docs&lt;/a&gt;, &lt;a href="https://www.openpolicyagent.org/docs/envoy" rel="noopener noreferrer"&gt;OPA-Envoy&lt;/a&gt;, &lt;a href="https://www.openpolicyagent.org/docs/latest/policy-language/" rel="noopener noreferrer"&gt;Rego&lt;/a&gt;, &lt;a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/attribute_context.proto" rel="noopener noreferrer"&gt;Envoy attribute context&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-wimse-aims/" rel="noopener noreferrer"&gt;IETF WIMSE AIMS draft&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/html/draft-klrc-aiagent-auth-00" rel="noopener noreferrer"&gt;IETF AI agent auth draft&lt;/a&gt;, &lt;a href="https://next.redhat.com/2026/06/10/wiring-zero-trust-identity-for-ai-agents-spiffe-token-exchange-and-kagenti/" rel="noopener noreferrer"&gt;Red Hat: zero-trust identity for AI agents&lt;/a&gt;, &lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;OWASP LLM01 Prompt Injection&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
    </item>
    <item>
      <title>Is a Docker Container a Sandbox? AI Agent Sandboxing Explained: seccomp, gVisor, Firecracker, Kata and Wasm</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:08:26 +0000</pubDate>
      <link>https://dev.to/runtimestack/is-a-docker-container-a-sandbox-ai-agent-sandboxing-explained-seccomp-gvisor-firecracker-kata-5f4b</link>
      <guid>https://dev.to/runtimestack/is-a-docker-container-a-sandbox-ai-agent-sandboxing-explained-seccomp-gvisor-firecracker-kata-5f4b</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pvh4lbc0Ssc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;When headlines say an AI agent "escaped its sandbox"... what wall did it pass?&lt;/p&gt;

&lt;p&gt;Some sandboxes are walls. Some are just curtains.&lt;/p&gt;

&lt;p&gt;Containers, seccomp, gVisor, Firecracker, Kata, WebAssembly, V8 isolates. They all sort into one ladder, with one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much of the host kernel can the code reach?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And there's one door none of them close. We'll get there.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo3up4jg8cz03y3f02xu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo3up4jg8cz03y3f02xu.png" alt="An apartment building: the agent container behind a dashed curtain, the host and another app behind a wall, all on one shared kernel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sandbox at all?
&lt;/h2&gt;

&lt;p&gt;Your agent writes code, then runs it. But who really wrote it?&lt;/p&gt;

&lt;p&gt;Treat agent code like code from a stranger. Because it might be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The building and its plumbing: the kernel
&lt;/h2&gt;

&lt;p&gt;Here's the building. Every program lives in its own apartment.&lt;/p&gt;

&lt;p&gt;Down in the basement is the &lt;strong&gt;kernel&lt;/strong&gt;: the plumbing every apartment shares.&lt;/p&gt;

&lt;p&gt;Open a file, send a packet? You ask the kernel. That request is a &lt;strong&gt;syscall&lt;/strong&gt;. Linux has hundreds of them, and every one is a pipe into the basement.&lt;/p&gt;

&lt;p&gt;One bug in that plumbing, and a tenant can flood the whole building.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwaqnvqnd1bj64vzaqxzu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwaqnvqnd1bj64vzaqxzu.png" alt="Hundreds of syscalls running from every apartment into one shared kernel; one bug floods the whole building" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Containers: a locked room
&lt;/h2&gt;

&lt;p&gt;So what's a container? A locked room in this same building.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Namespaces&lt;/strong&gt; control what you can &lt;em&gt;see&lt;/em&gt;: your own processes, files and network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroups&lt;/strong&gt; control how much you can &lt;em&gt;use&lt;/em&gt;: CPU, memory, how many processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a Mac or Windows, Docker runs a Linux VM, so the shared kernel is that VM's.&lt;/p&gt;

&lt;h2&gt;
  
  
  seccomp: the list of taps
&lt;/h2&gt;

&lt;p&gt;Next layer: &lt;strong&gt;seccomp&lt;/strong&gt;, a list of taps you're allowed to turn.&lt;/p&gt;

&lt;p&gt;It's a tiny filter program that checks each syscall's number and arguments, and answers allow, errno or kill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read       allow
write      allow
mount      blocked
ptrace     blocked
kexec_load blocked
openat     allow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the kernel's own docs say it plainly: syscall filtering isn't a sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is a Docker container a sandbox?
&lt;/h2&gt;

&lt;p&gt;Not on its own.&lt;/p&gt;

&lt;p&gt;In January 2024, a bug in &lt;strong&gt;runc&lt;/strong&gt;, the tool that starts containers, let them reach the host's files. That's CVE-2024-21626. A full container escape.&lt;/p&gt;

&lt;p&gt;And that wasn't even the kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  The classic own-goal: docker.sock
&lt;/h3&gt;

&lt;p&gt;Mounting the Docker socket into the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whoever controls that socket controls the Docker daemon, and the daemon runs as root.&lt;/p&gt;

&lt;p&gt;Volumes only, a separate network? Better. But it's still the same kernel.&lt;/p&gt;

&lt;p&gt;So the people saying "if it escaped, it wasn't a sandbox"? They're right. If it shares a kernel, it's a curtain, not a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  gVisor: hire a concierge
&lt;/h2&gt;

&lt;p&gt;So how do you keep code away from the pipes? Hire a concierge.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;gVisor&lt;/strong&gt;: its &lt;strong&gt;Sentry&lt;/strong&gt; is a kernel, written in Go, running in user space. Your code makes a syscall, and the Sentry answers it itself.&lt;/p&gt;

&lt;p&gt;It only makes a small, filtered set of host calls. Files go through a helper, the &lt;strong&gt;Gofer&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9tndhgt5653194tooe1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9tndhgt5653194tooe1.png" alt="gVisor: your code's open() goes to the Sentry in user space instead of the host kernel; files go through the Gofer" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What does the concierge cost? Every syscall takes a detour, so syscall-heavy work slows down. And some programs expect plumbing the Sentry doesn't provide, so compatibility drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  MicroVMs: your own tiny house
&lt;/h2&gt;

&lt;p&gt;Still nervous about sharing a building? Then move out. Get your own tiny house.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;microVM&lt;/strong&gt; runs your code on its own guest kernel, with its own plumbing. &lt;strong&gt;Firecracker&lt;/strong&gt; is the VMM, the virtual machine monitor, built on &lt;strong&gt;KVM&lt;/strong&gt;, the hypervisor inside Linux.&lt;/p&gt;

&lt;p&gt;The house still has one metered service line into the building's basement: KVM. To escape, code has to get past its own kernel, and then break the VMM or KVM too.&lt;/p&gt;

&lt;p&gt;Share a folder in, though, and you've cut a door in that wall.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxskjwj6e3m0kneifa8f.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxskjwj6e3m0kneifa8f.png" alt="Firecracker microVM: your code on its own guest kernel, with only the VMM and KVM between it and the host kernel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't a VM slow?
&lt;/h3&gt;

&lt;p&gt;Firecracker's spec says the guest reaches init within 125 ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.000] InstanceStart
[0.003] guest kernel boots...
[0.125] /sbin/init
/dev/kvm: required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the VMM itself adds at most about 5 MB of memory. Low-powered machine? The overhead is tiny, if it has KVM. The guest's own memory is the real bill.&lt;/p&gt;

&lt;p&gt;Many sandboxes at once? Fine. They only talk to each other if you wire up a network.&lt;/p&gt;

&lt;p&gt;That's why platforms like E2B describe a sandbox as a Linux VM made on demand. Even Docker's own agent product, Docker Sandboxes, runs each agent in its own microVM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kata Containers: a container's front door
&lt;/h2&gt;

&lt;p&gt;But your tools speak containers, not VMs. So &lt;strong&gt;Kata&lt;/strong&gt; puts a lightweight VM behind the container interface.&lt;/p&gt;

&lt;p&gt;Same front door as an apartment, but a tiny house behind it. Your container tools never know they're running a VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly and V8 isolates: the glovebox
&lt;/h2&gt;

&lt;p&gt;Now flip it. Picture a sealed glovebox in the lobby. You work through the gloves you're handed, and nothing else.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;WebAssembly&lt;/strong&gt; module's memory is bounds-checked. Step outside it, and the module traps: no pipes to the basement at all.&lt;/p&gt;

&lt;p&gt;Same idea for JavaScript. &lt;strong&gt;V8&lt;/strong&gt; is Chrome's JavaScript engine, and an &lt;strong&gt;isolate&lt;/strong&gt; is one sealed instance of it. It gets its own memory and only the APIs it's handed. Cloudflare Workers run thousands per process. Cloudflare says they start about a hundred times faster than a containerized Node process, using a tenth of the memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  The glovebox catch
&lt;/h3&gt;

&lt;p&gt;So the glovebox is the strongest rung, right? Not so fast.&lt;/p&gt;

&lt;p&gt;Now the wall is the runtime itself, and runtimes have bugs. Break one, and you're a normal host process. In 2024, V8's team said even its newest sandbox wasn't yet a strong boundary.&lt;/p&gt;

&lt;p&gt;Side channels, timing the shared CPU, still work too. Even between tiny houses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ladder
&lt;/h2&gt;

&lt;p&gt;Sorted by how much host kernel the code can reach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;What the code reaches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container + seccomp&lt;/td&gt;
&lt;td&gt;locked room, list of taps&lt;/td&gt;
&lt;td&gt;the shared kernel, filtered by seccomp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gVisor&lt;/td&gt;
&lt;td&gt;the concierge&lt;/td&gt;
&lt;td&gt;the Sentry, which makes a small, filtered set of host calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecracker microVM / Kata&lt;/td&gt;
&lt;td&gt;your own tiny house&lt;/td&gt;
&lt;td&gt;only its VMM and the KVM under it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm / V8 isolates&lt;/td&gt;
&lt;td&gt;the glovebox&lt;/td&gt;
&lt;td&gt;nothing but their imports&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u6wshn8ibs16uqzbrt8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u6wshn8ibs16uqzbrt8.png" alt="The sandbox ladder: rungs from container to V8 isolate, with what each reaches of the host kernel and what it costs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And every wall here is software, so any rung can have bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which rung should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your own agent on your own repo? A container-level sandbox is the floor. An injected command's easy win is your network, not a kernel exploit, and the next rule shuts that.&lt;/li&gt;
&lt;li&gt;Running strangers' code, or anything multi-tenant? Give it its own kernel: gVisor or a microVM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The door no rung closes: the network
&lt;/h2&gt;

&lt;p&gt;So what if your agent sat in a perfect microVM, holding real API keys, with open internet?&lt;/p&gt;

&lt;p&gt;Without network isolation, one injected instruction and one outbound request, and your keys are gone.&lt;/p&gt;

&lt;p&gt;Every wall held. The code just walked out the front door.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egress: the mailroom
&lt;/h3&gt;

&lt;p&gt;Allowlist destinations: a mailroom that only delivers to addresses on the list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALLOWLIST
  pypi.org
  github.com
everything else: DENIED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can it still leak? If it reads your data and can reach an allowed host, yes. So keep that list as short as the job allows, and lock down DNS too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets: keep the master keys out of the house
&lt;/h3&gt;

&lt;p&gt;The sandbox gets a placeholder, and the mailroom attaches the real credential on the way out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inside the sandbox:   Authorization: Bearer PLACEHOLDER
after the mailroom:   Authorization: Bearer sk-live-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Injected code can read that placeholder all day. Off the box, it's worthless. It can still &lt;em&gt;use&lt;/em&gt; the key through the mailroom, so keep the allowlist short.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00llnq3hxwhjvw4g39io.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00llnq3hxwhjvw4g39io.png" alt="Keys stay out: the agent only holds a placeholder; the mailroom injects the real key at egress" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Agent code is untrusted on your laptop too: review it, or run it sandboxed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: Claude Code's sandbox
&lt;/h2&gt;

&lt;p&gt;Does anyone build it this way? Here's one example: Claude Code's sandbox.&lt;/p&gt;

&lt;p&gt;It's off by default. You turn it on with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux it uses &lt;strong&gt;bubblewrap&lt;/strong&gt;, the same namespaces trick a container uses. On macOS, &lt;strong&gt;Seatbelt&lt;/strong&gt;, Apple's built-in sandbox.&lt;/p&gt;

&lt;p&gt;Commands get no direct route out. A host proxy checks every domain against an allowlist that starts empty. Set up masking, and the proxy swaps a placeholder key for the real one.&lt;/p&gt;

&lt;p&gt;Its own docs say it reduces risk, but isn't a complete isolation boundary. For strangers' code, climb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;One building. The kernel is the shared plumbing. A container is a locked room. seccomp is the list of taps. gVisor is the concierge. A microVM is your own tiny house, and Kata gives it a container's front door. Wasm and isolates are the glovebox. And the mailroom guards the one door no wall closes.&lt;/p&gt;

&lt;p&gt;So did that agent escape its sandbox? Ask two things: was it a wall, or a curtain? And which door was open?&lt;/p&gt;

&lt;p&gt;Which rung is your agent on right now? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;More in this series: &lt;a href="https://youtu.be/Q3wdpWt09UM" rel="noopener noreferrer"&gt;AI agent identity with SPIFFE and SPIRE&lt;/a&gt;, &lt;a href="https://youtu.be/rIzbixCoSEs" rel="noopener noreferrer"&gt;Pi 1.0 and Pi Durable&lt;/a&gt;, &lt;a href="https://youtu.be/IF_1lFy7YM8" rel="noopener noreferrer"&gt;AI reverse engineering&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 10-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://man7.org/linux/man-pages/man2/syscalls.2.html" rel="noopener noreferrer"&gt;syscalls(2)&lt;/a&gt;, &lt;a href="https://man7.org/linux/man-pages/man7/namespaces.7.html" rel="noopener noreferrer"&gt;namespaces(7)&lt;/a&gt;, &lt;a href="https://man7.org/linux/man-pages/man7/cgroups.7.html" rel="noopener noreferrer"&gt;cgroups(7)&lt;/a&gt;, &lt;a href="https://docs.kernel.org/userspace-api/seccomp_filter.html" rel="noopener noreferrer"&gt;seccomp BPF&lt;/a&gt;, &lt;a href="https://access.redhat.com/security/vulnerabilities/RHSB-2024-001" rel="noopener noreferrer"&gt;Red Hat RHSB-2024-001&lt;/a&gt;, &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-21626" rel="noopener noreferrer"&gt;CVE-2024-21626&lt;/a&gt;, &lt;a href="https://dirtypipe.cm4all.com/" rel="noopener noreferrer"&gt;Dirty Pipe&lt;/a&gt;, &lt;a href="https://docs.docker.com/engine/security/" rel="noopener noreferrer"&gt;Docker Engine security&lt;/a&gt;, &lt;a href="https://docs.docker.com/ai/sandboxes/architecture/" rel="noopener noreferrer"&gt;Docker Sandboxes architecture&lt;/a&gt;, &lt;a href="https://docs.docker.com/desktop/features/vmm/" rel="noopener noreferrer"&gt;Docker Desktop VMM&lt;/a&gt;, &lt;a href="https://gvisor.dev/docs/" rel="noopener noreferrer"&gt;gVisor docs&lt;/a&gt;, &lt;a href="https://gvisor.dev/docs/architecture_guide/platforms/" rel="noopener noreferrer"&gt;gVisor platforms&lt;/a&gt;, &lt;a href="https://github.com/firecracker-microvm/firecracker/blob/main/README.md" rel="noopener noreferrer"&gt;Firecracker README&lt;/a&gt;, &lt;a href="https://github.com/firecracker-microvm/firecracker/blob/main/SPECIFICATION.md" rel="noopener noreferrer"&gt;Firecracker specification&lt;/a&gt;, &lt;a href="https://github.com/firecracker-microvm/firecracker/blob/main/docs/getting-started.md" rel="noopener noreferrer"&gt;Firecracker getting started&lt;/a&gt;, &lt;a href="https://www.usenix.org/conference/nsdi20/presentation/agache" rel="noopener noreferrer"&gt;Firecracker (NSDI '20)&lt;/a&gt;, &lt;a href="https://github.com/kata-containers/kata-containers/blob/main/README.md" rel="noopener noreferrer"&gt;Kata Containers&lt;/a&gt;, &lt;a href="https://github.com/kata-containers/kata-containers/blob/main/docs/hypervisors.md" rel="noopener noreferrer"&gt;Kata hypervisors&lt;/a&gt;, &lt;a href="https://webassembly.org/docs/security/" rel="noopener noreferrer"&gt;WebAssembly security&lt;/a&gt;, &lt;a href="https://developers.cloudflare.com/workers/reference/how-workers-works/" rel="noopener noreferrer"&gt;How Workers works&lt;/a&gt;, &lt;a href="https://developers.cloudflare.com/workers/reference/security-model/" rel="noopener noreferrer"&gt;Workers security model&lt;/a&gt;, &lt;a href="https://v8.dev/blog/sandbox" rel="noopener noreferrer"&gt;The V8 Sandbox&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/sandboxing" rel="noopener noreferrer"&gt;Claude Code sandboxing&lt;/a&gt;, &lt;a href="https://docs.e2b.dev/" rel="noopener noreferrer"&gt;E2B docs&lt;/a&gt;, &lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;OWASP LLM01&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>You've Never Actually Sent a JWT (JOSE, JWS, JWE, JWA and JWK Explained)</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:51:39 +0000</pubDate>
      <link>https://dev.to/runtimestack/youve-never-actually-sent-a-jwt-jose-jws-jwe-jwa-and-jwk-explained-enn</link>
      <guid>https://dev.to/runtimestack/youve-never-actually-sent-a-jwt-jose-jws-jwe-jwa-and-jwk-explained-enn</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Kl3LZRcJsbM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You've used JWTs a hundred times. But here's the thing: you've probably never actually sent one.&lt;/p&gt;

&lt;p&gt;That token in your &lt;code&gt;Authorization&lt;/code&gt; header? Technically, it's a &lt;strong&gt;JWS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;JOSE, JWS, JWE, JWA, JWK, JWT. It's alphabet soup. Here's one analogy that makes all of it click.&lt;/p&gt;

&lt;h2&gt;
  
  
  JOSE: the shipping company
&lt;/h2&gt;

&lt;p&gt;Start with &lt;strong&gt;JOSE&lt;/strong&gt;: JSON Object Signing and Encryption. It's not a token. It's a family of IETF standards.&lt;/p&gt;

&lt;p&gt;Think of it as a shipping company, with rules for how packages get &lt;strong&gt;sealed&lt;/strong&gt;, &lt;strong&gt;locked&lt;/strong&gt; and &lt;strong&gt;delivered&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWT: the letter
&lt;/h2&gt;

&lt;p&gt;A JWT isn't a container at all. It's the letter inside: a set of claims.&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;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth.example.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;"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;"user_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"orders-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1767225600&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;Who issued it. Who it's about. Who it's for. And when it expires.&lt;/p&gt;

&lt;p&gt;But a letter on its own isn't secure. It needs packaging.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWS: the sealed envelope
&lt;/h2&gt;

&lt;p&gt;Option one: &lt;strong&gt;JWS&lt;/strong&gt;, JSON Web Signature. Picture a clear envelope with a tamper-proof wax seal.&lt;/p&gt;

&lt;p&gt;It has three parts, separated by dots: a header that names the algorithm, the payload, and the signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJ...  .  eyJzdWIiOiI...  .  SflKxwRJSMe...
    header            payload           signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seal proves who sent it, and that nobody changed a single byte.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hq0duqhiesh8xx130z5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hq0duqhiesh8xx130z5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The gotcha: the envelope is clear
&lt;/h3&gt;

&lt;p&gt;The payload is just Base64URL encoded. Anyone can decode it:&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;echo&lt;/span&gt; &lt;span class="s1"&gt;'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ=='&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;   &lt;span class="c"&gt;# Base64URL drops the = padding; add it back for base64&lt;/span&gt;
&lt;span class="c"&gt;# {"sub":"1234567890","name":"John Doe","iat":1516239022}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;strong&gt;never put secrets inside a signed JWT&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngmam1jwa4zb0ppqou35.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngmam1jwa4zb0ppqou35.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  JWE: the locked box
&lt;/h2&gt;

&lt;p&gt;Need privacy? Option two: &lt;strong&gt;JWE&lt;/strong&gt;, JSON Web Encryption. This is a locked metal box, with five parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A header&lt;/li&gt;
&lt;li&gt;An encrypted key&lt;/li&gt;
&lt;li&gt;An initialization vector&lt;/li&gt;
&lt;li&gt;The ciphertext&lt;/li&gt;
&lt;li&gt;An authentication tag&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the recipient, holding the right key, can open 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgxrn2dh7mn1nvoj32z9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgxrn2dh7mn1nvoj32z9.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  JWA: the toolbox
&lt;/h2&gt;

&lt;p&gt;So how do you seal or lock it? That's &lt;strong&gt;JWA&lt;/strong&gt;, JSON Web Algorithms. The toolbox.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signing:&lt;/strong&gt; HS256, RS256 and ES256&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption:&lt;/strong&gt; RSA-OAEP and A256GCM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;alg&lt;/code&gt; field in the header tells the receiver which tool was used.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWK and JWKS: the keys
&lt;/h2&gt;

&lt;p&gt;And the keys themselves? That's &lt;strong&gt;JWK&lt;/strong&gt;, JSON Web Key. Instead of PEM files, a key is just JSON: a key type (&lt;code&gt;kty&lt;/code&gt;), what it's used for (&lt;code&gt;use&lt;/code&gt;), a key ID called &lt;code&gt;kid&lt;/code&gt;, and the key material.&lt;/p&gt;

&lt;p&gt;Put several keys in an array, and you get a &lt;strong&gt;JWKS&lt;/strong&gt;. Auth servers publish it at &lt;code&gt;/.well-known/jwks.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your API reads the &lt;code&gt;kid&lt;/code&gt; from the token header, fetches the matching public key, and verifies the signature. That's how keys rotate without redeploying anything.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4q8o6y0dh99l0o44965k.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4q8o6y0dh99l0o44965k.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested JWT: both at once
&lt;/h2&gt;

&lt;p&gt;And yes, you can combine them. Sign a JWT, then encrypt the result. That's a nested JWT: a sealed envelope, inside a locked box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&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;Analogy&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JOSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the shipping company&lt;/td&gt;
&lt;td&gt;the family of standards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the letter&lt;/td&gt;
&lt;td&gt;the claims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the sealed envelope&lt;/td&gt;
&lt;td&gt;signed, readable by anyone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the locked box&lt;/td&gt;
&lt;td&gt;encrypted, private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the toolbox&lt;/td&gt;
&lt;td&gt;the algorithms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the keys&lt;/td&gt;
&lt;td&gt;keys as JSON&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdzz23wb6wt0ohziqzbe.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdzz23wb6wt0ohziqzbe.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next time someone says JWT, you can say: well, actually... it's a JWS.&lt;/p&gt;

&lt;p&gt;Which of these acronyms confused you the most? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 3-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://www.rfc-editor.org/rfc/rfc7519" rel="noopener noreferrer"&gt;RFC 7519 (JWT)&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc7515" rel="noopener noreferrer"&gt;RFC 7515 (JWS)&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc7516" rel="noopener noreferrer"&gt;RFC 7516 (JWE)&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>jwt</category>
      <category>security</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
