<?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: JorelFuji</title>
    <description>The latest articles on DEV Community by JorelFuji (@jorelfuji).</description>
    <link>https://dev.to/jorelfuji</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%2F3998864%2Fb138ba9b-6bd8-4ba6-b511-a96e73f2cf65.png</url>
      <title>DEV Community: JorelFuji</title>
      <link>https://dev.to/jorelfuji</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jorelfuji"/>
    <language>en</language>
    <item>
      <title>REPO LAYOUT VS. RUNTIME ARCHITECTURE</title>
      <dc:creator>JorelFuji</dc:creator>
      <pubDate>Wed, 15 Jul 2026 14:31:02 +0000</pubDate>
      <link>https://dev.to/jorelfuji/repo-layout-vs-runtime-architecture-2oph</link>
      <guid>https://dev.to/jorelfuji/repo-layout-vs-runtime-architecture-2oph</guid>
      <description>&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fupdbveq2gxl0u7ot2qvn.png" alt="cover image" width="800" height="424"&gt;
&lt;/h2&gt;

&lt;p&gt;title: "Monorepo is not Monolith — and the difference decides your CVE response time"&lt;br&gt;
published: false&lt;br&gt;
description: "Repo layout and runtime topology are orthogonal axes. Here's what each one actually costs you in version control, updates, scaling, and scanning."&lt;br&gt;
tags: architecture, devops, security, kubernetes&lt;br&gt;
cover_image: &lt;/p&gt;
&lt;h2&gt;
  
  
  canonical_url: 
&lt;/h2&gt;

&lt;p&gt;Repo layout and deployment topology are &lt;strong&gt;orthogonal&lt;/strong&gt;. Most orgs conflate them, split their runtime to solve a version-control problem, and end up with a distributed monolith that fails together, deploys together, and now also drops packets.&lt;/p&gt;

&lt;p&gt;The decision that actually matters is &lt;strong&gt;where you enforce a boundary&lt;/strong&gt; — compiler, or network — and you make that choice once per boundary, not once per company.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two axes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1c5txc6ubs3xoi0wev5l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1c5txc6ubs3xoi0wev5l.png" alt="2x2 matrix: repo layout on the horizontal axis, deployment unit on the vertical axis, with real-world examples in each of the four quadrants" width="799" height="444"&gt;&lt;/a&gt;&lt;/p&gt;
Fig. 1 — The four real options. "Monorepo vs. microservices" is a category error.



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Reversible?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Source layout&lt;/strong&gt; (mono/poly-repo)&lt;/td&gt;
&lt;td&gt;How do teams land code?&lt;/td&gt;
&lt;td&gt;Yes — painful but mechanical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Deployment unit&lt;/strong&gt; (monolith/services)&lt;/td&gt;
&lt;td&gt;What ships and scales independently?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — data migrations aren't &lt;code&gt;git revert&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;Asymmetry of regret.&lt;/strong&gt; Repo layout is a two-way door. Runtime topology is a one-way door the moment each service owns its data. Sequence accordingly: change the repo layout first, and only split the runtime once module boundaries have proven stable in a monolith.&lt;br&gt;

&lt;/div&gt;


&lt;p&gt;&lt;/p&gt;
  Why is "polyrepo + monolith" so common if it's strictly worse?
  &lt;br&gt;
Because it arrives by accretion, not decision. Someone extracts a shared auth library into its own repo for "reuse," and now a one-line change is two PRs, a release, and a version bump — with none of the deploy independence that would justify it. The tell: your internal libraries have semantic versions but only one consumer.&lt;br&gt;


&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually runs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqgjf1wi34d94jlmcjcx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqgjf1wi34d94jlmcjcx.png" alt="Side by side: a monolith as one container with four internal modules, versus four separate service containers communicating over the network" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;
Fig. 2 — Same four capabilities. The difference is where the boundary is enforced.



