<?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: Karam Khoury</title>
    <description>The latest articles on DEV Community by Karam Khoury (@karamkhoury88).</description>
    <link>https://dev.to/karamkhoury88</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%2F4045189%2Fe44cb307-6c08-4fc0-8ff8-6bf3fa1dbfcc.png</url>
      <title>DEV Community: Karam Khoury</title>
      <link>https://dev.to/karamkhoury88</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karamkhoury88"/>
    <language>en</language>
    <item>
      <title>The AI Attack Wave Is Coming for Your App. Here's How to Harden It Now</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Mon, 07 Sep 2026 10:45:14 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/the-ai-attack-wave-is-coming-for-your-app-heres-how-to-harden-it-now-pmn</link>
      <guid>https://dev.to/karamkhoury88/the-ai-attack-wave-is-coming-for-your-app-heres-how-to-harden-it-now-pmn</guid>
      <description>&lt;h3&gt;
  
  
  What the industry warning actually means for the code you shipped last sprint
&lt;/h3&gt;




&lt;p&gt;Last year I watched a "quiet" internal API get hammered at 3 a.m. It wasn't a person. It was a script that read our public docs, inferred an undocumented endpoint, and walked our validation logic faster than any human tester ever had.&lt;/p&gt;

&lt;p&gt;That was a crude bot. The tools attackers now hold are not crude. In late August 2026, OpenAI, Microsoft, Google, Anthropic, and over 100 other organizations issued a joint warning: a surge of sophisticated, AI-powered attacks against critical infrastructure is coming, and the window to prepare is narrow.&lt;/p&gt;

&lt;p&gt;If you build software, this is not a policy story happening somewhere above you. It's a code review problem on your desk. Let me show you what changes and what to do about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Concept: The Attacker Just Got Cheaper, Faster, and Tireless
&lt;/h3&gt;

&lt;p&gt;AI doesn't invent new categories of vulnerability. It industrializes the old ones. The SQL injection, the missing authorization check, the leaked key in a log — attackers always knew how to exploit these. What's new is that discovery and exploitation now scale like a cloud workload.&lt;/p&gt;

&lt;p&gt;Think of it as the economics flipping. The cost of probing your entire attack surface just dropped to near zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        OLD MODEL                         NEW MODEL (AI-Assisted)
   ┌──────────────────┐              ┌──────────────────────────┐
   │  Human attacker  │              │   AI agent, 24/7          │
   │  picks 1 target  │              │   fans out across 10,000  │
   │  reads docs      │   ───────▶   │   endpoints, learns your  │
   │  tries by hand   │              │   error messages, adapts  │
   │  gives up at 5pm │              │   never sleeps            │
   └──────────────────┘              └──────────────────────────┘
     Slow, expensive,                  Fast, cheap, relentless,
     easily rate-limited               reads every response you leak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your defense strategy was implicitly sized for the old model. It assumed friction — that an attacker would only look so hard for so long. That assumption is now gone. Every weak default and every verbose error message you shipped is now discoverable at machine speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Dive: What Actually Changes for Your Application
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Your error messages are now free reconnaissance
&lt;/h4&gt;

&lt;p&gt;An AI agent doesn't need your source code. It reads what your app tells it. A stack trace, a "column 'user_role' does not exist" database error, a 500 that leaks a framework version — each one is a training signal that lets the attacker refine the next request. Security auditors have a name for this: &lt;strong&gt;CWE-209, Information Exposure Through an Error Message&lt;/strong&gt;. It has been on the books for years; AI just made it lucrative to exploit at scale.&lt;/p&gt;

