<?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>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>