&lt;p&gt;A network call is a function call that acquired five new failure modes and lost its type checker. That's the entire trade:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You gain&lt;/th&gt;
&lt;th&gt;You pay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Independent deploy&lt;/td&gt;
&lt;td&gt;Timeouts, retries, backoff, idempotency keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent scale&lt;/td&gt;
&lt;td&gt;mTLS certs + rotation, SPIFFE identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent failure&lt;/td&gt;
&lt;td&gt;Distributed tracing, or you debug blind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-workload least privilege&lt;/td&gt;
&lt;td&gt;NetworkPolicy per pair, contract versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team autonomy&lt;/td&gt;
&lt;td&gt;Eventual consistency where you had a transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The availability math nobody runs first
&lt;/h3&gt;

&lt;p&gt;Services in a &lt;strong&gt;synchronous&lt;/strong&gt; request path multiply, they don't average. For 

&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 services each at availability 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;p&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;h&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;At a respectable 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.999&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 per service:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Services in path&lt;/th&gt;
&lt;th&gt;Path availability&lt;/th&gt;
&lt;th&gt;Annual downtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (monolith)&lt;/td&gt;
&lt;td&gt;99.90%&lt;/td&gt;
&lt;td&gt;~8.8 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;99.50%&lt;/td&gt;
&lt;td&gt;~43 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;98.02%&lt;/td&gt;
&lt;td&gt;~174 h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;The corollary.&lt;/strong&gt; Microservices only improve availability if calls are asynchronous or optional. A synchronous fan-out of 20 services is strictly &lt;em&gt;less&lt;/em&gt; available than the monolith it replaced. If reporting being down can 500 your checkout, you didn't isolate failure — you multiplied it.&lt;br&gt;

&lt;/div&gt;


&lt;p&gt;&lt;/p&gt;
  Distributed monolith: the smell test
  &lt;ul&gt;
&lt;li&gt;Do two services have to be released in a fixed order? → &lt;strong&gt;not independent&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Do they share a database schema? → &lt;strong&gt;not independent&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Does one service's deploy require the other's integration suite? → &lt;strong&gt;not independent&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Does a single user story routinely touch 3+ repos? → &lt;strong&gt;boundaries follow the org chart, not the domain&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Is there a "shared-models" library every service pins? → &lt;strong&gt;you have a monolith with extra latency and a build step&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any of these = the costs of both architectures, the benefits of neither. Merge them back.&lt;br&gt;
&lt;/p&gt;

&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Version control and updates
&lt;/h2&gt;

&lt;p&gt;An engineering drawing has one revision block. A drawing &lt;em&gt;set&lt;/em&gt; has a rev per sheet plus an index saying which revs go together. &lt;strong&gt;That index is the whole problem with polyrepo microservices.&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;Concern&lt;/th&gt;
&lt;th&gt;Monolith&lt;/th&gt;
&lt;th&gt;Microservices&lt;/th&gt;
&lt;th&gt;Monorepo mitigates?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unit of change&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Caller + callee in one commit; the compiler catches the mismatch&lt;/td&gt;
&lt;td&gt;Two commits, two repos; nothing catches it until prod — unless you run consumer-driven contract tests&lt;/td&gt;
&lt;td&gt;✅ Atomic commit restores the invariant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Breaking a contract&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rename, fix call sites, merge&lt;/td&gt;
&lt;td&gt;Expand → migrate → contract: 3 releases, maybe 3 teams&lt;/td&gt;
&lt;td&gt;✅ Partially — still need runtime bake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;"What was running at 03:14?"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git tag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deploy manifest / cluster state. Git cannot tell you.&lt;/td&gt;
&lt;td&gt;❌ Orthogonal — that's GitOps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bisecting a regression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git bisect&lt;/code&gt;, works&lt;/td&gt;
&lt;td&gt;Bisect across the deploy manifest, by hand&lt;/td&gt;
&lt;td&gt;✅ Meaningfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update blast radius&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full-fleet restart for a one-line fix&lt;/td&gt;
&lt;td&gt;One deployment — &lt;strong&gt;the real win&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base-image bump&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 Dockerfile, 1 PR&lt;/td&gt;
&lt;td&gt;N Dockerfiles, N repos, N reviews, N releases&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;The killer argument&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency confusion risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One lockfile, one registry config&lt;/td&gt;
&lt;td&gt;N lockfiles, N registry configs, one misconfigured&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;DORA's actual finding:&lt;/strong&gt; deployment frequency correlates with loosely coupled architecture and independent testability — not with the number of repos or containers. A well-modularized monolith on trunk-based development beats a badly-split microservice estate on every DORA metric. The architecture is a means; the coupling is the thing.&lt;br&gt;