&lt;p&gt;The fix is old advice that just became urgent: never let internal detail cross the boundary to the client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs — one place, applied globally&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;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsProduction&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseExceptionHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/error"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHsts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The handler returns a correlation ID, never the exception detail.&lt;/span&gt;
&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TraceIdentifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Full detail goes to your logs, not the wire.&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;IExceptionHandlerFeature&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IExceptionHandlerFeature&lt;/span&gt;&lt;span class="p"&gt;&amp;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;feature&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Unhandled exception {CorrelationId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"\"An unexpected error occurred.\","&lt;/span&gt;
        &lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status500InternalServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;correlationId&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;The client gets an opaque ID. You get the full trace in your logs. The attacker's agent gets nothing to learn from.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Authorization gaps get found in minutes, not months
&lt;/h4&gt;

&lt;p&gt;Broken object-level authorization — the classic "I changed the ID in the URL and saw someone else's data" — is the single most reliable bug for an automated agent to find. It simply enumerates IDs and watches which ones return &lt;code&gt;200&lt;/code&gt;. This is &lt;strong&gt;OWASP API1:2023, Broken Object Level Authorization (BOLA)&lt;/strong&gt; — consistently the number one API risk, and the easiest one for a tireless agent to weaponize.&lt;/p&gt;

&lt;p&gt;Stop trusting the ID in the request. Every data access must be scoped to the caller's identity at the query, not filtered afterward in memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG: fetch by ID, then hope you remember to check ownership.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetInvoiceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// RIGHT: the tenant/owner is part of the query. An unauthorized ID returns null.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetInvoiceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;callerTenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Invoices&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;invoiceId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;callerTenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefaultAsync&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;If ownership is a filter you apply &lt;em&gt;after&lt;/em&gt; fetching, an agent will eventually find the code path where you forgot. If ownership is part of the &lt;code&gt;WHERE&lt;/code&gt; clause, there is no path to forget.&lt;/p&gt;

&lt;p&gt;For multi-tenant systems, take it one level deeper and enforce isolation at the engine, not the developer. EF Core's &lt;code&gt;HasQueryFilter&lt;/code&gt; applies a tenant predicate to &lt;em&gt;every&lt;/em&gt; query for an entity automatically — so the junior dev who writes a new repository method next quarter inherits the isolation whether they remember it or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OnModelCreating — isolation becomes a property of the model, not of discipline.&lt;/span&gt;
&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasQueryFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenantProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentTenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit scoping in each query and a global filter aren't either/or — the filter is your safety net for human omission, the explicit &lt;code&gt;WHERE&lt;/code&gt; is your intent on the hot paths. Just remember that &lt;code&gt;IgnoreQueryFilters()&lt;/code&gt; exists, so keep it out of tenant-scoped code and flag it in review.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Rate limiting is no longer optional plumbing
&lt;/h4&gt;

&lt;p&gt;The old assumption was that abusive traffic looked obviously abusive. AI-driven traffic can mimic real user patterns while still probing thousands of variations. You need limits that are boring, default-on, and applied per-identity — not just per-IP, since IPs are cheap to rotate.&lt;/p&gt;

&lt;p&gt;.NET has this built in. Use it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RejectionStatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status429TooManyRequests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Partition by authenticated user where possible; fall back to IP.&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GlobalLimiter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PartitionedRateLimiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;partitionKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"anonymous"&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoteIpAddress&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"unknown"&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;RateLimitPartition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFixedWindowLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;FixedWindowRateLimiterOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PermitLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Window&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;QueueLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRateLimiter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One critical caveat: &lt;code&gt;AddRateLimiter&lt;/code&gt; counts requests &lt;em&gt;in-process&lt;/em&gt;, per node. On a multi-node or Kubernetes deployment, an attacker's round-robin traffic gets a fresh limit on every pod — so pair it with a shared enforcement point: a Redis-backed distributed partitioner, or edge/gateway limiting at YARP, an API gateway, or Cloudflare. In-process limiting is your last line, not your only one.&lt;/p&gt;

&lt;p&gt;This won't stop a determined adversary alone, but it removes the free lunch. It turns "probe everything instantly" back into "probe slowly and get noticed."&lt;/p&gt;

&lt;h4&gt;
  
  
  4. If your app calls an LLM, its input is now an attack surface
&lt;/h4&gt;

&lt;p&gt;This is the genuinely new category, and OWASP now tracks it in its own top-ten list: &lt;strong&gt;LLM01, Prompt Injection&lt;/strong&gt; and &lt;strong&gt;LLM02, Insecure Output Handling&lt;/strong&gt;. If any part of your system feeds untrusted text into a model — a support summarizer, a document Q&amp;amp;A feature, an agent that calls tools — then prompt injection is now in your threat model. A malicious document can carry instructions that hijack your agent's behavior.&lt;/p&gt;

&lt;p&gt;Two rules hold the line. First, never let model output trigger a privileged action without a deterministic check you control. Second, treat everything the model returns as untrusted user input, not as a command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The model SUGGESTS an action. Your code DECIDES whether it is allowed.&lt;/span&gt;
&lt;span class="n"&gt;ModelToolCall&lt;/span&gt; &lt;span class="n"&gt;suggestion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSuggestedActionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Deterministic authorization — the model has no say in this.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsActionPermitted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Model suggested unpermitted action {Action} for {Caller}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&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;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Forbid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model can be tricked. Your authorization layer cannot be talked out of its rules. Keep the decision in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Practical Impact: This Is Cheaper Than You Think
&lt;/h3&gt;

&lt;p&gt;None of the above is a rewrite. It's a set of defaults you apply once and enforce in review. The payoff compounds: a global exception handler and a query-scoped repository pattern don't just close today's holes — they make it structurally hard to open new ones.&lt;/p&gt;

&lt;p&gt;For your team, this becomes a review checklist, not a security sprint. New endpoints inherit the rate limiter. New data access inherits the ownership filter. The secure path becomes the path of least resistance, which is the only kind of security that survives a busy backlog.&lt;/p&gt;

&lt;p&gt;And the maintainability win is real. Code that leaks nothing, trusts nothing from the client, and keeps decisions deterministic is simply easier to reason about — for humans and for the next audit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Actionable Takeaways: Your Hardening Checklist
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Silence your app.&lt;/strong&gt; Ship a global exception handler in production that returns a correlation ID and nothing else. Grep your codebase for verbose error responses and stack traces on the wire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move ownership into the query.&lt;/strong&gt; Audit every data-access method. If tenant or user scoping is applied after the fetch, rewrite it into the &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on rate limiting by default.&lt;/strong&gt; Partition by authenticated identity, not just IP. Make new endpoints inherit it automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat LLM I/O as untrusted.&lt;/strong&gt; If you call a model, put a deterministic authorization check between its output and any real action. Log every rejected suggestion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate and centralize secrets.&lt;/strong&gt; Assume any key that has ever touched a log or a repo is compromised. Move to a managed secret store and short-lived credentials now, not after the incident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The warning from the industry wasn't "panic." It was "you have a narrow window." The good news is that the work fits inside your normal sprint cadence — if you start this one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the first thing you'd harden in your own stack? I read every comment.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.axios.com/2026/08/27/openai-anthropic-issue-dire-cyber-threat-warning" rel="noopener noreferrer"&gt;Axios: OpenAI, Anthropic, Microsoft warn of growing AI cyberattacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gizmodo.com/google-openai-and-over-100-companies-call-for-more-action-on-ai-driven-cyberattacks-2000804091" rel="noopener noreferrer"&gt;Gizmodo: Google, OpenAI and Over 100 Companies Call for More Action on AI-Driven Cyberattacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.csis.org/blogs/strategic-technologies-blog/beyond-autonomous-attacks-reality-ai-enabled-cyber-threats" rel="noopener noreferrer"&gt;CSIS: Beyond Autonomous Attacks — The Reality of AI-Enabled Cyber Threats&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Give Your .NET REST API an AI Mouth: Adding MCP So Claude and Gemini Can Actually Use It</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:18:11 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually-use-it-3fi4</link>
      <guid>https://dev.to/karamkhoury88/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually-use-it-3fi4</guid>
      <description>&lt;h3&gt;
  
  
  A production engineer's guide to wrapping an existing ASP.NET Core API with the Model Context Protocol — without tearing out your JWT stack.
&lt;/h3&gt;




&lt;p&gt;Last quarter a product manager dropped a Slack message that a lot of us are getting now: &lt;em&gt;"Can I just ask Claude to pull the open invoices from our system?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We already had the API. Fully built, JWT-secured, battle-tested in production. The problem was never the data — it was that an LLM has no idea our &lt;code&gt;/api/invoices?status=open&lt;/code&gt; endpoint exists, and even if it did, it can't read our OpenAPI spec and authenticate itself.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol (MCP) is the missing adapter. This is how you bolt it onto an API you already own, in an afternoon, and what to do about authentication before security reviews it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Concept: MCP Is an Adapter, Not a Rewrite
&lt;/h2&gt;

&lt;p&gt;MCP is an open protocol that lets AI clients (Claude Desktop, Gemini, Cursor, and others) &lt;strong&gt;discover and call your capabilities as "tools."&lt;/strong&gt; You do not rewrite your API. You stand up a thin MCP server that exposes selected endpoints as tools and forwards the calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   AI Chat (Claude / Gemini)
            │   MCP protocol (JSON-RPC)
            ▼
   ┌──────────────────────┐
   │   MCP Server (.NET)  │  ← [McpServerTool] methods
   │   - GetOpenInvoices  │
   │   - CreateTicket     │
   └─────────┬────────────┘
             │   HttpClient + Bearer/API key
             ▼
   ┌──────────────────────┐
   │  Existing REST API   │  ← unchanged, still JWT-secured
   └──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server is a translator: it speaks JSON-RPC to the model and plain HTTP to your existing API. Your business logic never moves.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Stand Up the MCP Server
&lt;/h3&gt;

&lt;p&gt;Use the official C# SDK (maintained together with Microsoft). Add it to a new minimal ASP.NET Core project so you can host a &lt;strong&gt;remote&lt;/strong&gt; MCP server over Streamable HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package ModelContextProtocol.AspNetCore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;WebApplicationBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHttpTransport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// Streamable HTTP for remote clients&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithToolsFromAssembly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// discover [McpServerTool] methods&lt;/span&gt;

&lt;span class="c1"&gt;// Typed client to your EXISTING API&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BackendApi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.internal.t1tech.com/"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;WebApplication&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapMcp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                     &lt;span class="c1"&gt;// exposes the /mcp endpoint&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire host. &lt;code&gt;MapMcp()&lt;/code&gt; wires up discovery, so any compliant AI client can enumerate your tools.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Turn an Endpoint Into a Tool
&lt;/h3&gt;

&lt;p&gt;A tool is just a method. The attributes and XML-style descriptions are not decoration — the model reads them to decide &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; to call you. Be explicit; vague descriptions cause hallucinated arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceTools&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;_httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;InvoiceTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_httpClientFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Returns open (unpaid) invoices for a given customer ID."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOpenInvoices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The numeric customer identifier."&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BackendApi"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"api/invoices?customerId=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;status=open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice there is no token in that call yet. That is the entire fight, and the next two sections are where production engineering actually happens.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: The JWT Question — Whose Identity Is Calling?
&lt;/h3&gt;

&lt;p&gt;Your API trusts a JWT. The naive instinct is to bake a service account token into the MCP server. &lt;strong&gt;Do not.&lt;/strong&gt; That collapses every user into one identity and hands the LLM god-mode over your data.&lt;/p&gt;

&lt;p&gt;The correct model: the MCP server is an &lt;strong&gt;OAuth 2.1 Resource Server.&lt;/strong&gt; The AI client authenticates the &lt;em&gt;human&lt;/em&gt;, receives a token, and passes it through. The MCP Authorization spec standardizes this with Protected Resource Metadata (RFC 9728), so the client can discover where to log in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJwtBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://login.t1tech.com/"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Audience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mcp-invoice-server"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ...after Build()&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthentication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapMcp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then forward the caller's identity instead of a hardcoded secret. Grab the incoming token from &lt;code&gt;HttpContext&lt;/code&gt; and attach it downstream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;InvoiceTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt; &lt;span class="n"&gt;httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_httpClientFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_httpContextAccessor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateAuthorizedClientAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BackendApi"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"access_token"&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&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;Now the existing API sees the &lt;em&gt;real&lt;/em&gt; user's scopes and roles. Your authorization rules keep working, untouched. The LLM cannot read an invoice the human behind it could not read.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4: Do You Need an API Key Instead? Pick By Client Type
&lt;/h3&gt;

&lt;p&gt;JWT/OAuth is right for interactive chat where a human is present. But not every MCP consumer is a person clicking "Authorize." Choose the scheme by &lt;em&gt;who&lt;/em&gt; connects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote HTTP server, human in a chat client → OAuth 2.1 with JWT pass-through.&lt;/strong&gt; This is the spec's blessed path. Full user identity, scoped access, revocable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local &lt;code&gt;stdio&lt;/code&gt; server or machine-to-machine automation → API key.&lt;/strong&gt; When there is no interactive login (a scheduled agent, a local dev tool), issue a scoped API key and read it from configuration, never from source.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API key path — for non-interactive / stdio clients&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Backend:ApiKey"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Missing Backend:ApiKey."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Api-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My recommendation for a team shipping this: &lt;strong&gt;default to OAuth 2.1 JWT pass-through for anything remote&lt;/strong&gt;, and reserve API keys for headless integrations where you can mint &lt;em&gt;narrowly scoped, per-integration&lt;/em&gt; keys and rotate them. Treat a static API key like a password — short TTL, per-client, logged, revocable. Never a single shared secret with full API surface.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Flow: What Actually Happens When Claude Uses Your Tool
&lt;/h3&gt;

&lt;p&gt;Code is abstract until you watch a real request move through it. Here is the end-to-end round trip when a user types &lt;em&gt;"Show me the open invoices for customer 4821"&lt;/em&gt; into Claude.&lt;/p&gt;

&lt;p&gt;But first — the question everyone asks: &lt;strong&gt;where does Claude get that JWT?&lt;/strong&gt; It is not magic and Claude does not "have your token." The MCP client obtains it through a standard OAuth 2.1 handshake the &lt;em&gt;first&lt;/em&gt; time it connects, and this is worth seeing on its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0a] Claude → MCP Server:  first call, no token
     ← 401 Unauthorized
       WWW-Authenticate: Bearer resource_metadata="https://mcp.t1tech.com/.well-known/..."

