<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Dam Secure Docs Blog</title>
        <link>https://docs.damsecure.ai/blog</link>
        <description>Dam Secure Docs Blog</description>
        <lastBuildDate>Thu, 14 May 2026 04:09:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[How We Built Tenant Isolation for hackaws.cloud. And How Dam Secure Finds What We Miss]]></title>
            <link>https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud</link>
            <guid>https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud</guid>
            <pubDate>Thu, 14 May 2026 04:09:52 GMT</pubDate>
            <description><![CDATA[hackaws.cloud is an autonomous AWS penetration testing platform. Customers connect their AWS accounts, configure a foothold identity, and our agent performs real lateral movement and privilege escalation, building a live attack graph as it works.]]></description>
            <content:encoded><![CDATA[<p>hackaws.cloud is an autonomous AWS penetration testing platform. Customers connect their AWS accounts, configure a foothold identity, and our agent performs real lateral movement and privilege escalation, building a live attack graph as it works.</p>
<p>That means we store AWS account IDs, IAM role ARNs, assumed-role credentials, and full attack path graphs. If tenant isolation fails, one customer could see another customer's AWS infrastructure map, or perform <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" target="_blank" rel="noopener noreferrer" class="">confused deputy</a> attacks to assume roles in other customers' accounts. In an extreme scenario, chained with another bug, an attacker could even access stored credentials. The stakes are about as high as they get for a multi-tenant SaaS.</p>
<p>Tenant isolation isn't new territory for us. At <a href="https://www.plerion.com/" target="_blank" rel="noopener noreferrer" class="">Plerion</a>, where I'm Chief Innovation Officer, we face the same class of problem: our cloud security platform ingests customer cloud configurations, vulnerability data, and identity graphs across thousands of accounts. The compliance requirements alone (SOC 2, ISO 27001) demand rigorous tenant boundaries. hackaws.cloud gave us a greenfield opportunity to apply those lessons from day one rather than retrofitting them.</p>
<!-- -->
<p>This post covers our tenant isolation architecture, the technical decisions behind it, and how we use Dam Secure's team rules to continuously systematically verify the security of our implementation. We deployed 6 rules in Dam Secure, like "Never expose tenant ID to the frontend", and because Dam Secure doesn't suffer from context rot, the rules were applied every time. The rules also got automatically applied directly to an agentic planning session to remind the LLM to use these rules before a line of code was generated.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="the-architecture">The Architecture<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#the-architecture" class="hash-link" aria-label="Direct link to The Architecture" title="Direct link to The Architecture" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="tenant-id-server-generated-never-client-supplied">Tenant ID: Server-Generated, Never Client-Supplied<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#tenant-id-server-generated-never-client-supplied" class="hash-link" aria-label="Direct link to Tenant ID: Server-Generated, Never Client-Supplied" title="Direct link to Tenant ID: Server-Generated, Never Client-Supplied" translate="no">​</a></h3>
<p>Every tenant gets a UUID v4 identifier, generated server-side on sign-up:</p>
<div class="language-typescript codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-typescript codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// user-repository.ts</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> newUser</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> UserRecord </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">PK</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">USER#</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">cognitoSub</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> cognitoSub</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  tenantId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateTenantId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// crypto.randomUUID()</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  createdAt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Date</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toISOString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The tenant ID is stored in a dynamo db table, keyed by the Cognito <code>sub</code> claim. It never appears in JWTs, never comes from the client, and never leaves our backend.</p>
<p>This was a deliberate decision. Many multi-tenant apps embed a <code>tenantId</code> in the JWT or accept it as a header. That creates a trust boundary problem: you're relying on the client to tell you who it is. We made the boundary explicit: the tenant ID exists only in the database and is resolved on every request.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="auth-middleware-the-single-entry-point">Auth Middleware: The Single Entry Point<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#auth-middleware-the-single-entry-point" class="hash-link" aria-label="Direct link to Auth Middleware: The Single Entry Point" title="Direct link to Auth Middleware: The Single Entry Point" translate="no">​</a></h3>
<p>Every API handler is wrapped with <code>withAuth()</code>, a higher-order function that:</p>
<ol>
<li class="">Extracts the Bearer token from the Authorization header</li>
<li class="">Verifies the JWT against Cognito's JWKS</li>
<li class="">Looks up the user record by Cognito <code>sub</code></li>
<li class="">Builds an <code>AuthContext</code> with the tenant ID from the database</li>
<li class="">Passes it to the handler</li>
</ol>
<div class="language-typescript codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-typescript codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// auth-middleware.ts</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> authContext</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> AuthContext </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> claims</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sub</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  tenantId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tenantId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// From DB, never from claims</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> authContext</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The <code>AuthContext</code> interface is intentionally <code>readonly</code>:</p>
<div class="language-typescript codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-typescript codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">AuthContext</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> tenantId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Handlers never construct their own context. They receive it from the middleware or they don't get one at all.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="dynamodb-tenant-id-as-partition-key">DynamoDB: Tenant ID as Partition Key<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#dynamodb-tenant-id-as-partition-key" class="hash-link" aria-label="Direct link to DynamoDB: Tenant ID as Partition Key" title="Direct link to DynamoDB: Tenant ID as Partition Key" translate="no">​</a></h3>
<p>For tables that are purely tenant-scoped, the DynamoDB partition key <em>is</em> the tenant ID:</p>
<div class="language-typescript codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-typescript codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// data-stack.ts (CDK)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">accountsTable </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">dynamodb</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"AccountsTable"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  partitionKey</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"tenantId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> dynamodb</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AttributeType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">STRING</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  sortKey</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"accountId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> dynamodb</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AttributeType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">STRING</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">assessmentsTable </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">dynamodb</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"AssessmentsTable"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  partitionKey</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"tenantId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> dynamodb</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AttributeType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">STRING</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  sortKey</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"assessmentId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> dynamodb</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AttributeType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">STRING</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>This means a <code>Query</code> on the accounts table physically cannot return another tenant's data. DynamoDB's partition key is the first filter, before any application logic runs.</p>
<p>For tables with composite keys (identities, graph nodes, events), the tenant ID is embedded in the partition key string: <code>{tenantId}#{assessmentId}</code> or <code>{tenantId}#{accountId}</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="tenantdb-the-guard-at-the-data-layer">TenantDb: The Guard at the Data Layer<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#tenantdb-the-guard-at-the-data-layer" class="hash-link" aria-label="Direct link to TenantDb: The Guard at the Data Layer" title="Direct link to TenantDb: The Guard at the Data Layer" translate="no">​</a></h3>
<p>Even with partition keys, we wanted defense in depth. <code>TenantDb</code> is a wrapper around the DynamoDB document client that automatically enforces tenant scoping on every operation:</p>
<ul>
<li class=""><strong>Put</strong>: Injects <code>tenantId</code> into the item automatically</li>
<li class=""><strong>Get</strong>: Returns <code>null</code> if the item's <code>tenantId</code> doesn't match the caller's</li>
<li class=""><strong>Query</strong>: Appends a <code>tenantId = :__tenantId__</code> filter expression</li>
<li class=""><strong>Update/Delete</strong>: Adds a <code>tenantId = :__tenantId__</code> condition expression (the operation fails if ownership doesn't match)</li>
</ul>
<div class="language-typescript codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-typescript codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// tenant-db.ts</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tenantId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> params</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> GetCommandInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">assertValidTenantId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tenantId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">docClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">GetCommand</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Item</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Item</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tenantId </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> tenantId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Silent rejection</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Item</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tenantId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> params</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> DeleteCommandInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">assertValidTenantId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tenantId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// Appends: ConditionExpression = "tenantId = :__tenantId__"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// DynamoDB rejects the delete if the tenant doesn't own the item</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Every method validates the tenant ID format (UUID v4 regex) before touching DynamoDB. An invalid or missing tenant ID throws a <code>ForbiddenError</code> immediately.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="response-sanitization">Response Sanitization<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#response-sanitization" class="hash-link" aria-label="Direct link to Response Sanitization" title="Direct link to Response Sanitization" translate="no">​</a></h3>
<p>Tenant IDs are stripped from every API response before it reaches the frontend:</p>
<div class="language-typescript codeBlockContainer_ZGJx theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_kX1v"><pre tabindex="0" class="prism-code language-typescript codeBlock_TAPP thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_AdAo"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// accounts-list.ts</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> accounts </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getAccountRepo</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">listAccounts</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authContext</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tenantId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sanitized </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> accounts</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> tenantId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> _</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">...</span><span class="token plain">rest </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> rest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">jsonResponse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">200</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> accounts</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> sanitized </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> origin</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The <code>whoami</code> endpoint returns <code>userId</code> and <code>email</code>, but not <code>tenantId</code>. The frontend identifies users by their Cognito <code>sub</code>, never by tenant ID.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="error-handling">Error Handling<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#error-handling" class="hash-link" aria-label="Direct link to Error Handling" title="Direct link to Error Handling" translate="no">​</a></h3>
<p>Backend errors return generic messages. If an assessment doesn't belong to your tenant, you get <code>404 Assessment not found</code>, not <code>403 You don't own this assessment</code>. AWS SDK errors are caught and replaced with <code>Internal server error</code> rather than forwarded to the client, where they could leak table names, ARNs, or account structure.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="how-dam-secure-helped">How Dam Secure Helped<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#how-dam-secure-helped" class="hash-link" aria-label="Direct link to How Dam Secure Helped" title="Direct link to How Dam Secure Helped" translate="no">​</a></h2>
<p>Building all of this is one thing. <em>Verifying</em> it stays correct as the codebase grows is another.</p>
<p>We built hackaws.cloud almost entirely with AI coding agents. Claude Code, Cursor, etc. These tools are incredibly productive, but they share a fundamental limitation. They lose context. A long session drifts. A new session starts fresh. You can write perfect rules in CLAUDE.md or .cursorrules telling the agent exactly how tenant isolation works, but those instructions compete with everything else in the context window. By the time the agent is deep in a feature branch, wrestling with some unrelated DynamoDB pagination bug, those tenant isolation rules are a distant memory.</p>
<p>This isn't a tooling bug. It's an inherent property of how LLM-based agents work. Context windows are finite, attention degrades over distance, and the agent optimizes for the immediate task, not the architectural invariant you defined three thousand tokens ago. The result is subtle drift: a handler that constructs its own DynamoDB query instead of going through <code>TenantDb</code>, a new endpoint that reads <code>tenantId</code> from the request body because that's what was in the test fixture. Each violation looks reasonable in isolation. None of them trigger an error. They just quietly erode your security model.</p>
<p>This is why you need something external to the coding agent. Something that doesn't lose context, doesn't get distracted, and checks every line against the rules you actually care about. That's what Dam Secure gives us: a persistent, stateless verification layer that re-evaluates the entire codebase against our tenant isolation rules on every scan, regardless of how the code was written or which agent wrote it.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="the-rules-we-wrote">The Rules We Wrote<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#the-rules-we-wrote" class="hash-link" aria-label="Direct link to The Rules We Wrote" title="Direct link to The Rules We Wrote" translate="no">​</a></h3>
<p>We created six team rules in Dam Secure, each targeting a specific class of tenant isolation failure. Here's what they are and why each one matters.</p>
<p><strong>1. Never bypass TenantDb</strong></p>
<blockquote>
<p><em>Never bypass TenantDb. No direct DocumentClient.send() on tenant-scoped data. All access goes through TenantDb or a repository that enforces tenant ownership.</em></p>
</blockquote>
<p>This is the foundational rule. <code>TenantDb</code> exists to make tenant isolation automatic, but it only works if developers actually use it. A single <code>docClient.send(new GetCommand(...))</code> without tenant checking creates a bypass. This rule flags any direct DynamoDB operations on tenant-scoped tables that skip the tenant wrapper.</p>
<p><strong>2. Never expose tenant ID to the frontend</strong></p>
<blockquote>
<p><em>Never expose tenant ID to the frontend. Strip it from all API responses. The frontend identifies users by userId (Cognito sub), never by tenantId.</em></p>
</blockquote>
<p>If the tenant ID leaks to the client, it becomes an attack vector. An attacker who knows another tenant's ID could attempt IDOR attacks, even if the backend validates ownership. Keeping the ID server-side-only eliminates this class of attack entirely. This rule catches any API response that includes a <code>tenantId</code> field.</p>
<p><strong>3. Return generic errors</strong></p>
<blockquote>
<p><em>Return generic errors. Never leak table names, config values, tenant IDs, or internal error details in responses.</em></p>
</blockquote>
<p>AWS SDK errors contain rich debugging information: table names, condition expression details, request IDs. Forwarding these to the client tells an attacker exactly what your data model looks like. This rule flags any handler that re-throws or forwards raw error objects rather than returning sanitized messages.</p>
<p><strong>4. Scope every DB operation to the tenant</strong></p>
<blockquote>
<p><em>Scope every DB operation to the tenant. Use TenantDb for shared tables (it injects/validates tenantId automatically). For tenant-keyed tables like accounts, use tenantId from AuthContext as the partition key.</em></p>
</blockquote>
<p>This is the operational detail behind rule 1. We have two table patterns: shared tables (where <code>TenantDb</code> injects filtering) and tenant-keyed tables (where <code>tenantId</code> is the partition key). This rule checks that both patterns are followed correctly: that shared table access goes through <code>TenantDb</code>, and that tenant-keyed table access uses <code>authContext.tenantId</code> as the key, not a value from the request body.</p>
<p><strong>5. Always use withAuth()</strong></p>
<blockquote>
<p><em>Always use withAuth(). Every handler must be wrapped with withAuth(). This is the only way to get an AuthContext.</em></p>
</blockquote>
<p>If a handler skips <code>withAuth()</code>, it has no <code>AuthContext</code>, which means it has no verified tenant ID. It might still work (by reading tenant info from the request body or query params), but that tenant info is attacker-controlled. This rule flags any exported Lambda handler that isn't wrapped in <code>withAuth()</code>.</p>
<p><strong>6. Never read tenant ID from the client</strong></p>
<blockquote>
<p><em>Never read tenant ID from the client. Not from body, headers, query params, path params, or cookies. It comes from AuthContext only.</em></p>
</blockquote>
<p>The complement to rule 5. Even within a <code>withAuth()</code>-wrapped handler, a developer might accidentally read <code>event.body.tenantId</code> instead of <code>authContext.tenantId</code>. This rule catches that pattern: any reference to tenant ID from the request object rather than the auth context.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="using-extra-notes-to-descope-non-auth-operations">Using Extra Notes to Descope Non-Auth Operations<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#using-extra-notes-to-descope-non-auth-operations" class="hash-link" aria-label="Direct link to Using Extra Notes to Descope Non-Auth Operations" title="Direct link to Using Extra Notes to Descope Non-Auth Operations" translate="no">​</a></h3>
<p>Not every DynamoDB operation in the codebase goes through <code>withAuth()</code>. There are generic backend-to-backend Lambda functions. They're invoked by Step Functions or DynamoDB Streams, not by API Gateway. They don't have a user session, so they don't have an <code>AuthContext</code>.</p>
<p>Rather than creating exceptions that weaken the rules, we used Dam Secure's <strong>extra notes</strong> feature to descope these operations. We annotated the non-auth handlers with context explaining why they're exempt: they run in backend-only execution contexts with no user-facing API surface, and the tenant ID is passed in the Lambda invocation payload from a trusted upstream handler that <em>did</em> verify it.</p>
<p>This keeps the rules strict for the API surface (where the actual attack surface lives) without generating false positives on internal plumbing.</p>
<h3 class="anchor anchorTargetStickyNavbar_SAay" id="what-dam-secure-found">What Dam Secure Found<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#what-dam-secure-found" class="hash-link" aria-label="Direct link to What Dam Secure Found" title="Direct link to What Dam Secure Found" translate="no">​</a></h3>
<p>The scan surfaced several categories of issues across our repositories:</p>
<ul>
<li class=""><strong>DynamoDB operations lacking tenant-scoping</strong>: direct <code>DocumentClient.send()</code> calls that bypassed <code>TenantDb</code>, particularly in newer handlers that hadn't been reviewed yet</li>
<li class=""><strong>Backend error messages leaking to the frontend</strong>: raw AWS SDK errors being re-thrown instead of sanitized</li>
<li class=""><strong>Production secrets as plain strings</strong>: infrastructure code consuming secrets without going through Secrets Manager</li>
</ul>
<p>None of these were actively exploited vulnerabilities. They were structural weaknesses, places where the pattern was broken and tenant data <em>could</em> leak if the wrong conditions aligned. That's exactly what codified rules are good at catching: not the spectacular failures, but the quiet drift away from the architecture you designed.</p>
<h2 class="anchor anchorTargetStickyNavbar_SAay" id="lessons-learned">Lessons Learned<a href="https://docs.damsecure.ai/blog/how-we-built-tenant-isolation-for-hackaws-cloud#lessons-learned" class="hash-link" aria-label="Direct link to Lessons Learned" title="Direct link to Lessons Learned" translate="no">​</a></h2>
<p><strong>Make the safe path the easy path.</strong> <code>TenantDb</code> exists so developers don't have to remember to add tenant filters. <code>withAuth()</code> exists so developers don't have to parse JWTs. The rules enforce that these easy paths are the <em>only</em> paths.</p>
<p><strong>Tenant isolation is a property of the system, not individual handlers.</strong> Any single handler can look correct in isolation. The value of codified rules is verifying the property holds across the entire codebase, including code written by new team members who weren't there when the architecture was designed.</p>
<p><strong>Descoping is better than weakening.</strong> When we hit false positives on backend-to-backend handlers, the temptation was to soften the rules. Using extra notes to descope specific contexts kept the rules strict where they matter most: on the API surface.</p>
<p><strong>Defense in depth pays off.</strong> Partition keys, <code>TenantDb</code> wrappers, <code>AuthContext</code> resolution, response sanitization, generic errors. Any one of these could fail and the others would still prevent a cross-tenant data leak. The rules verify that all layers are present, not just one.</p>
<p>--</p>
<p>Written by <a href="https://www.linkedin.com/in/danielgrzelak/" target="_blank" rel="noopener noreferrer" class="">Daniel Grzelak</a>, Chief Innovation Officer at <a href="https://www.plerion.com/" target="_blank" rel="noopener noreferrer" class="">Plerion</a> - a leading cloud security platform with an AI security engineer you can hire.</p>]]></content:encoded>
            <category>AWS</category>
            <category>Tenant isolation</category>
            <category>Cloud security</category>
        </item>
    </channel>
</rss>