&lt;/div&gt;


&lt;h2&gt;
  
  
  Scalability
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw99yrlp8jic9wr497hml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw99yrlp8jic9wr497hml.png" alt="Scaling comparison: six monolith replicas each duplicating idle modules, versus twelve replicas of only the hot resizer service" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;
Fig. 3 — Granularity of scaling is the real dividing line, not code size.



&lt;p&gt;The only scaling argument that survives contact is &lt;strong&gt;divergent resource profiles&lt;/strong&gt;. If your resizer is CPU-bound and spiky while auth is memory-flat and steady, you're sizing every replica for the resizer and paying for idle auth 24/7. That's a real bill.&lt;/p&gt;

&lt;p&gt;If every module has the same profile, splitting buys you a network hop and a Grafana dashboard.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Monolith&lt;/th&gt;
&lt;th&gt;Microservices&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Horizontal scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add replicas behind an LB. Stateless monoliths go much further than folklore suggests.&lt;/td&gt;
&lt;td&gt;Per-service HPA/KEDA. Efficient &lt;strong&gt;only&lt;/strong&gt; when profiles diverge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vertical waste&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pod sized for the hungriest module&lt;/td&gt;
&lt;td&gt;Right-size per service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failure isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reporting's leak OOM-kills auth. Shared fate.&lt;/td&gt;
&lt;td&gt;Reporting dies alone — &lt;em&gt;iff&lt;/em&gt; callers have timeouts and breakers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usually the actual bottleneck&lt;/td&gt;
&lt;td&gt;Splitting app code does &lt;strong&gt;not&lt;/strong&gt; split the DB. Real split means outbox and sagas where you had &lt;code&gt;BEGIN TRANSACTION&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Merge contention past ~20–30 engineers — solvable with a merge queue&lt;/td&gt;
&lt;td&gt;Conway's law made explicit: independent cadence per team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
If your monolith is slow because of an unindexed query, microservices will give you the same unindexed query plus a service mesh.&lt;br&gt;

&lt;/div&gt;


&lt;h2&gt;
  
  
  Supply chain and scanning
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7ly2ujzcixiuipdpfd0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7ly2ujzcixiuipdpfd0.png" alt="Pipeline comparison: one repo producing one SBOM and one signed image, versus four services each with their own SBOM and base image, one of which has drifted" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;
Fig. 4 — Microservices shrink each blast radius and multiply the number of blast radii.



&lt;p&gt;This is where the two architectures invert, and where most security programs get it backwards.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Monolith&lt;/th&gt;
&lt;th&gt;Microservices&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One scan, whole call graph. Taint analysis works end to end.&lt;/td&gt;
&lt;td&gt;Per-service scans. &lt;strong&gt;The scanner cannot follow taint across a network hop&lt;/strong&gt; — cross-service injection is structurally invisible. Under-priced regression.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SCA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One lockfile, one fix. But one bad transitive dep taints everything.&lt;/td&gt;
&lt;td&gt;N lockfiles. Smaller each; drift guaranteed; inventory mandatory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Findings noise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Big image → many findings, most in code paths never executed. Kill this with &lt;strong&gt;VEX&lt;/strong&gt;, not with an architecture change.&lt;/td&gt;
&lt;td&gt;Slim images, genuinely fewer findings. Real benefit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fleet CVE response&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Hours.&lt;/strong&gt; Bump, rebuild, roll.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Days to weeks&lt;/strong&gt; — unless monorepo or golden-base auto-rebuild. Ask your worst team, not your best one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime attack surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One identity, one NetworkPolicy. Compromise = full app + full DB creds.&lt;/td&gt;
&lt;td&gt;Per-workload SPIFFE identity, per-pair NetworkPolicy, minimal filesystem. &lt;strong&gt;The genuine security win.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Policy enforcement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One gate&lt;/td&gt;
&lt;td&gt;Enforce &lt;strong&gt;centrally at admission&lt;/strong&gt;, never per-repo. Per-repo policy rots to the level of the least-staffed team.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provenance / SLSA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One attestation, one control mapping&lt;/td&gt;
&lt;td&gt;N attestations. SLSA L3 needs a hardened build platform — N times, or one shared platform.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern that actually works, regardless of which side you land on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;golden base image (hardened UBI/distroless)
  └─&amp;gt; bump triggers rebuild of ALL dependents
        └─&amp;gt; SBOM + in-toto provenance attestation
              └─&amp;gt; cosign sign
                    └─&amp;gt; admission control verifies signature (Kyverno/Gatekeeper)
                          └─&amp;gt; Dependency-Track fleet inventory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;The synthesis.&lt;/strong&gt; Monolith: fast, complete remediation + wide blast radius. Microservices: narrow blast radius + slow, incomplete remediation. &lt;strong&gt;Monorepo + microservices + golden base&lt;/strong&gt; is the only combination that buys both. If you're splitting the runtime and staying polyrepo, you're choosing the slow remediation on purpose — write that down in the ADR.&lt;br&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The gate