[0b] Claude reads Protected Resource Metadata (RFC 9728)
     → learns which Authorization Server (your IdP) issues tokens

[0c] Claude runs OAuth 2.1 Authorization Code + PKCE:
     → opens a browser window
     → USER logs in at login.t1tech.com and clicks "Allow"
     → IdP redirects back with an auth code
     → Claude exchanges code → access_token (JWT) + refresh_token

[0d] Claude securely stores the token and reuses it.
     Refresh happens silently; the login prompt does NOT repeat each message.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical point: &lt;strong&gt;the human authenticates directly with your identity provider, not with Claude.&lt;/strong&gt; Claude never sees a password. It receives a scoped, expiring JWT through the same OAuth flow your web frontend would use — and your MCP server, as the Resource Server, advertises where that login lives via the 401 challenge in Step 0a.&lt;/p&gt;

&lt;p&gt;On the .NET side, &lt;code&gt;RequireAuthorization()&lt;/code&gt; already returns the 401. The one extra thing you owe the client is the discovery pointer in Step 0b — register the MCP server as a protected resource so the SDK emits the &lt;code&gt;resource_metadata&lt;/code&gt; challenge and serves the metadata document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Advertises this server as an OAuth 2.1 Resource Server&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResourceMetadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProtectedResourceMetadata&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;AuthorizationServers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://login.t1tech.com/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single registration is what turns "Claude somehow has a token" into a discoverable, spec-compliant login the client can drive on its own.&lt;/p&gt;

&lt;p&gt;Only &lt;em&gt;after&lt;/em&gt; that handshake does the everyday request loop below run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1] USER  →  Claude Chat
    "Show me the open invoices for customer 4821."

[2] Claude → MCP Server:  discovery (once, on connect)
    "What tools do you have?"
    ← [{ name: "GetOpenInvoices",
         description: "Returns open (unpaid) invoices for a customer ID.",
         params: { customerId: int } }]

[3] Claude reasons:
    intent = list open invoices  →  tool = GetOpenInvoices
    extracts argument  →  customerId = 4821

[4] Claude → MCP Server:  tools/call (JSON-RPC)
    Authorization: Bearer &amp;lt;the signed-in user's JWT&amp;gt;
    { "name": "GetOpenInvoices", "arguments": { "customerId": 4821 } }

[5] MCP Server validates the JWT, then forwards downstream:
    GET /api/invoices?customerId=4821&amp;amp;status=open
    Authorization: Bearer &amp;lt;same JWT — user identity preserved&amp;gt;

[6] Existing REST API applies the user's scopes → returns JSON

[7] MCP Server → Claude:  tool result (raw JSON)
    [{ "id": 5567, "amount": 1200.00, "due": "2026-09-15" }, ...]

[8] Claude → USER:
    "Customer 4821 has 2 open invoices totaling $2,050 —
     one due Sep 15 ($1,200) and one due Oct 2 ($850)."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are worth pausing on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 happens once, not per message.&lt;/strong&gt; The client caches your tool catalog on connect. This is why the &lt;code&gt;[Description]&lt;/code&gt; text is load-bearing — the model chooses tools purely from it, long before your code ever runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 to Step 6 is the whole security story.&lt;/strong&gt; The JWT the human logged in with is the same JWT that reaches your API. Claude never sees a service credential, and it can never retrieve an invoice the signed-in user isn't authorized to see. The model orchestrates; your API still decides.&lt;/p&gt;

&lt;p&gt;If you want to see this concretely, the tool result at Step 7 is exactly the string your &lt;code&gt;GetOpenInvoices&lt;/code&gt; method returned — Claude does the natural-language summarization in Step 8. You return data; the model handles the prose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Impact
&lt;/h2&gt;

&lt;p&gt;The payoff is not "AI hype." It is that a capability you already built becomes usable by a new class of client with near-zero duplication.&lt;/p&gt;

&lt;p&gt;Your team writes one thin tool method per endpoint you want to expose — not a second API. Because identity flows through, your security posture is &lt;em&gt;unchanged&lt;/em&gt;: the same JWT, the same scopes, the same audit trail. And because tools are just decorated C# methods, they unit-test like any other service, and new endpoints become new tools in minutes.&lt;/p&gt;