&lt;/h2&gt;

&lt;p&gt;Split a service out only if you can say yes to &lt;strong&gt;at least one trigger&lt;/strong&gt; and yes to &lt;strong&gt;all the mitigations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triggers — at least one:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Materially different &lt;strong&gt;resource profile&lt;/strong&gt; (CPU vs memory, spiky vs flat)&lt;/li&gt;
&lt;li&gt;Materially different &lt;strong&gt;failure domain&lt;/strong&gt; (this must survive when that dies)&lt;/li&gt;
&lt;li&gt;Materially different &lt;strong&gt;compliance boundary&lt;/strong&gt; (this touches PII and that doesn't)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;team&lt;/strong&gt; that needs independent cadence and owns the whole domain&lt;/li&gt;
&lt;li&gt;Different &lt;strong&gt;scaling ceiling&lt;/strong&gt; or &lt;strong&gt;language/runtime&lt;/strong&gt;, with a real justification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigations — all of them:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Callers have timeouts, jittered retries, circuit breakers&lt;/li&gt;
&lt;li&gt;The service &lt;strong&gt;owns its data&lt;/strong&gt;; no shared schema&lt;/li&gt;
&lt;li&gt;Contract tests in CI on both sides&lt;/li&gt;
&lt;li&gt;Distributed tracing before the first split, not after&lt;/li&gt;
&lt;li&gt;Golden base image with rebuild-on-bump&lt;/li&gt;
&lt;li&gt;Centralized admission policy and signed images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not on the list, and never a valid trigger: &lt;em&gt;"the codebase is big."&lt;/em&gt; Big is what modules are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start monorepo + modular monolith.&lt;/strong&gt; Enforce module boundaries in CI (import linting, architecture tests) so the seams are real before they're physical. Split one service at a time, strangler-fig style, only against the gate above. Stay monorepo through the split — it's the cheapest insurance you'll ever buy against base-image drift.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Version control&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Monorepo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High — few real counterarguments below ~1000 engineers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Monolith&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Monolith&lt;/strong&gt;, unless calls are async&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Microservices&lt;/strong&gt;, iff profiles diverge&lt;/td&gt;
&lt;td&gt;Conditional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply chain&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Monorepo + microservices + golden base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Which axis does your org get wrong? I'd guess the one nobody wrote an ADR for.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>security</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Enforcing Zero-Trust Egress in Kubernetes with NetworkPolicies</title>
      <dc:creator>JorelFuji</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:09:03 +0000</pubDate>
      <link>https://dev.to/jorelfuji/enforcing-zero-trust-egress-in-kubernetes-with-networkpolicies-3hlc</link>
      <guid>https://dev.to/jorelfuji/enforcing-zero-trust-egress-in-kubernetes-with-networkpolicies-3hlc</guid>
      <description>&lt;p&gt;Most teams invest heavily in locking down &lt;em&gt;inbound&lt;/em&gt; traffic — ingress rules, service meshes, mutual TLS — while leaving outbound traffic largely uncontrolled. That oversight creates a significant attack surface: a compromised container can silently reach out to an adversary-controlled server, exfiltrate sensitive data, or retrieve a second-stage payload without triggering a single alert, because nothing was monitoring traffic in the &lt;em&gt;outbound&lt;/em&gt; direction.&lt;/p&gt;

&lt;p&gt;Zero-trust networking applies the principle of least privilege in both directions. The default answer to "can this pod initiate this connection?" is &lt;strong&gt;no&lt;/strong&gt; — for both ingress and egress. This guide walks through implementing that model for egress using native Kubernetes &lt;code&gt;NetworkPolicy&lt;/code&gt; objects: deny all outbound traffic by default, then explicitly allow only what each workload legitimately requires. No service mesh, no additional tooling — just declarative YAML you can apply to any compliant cluster today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisite: CNI Enforcement
&lt;/h2&gt;

&lt;p&gt;Before applying any &lt;code&gt;NetworkPolicy&lt;/code&gt; manifest, verify that your CNI plugin actually enforces policy. This is the single most common source of confusion when getting started.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NetworkPolicy&lt;/code&gt; is a &lt;strong&gt;Kubernetes API abstraction&lt;/strong&gt;, not an implementation. The API server will accept any well-formed policy object, but the policy has no effect unless the underlying CNI plugin is configured to enforce it. The default CNI on a standard &lt;code&gt;kind&lt;/code&gt; cluster or many stock configurations does &lt;strong&gt;not&lt;/strong&gt; enforce &lt;code&gt;NetworkPolicy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use a policy-enforcing CNI — Calico and Cilium are the most widely deployed options. For a disposable test cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube start &lt;span class="nt"&gt;--cni&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;calico
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the CNI is operational before proceeding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system | &lt;span class="nb"&gt;grep &lt;/span&gt;calico
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you apply the policies in this guide and observe no change in connectivity, a non-enforcing CNI is almost always the root cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a Namespace and Test Workload
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace app
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; app run web &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx &lt;span class="nt"&gt;--labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app=web"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;netshoot&lt;/code&gt; as an ephemeral debug pod to validate connectivity from within the namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; app run netshoot &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nicolaka/netshoot &lt;span class="nt"&gt;--&lt;/span&gt; /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From inside that shell, confirm the cluster is currently operating with no egress restrictions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 5 https://example.com   &lt;span class="c"&gt;# succeeds&lt;/span&gt;
nslookup kubernetes.default     &lt;span class="c"&gt;# succeeds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, any pod can reach any destination. The following steps will close that off systematically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Default-Deny All Egress
&lt;/h2&gt;

&lt;p&gt;Apply a &lt;code&gt;NetworkPolicy&lt;/code&gt; that selects all pods in the namespace (via an empty &lt;code&gt;podSelector&lt;/code&gt;) and specifies &lt;code&gt;Egress&lt;/code&gt; in &lt;code&gt;policyTypes&lt;/code&gt; with &lt;strong&gt;no allow rules&lt;/strong&gt;. This results in a deny-all for outbound traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default-deny-egress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Egress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the manifest and re-run the test pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; default-deny-egress.yaml
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; app run netshoot &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nicolaka/netshoot &lt;span class="nt"&gt;--&lt;/span&gt; /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 5 https://example.com   &lt;span class="c"&gt;# times out&lt;/span&gt;
nslookup kubernetes.default     &lt;span class="c"&gt;# fails&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that DNS resolution has also broken. This is expected and is addressed in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Restore DNS Resolution
&lt;/h2&gt;

&lt;p&gt;The moment you enforce a default-deny egress policy, pods lose the ability to reach &lt;code&gt;kube-dns&lt;/code&gt;, which causes all hostname resolution to fail — including for destinations you intend to allow. You must explicitly permit egress to the cluster DNS service.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kube-dns&lt;/code&gt; pods are identifiable by the label &lt;code&gt;k8s-app: kube-dns&lt;/code&gt;. The following policy opens egress from all pods in the namespace to that target on UDP and TCP port 53:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow-dns&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Egress&lt;/span&gt;
  &lt;span class="na"&gt;egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
          &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;k8s-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kube-dns&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UDP&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;53&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;53&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NetworkPolicy&lt;/code&gt; rules are &lt;strong&gt;additive&lt;/strong&gt; — this policy adds a permitted path on top of the existing default-deny. After applying it, DNS resolution is restored, but arbitrary outbound connections remain blocked. That is the intended state: name resolution functions, but no traffic flows unless explicitly allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Grant Per-Workload Egress Permissions
&lt;/h2&gt;

&lt;p&gt;With the baseline in place, you can now issue narrow, workload-specific allow rules. Suppose a &lt;code&gt;checkout&lt;/code&gt; service requires outbound connectivity to an external payments API over HTTPS, and nothing else. Scope the rule to that workload's label selector and the relevant destination CIDR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow-egress-payments&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Egress&lt;/span&gt;
  &lt;span class="na"&gt;egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ipBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cidr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;203.0.113.0/24&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;443&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this policy in place, only pods labeled &lt;code&gt;app: checkout&lt;/code&gt; can initiate outbound connections, and only to that CIDR on port 443. All other pods and all other destinations remain denied. You have moved from an implicit open-by-default posture to an explicit allow-list — the foundational principle of zero-trust egress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;Several operational realities become apparent once this pattern moves beyond a lab environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hostname-based matching is not supported in vanilla Kubernetes.&lt;/strong&gt; &lt;code&gt;NetworkPolicy&lt;/code&gt; operates exclusively on IPs and CIDRs, not FQDNs. If a dependency resolves to a rotating IP pool — as most SaaS APIs do — an &lt;code&gt;ipBlock&lt;/code&gt; rule becomes fragile and operationally expensive. Cilium's FQDN-based policy (a CRD, not a core &lt;code&gt;NetworkPolicy&lt;/code&gt;) addresses this directly: specify &lt;code&gt;toFQDNs: api.stripe.com&lt;/code&gt; and Cilium tracks the resolved IPs automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pods not selected by any policy are unrestricted.&lt;/strong&gt; Default-deny applies only to pods that a policy's &lt;code&gt;podSelector&lt;/code&gt; actually matches. Regularly audit for workloads that have no applicable policy and would therefore bypass all egress controls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Policies are namespace-scoped.&lt;/strong&gt; A &lt;code&gt;default-deny-egress&lt;/code&gt; policy in the &lt;code&gt;app&lt;/code&gt; namespace has no effect on pods in &lt;code&gt;payments&lt;/code&gt; or any other namespace. Apply the baseline deny policy to every namespace — ideally via a templated manifest managed in your GitOps repository, so no new namespace can be provisioned without it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Denied traffic is not logged by default.&lt;/strong&gt; Native &lt;code&gt;NetworkPolicy&lt;/code&gt; silently drops blocked connections without emitting any log or event. Debugging failed connectivity relies on inference from timeouts, which is slow and error-prone in production. Calico and Cilium both provide flow-level visibility — enable it before rolling this pattern to any environment where you need operational observability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apply allow rules before deny rules.&lt;/strong&gt; In production environments, apply all workload-specific allow rules first and validate that legitimate traffic continues to flow, then apply the default-deny policy last. Reversing that order will cause an immediate outage while you reconstruct your dependency graph under pressure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Egress control is the half of zero-trust networking that is easiest to defer and most costly to neglect. With three focused manifests — a namespace-wide default-deny, a DNS allow rule, and per-workload egress permissions — you transform outbound traffic from an unmonitored open channel into an auditable, explicit allow-list using nothing beyond standard Kubernetes primitives and a CNI that enforces them.&lt;/p&gt;

&lt;p&gt;The recommended rollout path: start in a non-production namespace, enable flow logging from day one, validate all required paths, then promote the pattern namespace by namespace with your GitOps tooling driving consistency.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The author is a Platform and DevSecOps engineer (CKA, CISSP) who publishes production-grounded guides on Kubernetes security, CI/CD pipelines, and cloud compliance. If your organization is looking for technical content that practitioners trust, feel free to reach out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