&lt;p&gt;The long-term maintainability win is that MCP is a stable seam. When you swap Claude for Gemini, or add a third client, nothing changes on your side. You built the adapter once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Actionable Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrap, don't rewrite.&lt;/strong&gt; Stand up a separate MCP server that forwards to your existing API over &lt;code&gt;HttpClient&lt;/code&gt;. Business logic stays put.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write ruthless tool descriptions.&lt;/strong&gt; The model chooses tools from your &lt;code&gt;[Description]&lt;/code&gt; text. Vague text produces bad calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass the user's identity through — never hardcode a service token.&lt;/strong&gt; Make the MCP server an OAuth 2.1 Resource Server and forward the caller's JWT so existing authorization still applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose auth by client type.&lt;/strong&gt; OAuth 2.1/JWT for interactive remote clients; scoped, rotatable API keys only for headless &lt;code&gt;stdio&lt;/code&gt; or M2M integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the smallest surface first.&lt;/strong&gt; Expose read-only tools before write tools, and let least-privilege scopes gate everything the LLM can reach.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>dotnet</category>
      <category>mcp</category>
    </item>
    <item>
      <title>MCP vs RAG: Two AI Buzzwords, Explained Without the Buzzwords</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Thu, 06 Aug 2026 12:33:37 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/mcp-vs-rag-two-ai-buzzwords-explained-without-the-buzzwords-d2h</link>
      <guid>https://dev.to/karamkhoury88/mcp-vs-rag-two-ai-buzzwords-explained-without-the-buzzwords-d2h</guid>
      <description>&lt;p&gt;A few weeks ago, a junior engineer on my team asked me: "Should we use RAG or MCP for this feature?"&lt;/p&gt;

&lt;p&gt;He said it like the two were competing options. Like picking between React and Vue.&lt;/p&gt;

&lt;p&gt;They're not. One is about &lt;strong&gt;giving an AI knowledge&lt;/strong&gt;. The other is about &lt;strong&gt;giving an AI hands&lt;/strong&gt;. Once you see that difference, the buzzwords stop being scary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept, in One Line Each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RAG (Retrieval-Augmented Generation):&lt;/strong&gt; Before the AI answers you, it goes and &lt;em&gt;reads&lt;/em&gt; relevant documents first, then writes its answer based on what it just read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP (Model Context Protocol):&lt;/strong&gt; A standard way for an AI to &lt;em&gt;connect to and use&lt;/em&gt; outside tools — like your calendar, your database, or your file system — instead of just talking about them.&lt;/p&gt;

&lt;p&gt;Here's the simplest way to picture it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RAG:  Question -&amp;gt; Search your documents -&amp;gt; Read the best matches -&amp;gt; Answer
MCP:  Question -&amp;gt; Call a real tool (email, calendar, database) -&amp;gt; Get a real result -&amp;gt; Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RAG makes the AI &lt;strong&gt;smarter about what it knows&lt;/strong&gt;.&lt;br&gt;
MCP makes the AI &lt;strong&gt;capable of doing something&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where They're Similar
&lt;/h3&gt;

&lt;p&gt;Both exist to fix the same core problem: an AI, by itself, only knows what it was trained on, up to a certain date. It can't see your company's internal wiki. It can't see today's stock price. It can't see the email that just landed in your inbox.&lt;/p&gt;

&lt;p&gt;Both RAG and MCP are ways of feeding an AI &lt;em&gt;fresh, outside information&lt;/em&gt; at the moment you ask your question, instead of relying only on what it memorized during training.&lt;/p&gt;

&lt;p&gt;That's the whole family resemblance. After that, they split in very different directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where They're Different
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;RAG is about reading. MCP is about acting.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;RAG&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it does&lt;/td&gt;
&lt;td&gt;Finds and reads relevant text&lt;/td&gt;
&lt;td&gt;Connects to and operates a tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical use&lt;/td&gt;
&lt;td&gt;"Answer using our documents"&lt;/td&gt;
&lt;td&gt;"Book this meeting" or "Update this record"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Better, more accurate answers&lt;/td&gt;
&lt;td&gt;Real actions with real side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example source&lt;/td&gt;
&lt;td&gt;A PDF, a knowledge base, a set of notes&lt;/td&gt;
&lt;td&gt;A calendar app, an email inbox, a database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Think of RAG as a &lt;strong&gt;very fast research assistant&lt;/strong&gt;. You ask a question, they run to the library, grab the three most relevant pages, and hand them to you before you even finish your coffee.&lt;/p&gt;

&lt;p&gt;Think of MCP as a &lt;strong&gt;universal remote control&lt;/strong&gt;. It doesn't know anything by itself — but it can press the right buttons on whichever device you point it at: your calendar, your CRM, your ticketing system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where To Use Each
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use RAG when the problem is "the AI doesn't know this."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A customer support bot that needs to answer questions using your product manuals.&lt;/li&gt;
&lt;li&gt;A legal assistant that needs to reference your actual contracts, not generic legal knowledge.&lt;/li&gt;
&lt;li&gt;An internal chatbot that answers HR questions using your company's real policy documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use MCP when the problem is "the AI needs to do this."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An assistant that checks your real calendar and books a meeting for you.&lt;/li&gt;
&lt;li&gt;A support agent that actually creates a ticket in your helpdesk system, not just describes one.&lt;/li&gt;
&lt;li&gt;A coding assistant that reads your real files and runs your real tests, instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use both when you need an AI that is both well-informed and useful.&lt;/strong&gt; A great example: a customer support AI that uses RAG to pull the correct refund policy from your documents, then uses MCP to actually process the refund in your billing system.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Real-Life Example
&lt;/h3&gt;

&lt;p&gt;Imagine you run an online store and you build an AI assistant for customer support.&lt;/p&gt;

&lt;p&gt;A customer asks: &lt;em&gt;"What's your return policy for items bought during the sale, and can you start my return?"&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG&lt;/strong&gt; kicks in first. It searches your actual return policy documents, finds the section about sale items (which usually has different rules than regular items), and pulls the exact, correct answer instead of a generic guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP&lt;/strong&gt; kicks in next. It connects to your order management system, finds the customer's real order, and actually starts the return process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without RAG, the AI might confidently give the wrong policy. Without MCP, the AI could only &lt;em&gt;tell&lt;/em&gt; the customer how to start a return, not &lt;em&gt;actually&lt;/em&gt; start it. Together, the customer gets a correct answer and a completed action, in one conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Impact for Your Team
&lt;/h2&gt;

&lt;p&gt;Getting this distinction right changes how you scope a project.&lt;/p&gt;

&lt;p&gt;If a stakeholder says "make the AI smarter about our documents," that's a RAG problem — you're building a search-and-retrieve pipeline over your content.&lt;/p&gt;

&lt;p&gt;If they say "make the AI actually do things in our systems," that's an MCP problem — you're building integrations and giving the AI safe, well-defined tools to call.&lt;/p&gt;

&lt;p&gt;Confusing the two leads to wasted sprints: teams building elaborate document search when what the business actually wanted was for the AI to update a record, or vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actionable Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RAG = knowledge.&lt;/strong&gt; Use it when the AI needs facts it wasn't trained on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP = capability.&lt;/strong&gt; Use it when the AI needs to perform a real action in a real system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're not rivals.&lt;/strong&gt; Most serious AI products end up using both together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope your project by the verb.&lt;/strong&gt; "Answer using X" → RAG. "Do X" → MCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start small.&lt;/strong&gt; Pick one document set for RAG, or one tool for MCP, prove it works, then expand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you strip away the acronyms, both ideas are things you already understand: look it up before you answer, and use the right tool for the job.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>mcp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Concurrency vs Parallelism: A Simple Guide</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Tue, 04 Aug 2026 06:44:36 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/concurrency-vs-parallelism-a-simple-guide-33l9</link>
      <guid>https://dev.to/karamkhoury88/concurrency-vs-parallelism-a-simple-guide-33l9</guid>
      <description>&lt;h2&gt;
  
  
  Concurrency: juggling tasks
&lt;/h2&gt;

&lt;p&gt;Think of concurrency like a chef in a busy kitchen. The chef doesn't cook all dishes at the exact same time — they chop some vegetables, stir a pot, check the oven, and go back to chopping. They make progress on multiple tasks by switching between them quickly.&lt;/p&gt;

&lt;p&gt;In computing, concurrency means a system can handle multiple tasks overlapping in time, but not necessarily running at the same instant.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks take turns (like a single CPU core switching between processes).&lt;/li&gt;
&lt;li&gt;Useful when tasks involve waiting (downloading files, handling user input).&lt;/li&gt;
&lt;li&gt;Common in applications like web servers that manage many users at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Parallelism: doing tasks side by side
&lt;/h2&gt;

&lt;p&gt;Now imagine two chefs in the same kitchen. One chops vegetables while the other grills meat — both working at the same time.&lt;/p&gt;

&lt;p&gt;That's parallelism: tasks actually running simultaneously, using multiple resources (like multiple CPU cores).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks run truly at the same time.&lt;/li&gt;
&lt;li&gt;Requires hardware support (multi-core processors, GPUs).&lt;/li&gt;
&lt;li&gt;Used for heavy computation (video encoding, scientific simulations).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-world examples
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency:&lt;/strong&gt; a chat app handling thousands of messages — it switches between users so fast it seems instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism:&lt;/strong&gt; a weather-forecast model running calculations across multiple CPUs to predict storms faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to use each
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;concurrency&lt;/strong&gt; when tasks involve waiting (I/O, network requests).&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;parallelism&lt;/strong&gt; when you need brute-force speed (data processing, rendering).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; = managing multiple tasks efficiently (even on a single core).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism&lt;/strong&gt; = executing multiple tasks at the same time (needs multiple cores).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are essential in modern software, and understanding them helps you write faster, more responsive programs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Karam Khoury — Lead Software Engineer (.NET &amp;amp; Azure) with 14+ years building secure, scalable fintech systems. More at &lt;a href="https://karamkhoury.me" rel="noopener noreferrer"&gt;karamkhoury.me&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>parallelism</category>
      <category>concurrency</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>AWS Introduction for Beginner Software Engineers</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Tue, 04 Aug 2026 06:39:39 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/aws-introduction-for-beginner-software-engineers-4ec5</link>
      <guid>https://dev.to/karamkhoury88/aws-introduction-for-beginner-software-engineers-4ec5</guid>
      <description>&lt;p&gt;As a software engineer starting with AWS, you might feel overwhelmed by all the services. Let me break down the key components with real-world examples to help you get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. DNS &amp;amp; How AWS Route 53 Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Users can't remember IP addresses like &lt;code&gt;54.210.167.101&lt;/code&gt; for your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; AWS Route 53 (DNS service) maps &lt;code&gt;yourdomain.com&lt;/code&gt; to your servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real example:&lt;/strong&gt; Your startup has servers in Ohio and Frankfurt. Route 53 automatically sends European users to Frankfurt and routes US traffic to Ohio — faster load times for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Networking: VPC &amp;amp; Subnets
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC:&lt;/strong&gt; Your private cloud network (like an office building).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public subnet:&lt;/strong&gt; For web servers (needs internet access).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private subnet:&lt;/strong&gt; For databases (blocked from direct internet access).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internet Gateway:&lt;/strong&gt; Connects public subnets to the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT Gateway:&lt;/strong&gt; Lets private subnets reach the internet (one-way).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real example:&lt;/strong&gt; In your e-commerce app, the web server runs in a public subnet while the database sits in a private subnet.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Storing &amp;amp; Delivering Static Files (S3 + CloudFront)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon S3:&lt;/strong&gt; Stores images, videos, and logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront (CDN):&lt;/strong&gt; Caches content globally for faster loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real example:&lt;/strong&gt; Your blog hosts images in S3; CloudFront caches them in 450+ locations, so a reader in Tokyo gets images from Japan instead of Virginia — cutting load time by ~50%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Enable S3 Versioning to recover accidentally deleted files.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Running Backend Services
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AWS Lambda (Serverless)&lt;/strong&gt; — best for event-driven tasks. &lt;em&gt;Example:&lt;/em&gt; a photo app resizes images automatically on upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EC2 (Virtual Servers)&lt;/strong&gt; — best for full control (custom OS, legacy apps). &lt;em&gt;Example:&lt;/em&gt; hosting a Java monolith with specific dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ECS/EKS (Containers)&lt;/strong&gt; — best for microservices (Docker/Kubernetes). &lt;em&gt;Example:&lt;/em&gt; a food-delivery app with separate services for orders, payments, and tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Databases: Picking the Right One
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon RDS (SQL):&lt;/strong&gt; Structured data with relationships — e.g. user accounts + orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB (NoSQL):&lt;/strong&gt; High-speed, scalable lookups — e.g. a ride-sharing app tracking real-time driver locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora (High-Performance SQL):&lt;/strong&gt; Low-latency apps — e.g. stock-trading platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Adding AI/ML to Your App
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock (pre-trained AI):&lt;/strong&gt; e.g. a customer-support chatbot trained on your FAQ docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SageMaker (custom ML models):&lt;/strong&gt; e.g. a recommendation engine ("users who bought X also liked Y").&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Security Essentials
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC:&lt;/strong&gt; Controls how your networking works — what can connect to what, what reaches the internet, and what stays private.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NACLs (Network ACLs):&lt;/strong&gt; A firewall for an entire subnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Groups:&lt;/strong&gt; A firewall for each specific instance or service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM (Identity &amp;amp; Access Management):&lt;/strong&gt; Controls who can access what.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Golden rule — least privilege:&lt;/strong&gt; a Lambda function should only access the one DynamoDB table it needs; backup processes get read-only access; only DevOps can reach production databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Monitoring &amp;amp; Debugging
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch:&lt;/strong&gt; Tracks performance metrics and logs (EC2 CPU, Lambda errors). &lt;em&gt;Example:&lt;/em&gt; alert when database CPU hits 90%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail:&lt;/strong&gt; Audit logs ("who changed IAM policies?", "who deleted an S3 bucket?") — critical for compliance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Imagine building "Twitter Lite":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Route 53 routes users to the nearest server.&lt;/li&gt;
&lt;li&gt;CloudFront + S3 deliver profile pictures fast.&lt;/li&gt;
&lt;li&gt;EC2 or Lambda runs the backend API.&lt;/li&gt;
&lt;li&gt;DynamoDB stores tweets for instant loading.&lt;/li&gt;
&lt;li&gt;CloudWatch alerts you if something breaks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next steps for beginners
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hands-on:&lt;/strong&gt; try the AWS Free Tier (12 months free).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First project:&lt;/strong&gt; deploy a static website on S3 + CloudFront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep dive:&lt;/strong&gt; learn Lambda + DynamoDB.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Written by Karam Khoury — Lead Software Engineer (.NET &amp;amp; Azure) with 14+ years building secure, scalable fintech systems. More at &lt;a href="https://karamkhoury.me" rel="noopener noreferrer"&gt;karamkhoury.me&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Oops, I Committed a Secret: A Calm Guide to Scrubbing Git History</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:00:25 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/oops-i-committed-a-secret-a-calm-guide-to-scrubbing-git-history-38ll</link>
      <guid>https://dev.to/karamkhoury88/oops-i-committed-a-secret-a-calm-guide-to-scrubbing-git-history-38ll</guid>
      <description>&lt;p&gt;You just finished a great feature. You're in the flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Fixed the bug!"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you see it, staring back from the GitHub UI: your live API key. 💀&lt;/p&gt;

&lt;p&gt;We've all been there. Here's the calm, correct order of operations — from "quick save" for a mistake you caught instantly, to the deep clean for a secret that's been buried in history for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: Rotate the secret first (yes, before anything else)
&lt;/h2&gt;

&lt;p&gt;This is the step people skip, and it's the only one that's non-negotiable. &lt;strong&gt;The moment a secret hits a remote, assume it's compromised.&lt;/strong&gt; Bots scrape public GitHub for credentials within &lt;em&gt;seconds&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So before you touch history:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Revoke / rotate&lt;/strong&gt; the leaked key, token, or password at its source (AWS, Stripe, your DB, wherever).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Then&lt;/em&gt; clean the repo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cleaning history without rotating is like changing the locks after posting the old key online. Rotate first, always.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: You caught it before pushing (the quick save)
&lt;/h2&gt;

&lt;p&gt;If the secret is only in your &lt;strong&gt;last, un-pushed commit&lt;/strong&gt;, this is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop tracking the file and ignore it going forward&lt;/span&gt;
git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; .env
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore

&lt;span class="c"&gt;# Fold the fix into the last commit&lt;/span&gt;
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Nothing left the building. (Still rotate the key if it was ever real.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: It's deep in history, or already pushed (the deep clean)
&lt;/h2&gt;

&lt;p&gt;Once the secret is several commits back — or on the remote — you need to rewrite history. The modern, recommended tool is &lt;strong&gt;&lt;code&gt;git filter-repo&lt;/code&gt;&lt;/strong&gt; (Git's own docs now steer you here instead of the old &lt;code&gt;filter-branch&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install (Python)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;git-filter-repo

&lt;span class="c"&gt;# Option A: remove a whole file from ALL history&lt;/span&gt;
git filter-repo &lt;span class="nt"&gt;--path&lt;/span&gt; config/secrets.yml &lt;span class="nt"&gt;--invert-paths&lt;/span&gt;

&lt;span class="c"&gt;# Option B: redact a specific string everywhere it appears&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'AKIAIOSFODNN7EXAMPLE==&amp;gt;REDACTED'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; replacements.txt
git filter-repo &lt;span class="nt"&gt;--replace-text&lt;/span&gt; replacements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;strong&gt;BFG Repo-Cleaner&lt;/strong&gt; if you like a faster, simpler tool for the common cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bfg &lt;span class="nt"&gt;--delete-files&lt;/span&gt; id_rsa
bfg &lt;span class="nt"&gt;--replace-text&lt;/span&gt; passwords.txt

&lt;span class="c"&gt;# Expire the old refs and garbage-collect&lt;/span&gt;
git reflog expire &lt;span class="nt"&gt;--expire&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git gc &lt;span class="nt"&gt;--prune&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="nt"&gt;--aggressive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Then: force-push and warn your team
&lt;/h2&gt;

&lt;p&gt;History rewriting changes commit hashes, so you must overwrite the remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ This rewrites shared history. Tell every collaborator to &lt;strong&gt;re-clone or hard-reset&lt;/strong&gt; — if someone merges an old local branch, the secret comes right back. And remember: on GitHub, the old commit can still be reachable via its hash / cached views for a while, which is exactly why &lt;strong&gt;Step 0 (rotation)&lt;/strong&gt; is what actually protects you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention: never live on the edge again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt; first.&lt;/strong&gt; Add &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;*.pem&lt;/code&gt;, &lt;code&gt;secrets.*&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; the first commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables / a secret manager&lt;/strong&gt; (Azure Key Vault, AWS Secrets Manager, Doppler) — secrets never touch the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pre-commit scanner&lt;/strong&gt; so a secret is blocked &lt;em&gt;before&lt;/em&gt; it's committed:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# gitleaks catches secrets in staged changes&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;gitleaks
gitleaks protect &lt;span class="nt"&gt;--staged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that into a pre-commit hook and the "cold sweat" moment mostly disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rotate the secret&lt;/strong&gt; (non-negotiable, do it first).&lt;/li&gt;
&lt;li&gt;Un-pushed? &lt;code&gt;git commit --amend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In history? &lt;code&gt;git filter-repo&lt;/code&gt; or BFG, then &lt;code&gt;git push --force&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tell your team to re-clone.&lt;/li&gt;
&lt;li&gt;Add a pre-commit scanner so it never happens again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How do you handle secrets in your workflow — pre-commit hooks, or living life on the edge? 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Karam Khoury — Lead Software Engineer (.NET &amp;amp; Azure) with 14+ years building secure, scalable fintech systems. More at &lt;a href="https://karamkhoury.me" rel="noopener noreferrer"&gt;karamkhoury.me&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>System Design: The 6 Core Load Balancing Algorithms, Explained</title>
      <dc:creator>Karam Khoury</dc:creator>
      <pubDate>Fri, 24 Jul 2026 11:58:19 +0000</pubDate>
      <link>https://dev.to/karamkhoury88/system-design-the-6-core-load-balancing-algorithms-explained-2gc</link>
      <guid>https://dev.to/karamkhoury88/system-design-the-6-core-load-balancing-algorithms-explained-2gc</guid>
      <description>&lt;p&gt;Stop guessing how your traffic scales — understand the routing mechanics. Here's how the six core algorithms actually dictate traffic distribution, and when to reach for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Round Robin
&lt;/h2&gt;

&lt;p&gt;Distributes requests sequentially across the server pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Dealing a deck of cards — Server A gets request 1, Server B gets 2, Server C gets 3, then the cycle repeats. Best for identical, stateless servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Weighted Round Robin
&lt;/h2&gt;

&lt;p&gt;Sequential routing that accounts for unequal hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Server A has 64 GB RAM, Server B has 32 GB. Assign weight 2 to A and 1 to B, and A receives exactly twice as many requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Least Connections
&lt;/h2&gt;

&lt;p&gt;Routes traffic to the server with the fewest active, open sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Joining the supermarket checkout with the fewest people. Ideal for long-lived connections — WebSockets, heavy database queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. IP Hash
&lt;/h2&gt;

&lt;p&gt;Uses a hash of the client's IP to guarantee the same client hits the same server every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A user at 192.168.1.50 is always routed to Server C. Mandatory for legacy stateful apps that store session data in-memory rather than a distributed cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Least Response Time
&lt;/h2&gt;

&lt;p&gt;A hybrid: lowest number of active connections combined with the lowest average response time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Joining the line that has both the fewest people &lt;em&gt;and&lt;/em&gt; the fastest cashier. Essential when server performance degrades unpredictably.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Adaptive (Resource-Based)
&lt;/h2&gt;

&lt;p&gt;Routes based on real-time hardware telemetry from an agent on each server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; The load balancer queries CPU, memory, and I/O wait in real time, and avoids any server spiking above 80% utilization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Choose your algorithm based on your infrastructure and state management — not trends. Build systems that scale predictably.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Karam Khoury — Lead Software Engineer (.NET &amp;amp; Azure), building secure, scalable fintech systems. More at &lt;a href="https://karamkhoury.me" rel="noopener noreferrer"&gt;karamkhoury.me&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
