<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Fatih Şennik</title>
    <description>The latest articles on Forem by Fatih Şennik (@fatihsennik).</description>
    <link>https://forem.com/fatihsennik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3927101%2F4fa9154e-d9f7-421e-afe6-131a16810090.jpg</url>
      <title>Forem: Fatih Şennik</title>
      <link>https://forem.com/fatihsennik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/fatihsennik"/>
    <language>en</language>
    <item>
      <title>AI-Written Code Is Only Better When a Skilled Programmer Is Holding the Wheel</title>
      <dc:creator>Fatih Şennik</dc:creator>
      <pubDate>Wed, 20 May 2026 19:31:09 +0000</pubDate>
      <link>https://forem.com/fatihsennik/ai-written-code-is-only-better-when-a-skilled-programmer-is-holding-the-wheel-2c4i</link>
      <guid>https://forem.com/fatihsennik/ai-written-code-is-only-better-when-a-skilled-programmer-is-holding-the-wheel-2c4i</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://fatihsennik.com/blog/ai-written-code-is-only-better-when-a-skilled-programmer-is-holding-the-wheel" rel="noopener noreferrer"&gt;fatihsennik.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm going to say something that will annoy people on both sides of the AI coding debate.&lt;/p&gt;

&lt;p&gt;AI-generated code is not good or bad by itself. It's a mirror. It reflects the skill level of the person using it and it does so at about three times the speed and ten times the volume. A skilled developer using AI writes better code faster. An inexperienced developer using AI writes worse code faster. The AI doesn't know the difference. It just outputs.&lt;/p&gt;

&lt;p&gt;I've been using local models in my daily workflow for months now such any in LLM Studio, Ollama and so on. I've watched what it does to code quality when I use it versus when junior developers on client projects use it. The gap is not subtle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fau9muxilzmhsp13ndp9e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fau9muxilzmhsp13ndp9e.jpeg" alt="AI-Written Code Is Only Better When a Skilled Programmer Is Holding the Wheel" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happens When a Junior Uses AI
&lt;/h2&gt;

&lt;p&gt;Ask a junior developer to build a user authentication system without AI. They'll spend time reading the Laravel docs, look at some examples, make some mistakes, learn from them. The output will be imperfect but they'll understand it. They can debug it when it breaks.&lt;/p&gt;

&lt;p&gt;Now give that same developer Cursor or GitHub Copilot. They describe what they want. The AI generates a complete auth system in thirty seconds. It looks professional. The tests pass. It ships.&lt;/p&gt;

&lt;p&gt;Six months later that system has a session fixation vulnerability nobody noticed because nobody fully read the code. There's no CSRF protection on one of the token refresh endpoints because the developer didn't know to ask for it, and the AI didn't volunteer it. The password reset flow has a subtle timing attack vector.&lt;/p&gt;

&lt;p&gt;None of this is the AI's fault, exactly. The AI generated plausible code for the prompt it was given. The prompt didn't mention security requirements because the person writing it didn't know those requirements existed. The AI can't tell you what you forgot to ask for.&lt;/p&gt;

&lt;p&gt;This is the core problem. AI is very good at answering questions. It is completely useless at telling you which questions you should have asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like When a Senior Uses It
&lt;/h2&gt;

&lt;p&gt;Here's what my actual workflow looks like when I use AI for a Laravel feature.&lt;/p&gt;

&lt;p&gt;I already know what the feature needs. I've thought about the data model, the edge cases, the security boundaries. I know which Eloquent methods are injectable if misused. I know which middleware needs to be on which routes. I know what the failure modes are.&lt;/p&gt;

&lt;p&gt;I use AI to generate the boilerplate the migration, the resource class, the form request skeleton. Stuff that's mechanical and correct-by-default. I review every line. I spot when the generated code makes an assumption I don't agree with. I change it. I add the validation rule the AI skipped. I move the authorization check the AI put in the wrong place.&lt;/p&gt;

&lt;p&gt;The AI made me faster at the parts that didn't require my judgment. My judgment handled everything that mattered.&lt;/p&gt;

&lt;p&gt;That's a completely different activity than what the junior developer was doing. It just looks the same from the outside both of us typed a prompt and got code back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Confidence Problem
&lt;/h2&gt;

&lt;p&gt;What makes this genuinely dangerous isn't the bad code. It's that the bad code &lt;em&gt;looks&lt;/em&gt; exactly like good code.&lt;/p&gt;

&lt;p&gt;Handwritten junior code usually has tells. Inconsistent patterns. Awkward variable names. Missing error handling that's obviously missing. An experienced developer reviewing a PR can see the skill level of the author and calibrate their scrutiny accordingly.&lt;/p&gt;

&lt;p&gt;AI-generated code has none of those tells. It's stylistically consistent. It uses the right method names. The error handling is &lt;em&gt;there&lt;/em&gt; it's just handling the wrong errors in some cases. The structure follows conventions. It passes linting. It looks, at a glance, like it was written by someone who knew what they were doing.&lt;/p&gt;

&lt;p&gt;That's the trap. Code review gets less rigorous because the code looks rigorous. Static analysis catches some of it — which is exactly why I started running Semgrep on every MR regardless of who wrote the code. But tooling isn't a full substitute for the reviewer understanding what they're looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  But AI Will Get Better
&lt;/h2&gt;

&lt;p&gt;Sure. Models improve. Reasoning gets sharper. Context windows get longer.&lt;/p&gt;

&lt;p&gt;But "better at generating code" and "better at generating secure, maintainable production code for your specific application's threat model" are not the same thing. The second one requires understanding your business logic, your deployment environment, your user base, your compliance requirements, and a hundred other things that live outside the prompt.&lt;/p&gt;

&lt;p&gt;A model that can generate a theoretically correct authentication system still can't tell you that your specific application handles healthcare data and therefore needs specific protections the generic implementation doesn't include. You have to know that. You have to bring it to the prompt. And knowing what to bring requires exactly the kind of hard-won experience that AI is supposedly making unnecessary.&lt;/p&gt;

&lt;p&gt;The people confidently saying "AI will replace senior developers in two years" are, in my observation, mostly people who haven't spent much time debugging AI-generated code in production. That experience has a way of clarifying your views.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tell Junior Developers
&lt;/h2&gt;

&lt;p&gt;Use it. It's a genuinely useful tool and pretending otherwise is just nostalgia dressed up as professionalism.&lt;/p&gt;

&lt;p&gt;But use it like a calculator, not like an oracle. A calculator gives you the right answer if you set up the equation correctly. Set it up wrong and you get a confident, precise, completely wrong answer. The calculator doesn't know your equation was wrong.&lt;/p&gt;

&lt;p&gt;Understand what the AI generates before you ship it. Not just "does it work" understand &lt;em&gt;why&lt;/em&gt; it works and what happens when the inputs are different from what the AI assumed. If you can't explain a piece of generated code to a teammate, you're not done yet.&lt;/p&gt;

&lt;p&gt;And build the fundamentals anyway. The developers who get the most out of AI tools are the ones who would be competent without them. The shortcuts are only useful if you know what you're shortcutting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI coding tools are productivity multipliers. The problem with multipliers is that they scale whatever you already have skill and judgment as much as speed and volume. Give a skilled developer a 3x multiplier and you get excellent code, faster. Give an inexperienced developer the same multiplier and you get more code, faster, with the same proportion of problems as before except the problems are now harder to spot because the code looks polished.&lt;/p&gt;

&lt;p&gt;The technology is not the variable here. The developer is.&lt;/p&gt;

&lt;p&gt;I'll keep using AI in my workflow. I'll keep running Semgrep on everything it touches. I'll keep reviewing every line before it goes to production. And I'll keep being slightly suspicious of any team that's shipping faster than before but hasn't gotten noticeably better at knowing what to ship.&lt;/p&gt;

&lt;p&gt;Speed without judgment isn't progress. It's just more surface area for things to go wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How Misconfigured Docker Ports Bypass Every Firewall You Set Up - Stealthy vulnerability</title>
      <dc:creator>Fatih Şennik</dc:creator>
      <pubDate>Sun, 17 May 2026 16:36:50 +0000</pubDate>
      <link>https://forem.com/fatihsennik/how-misconfigured-docker-ports-bypass-every-firewall-you-set-up-stealthy-vulnerability-hfa</link>
      <guid>https://forem.com/fatihsennik/how-misconfigured-docker-ports-bypass-every-firewall-you-set-up-stealthy-vulnerability-hfa</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://fatihsennik.com/blog/how-misconfigured-docker-ports-bypass-every-firewall-you-set-up-stealthy-vulnerability" rel="noopener noreferrer"&gt;fatihsennik.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You were running your own servers. You thought you understood firewalls such as UFW rules, iptables, Cloudflare in front of everything. You'd done the reading. You knew what you were doing.&lt;/p&gt;

&lt;p&gt;Then you set up Docker on a new server and watched it silently undo all of it.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical. I found this the hard way, on a friend's server, after something was already listening on a port that had no business being open.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Started
&lt;/h2&gt;

&lt;p&gt;The server was a fresh Ubuntu VPS. My friend had a Laravel application, a MySQL instance, a Redis cache, and a private admin panel they definitely did not want public. Standard setup. He had configured UFW before installing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw default deny incoming
ufw allow ssh
ufw allow 80
ufw allow 443
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Only ports 80, 443, and SSH open. He checked with &lt;code&gt;ufw status&lt;/code&gt; and it looked exactly right. He ran &lt;code&gt;nmap&lt;/code&gt; from my local machine against the server IP and the output confirmed it, three ports, nothing else.&lt;/p&gt;

&lt;p&gt;Then He installed Docker, deployed the stack with Docker Compose, and moved on.&lt;/p&gt;

&lt;p&gt;Three weeks later, a routine audit on their side flagged something in network logs. Traffic was hitting their server on port 6379. From an IP address in Romania. ⚠️&lt;/p&gt;

&lt;p&gt;Redis. The cache that's supposed to be internal-only. Completely exposed to the internet. 😱&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfrykdzuyxiqn6unpkjt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfrykdzuyxiqn6unpkjt.jpeg" alt="Misconfigured Docker Ports Bypass Every Firewall" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Docker Does to Your Firewall
&lt;/h2&gt;

&lt;p&gt;This is the thing that trips up a lot of people who come from a traditional server background. Docker doesn't treat nicely with UFW. It doesn't go through UFW. It goes around it.&lt;/p&gt;

&lt;p&gt;When you map a port in Docker such as ports: 6379:6379 in your Compose file. Docker modifies iptables directly. It inserts its own rules into the DOCKER chain, which gets evaluated before the INPUT chain where UFW rules live. Your UFW deny incoming rule never gets a chance to fire.&lt;/p&gt;

&lt;p&gt;The result: UFW says the port is blocked. &lt;code&gt;ufw status&lt;/code&gt; says the port is blocked. But the port is actually open and accepting connections from anywhere.&lt;/p&gt;

&lt;p&gt;You can verify this by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# UFW thinks everything is fine&lt;/span&gt;
ufw status verbose

&lt;span class="c"&gt;# But iptables tells the real story&lt;/span&gt;
iptables &lt;span class="nt"&gt;-L&lt;/span&gt; DOCKER &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see entries like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ACCEPT  tcp  --  0.0.0.0/0  172.17.0.2  tcp dpt:6379&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's Docker accepting connections from &lt;em&gt;anywhere&lt;/em&gt; (&lt;code&gt;0.0.0.0/0&lt;/code&gt;) and forwarding them to the Redis container. UFW knows nothing about it.&lt;/p&gt;

&lt;p&gt;This behavior is documented. Docker's official docs mention it. But it's buried, and most tutorials don't bring it up at all. So you follow a guide, set up UFW, feel secure, and then publish Redis or MySQL or your admin panel to the entire internet without realizing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an Attacker Finds
&lt;/h2&gt;

&lt;p&gt;Redis with no authentication which was the default for a long time, and still is if you don't configure it is one of the most exploited services on the internet. &lt;a href="https://www.shodan.io/" rel="noopener noreferrer"&gt;Shodan&lt;/a&gt; lists hundreds of thousands of exposed Redis instances at any given moment.&lt;/p&gt;

&lt;p&gt;What can someone do with access to an unauthenticated Redis? More than you'd expect.&lt;/p&gt;

&lt;p&gt;If it's a Laravel app, the session data is probably in Redis. Someone with read access to Redis can steal sessions and log in as any authenticated user by including your admin accounts without knowing a single password.&lt;/p&gt;

&lt;p&gt;Worse: if the Redis instance has write access and the server is running as root or a privileged user, there are known techniques to write SSH keys into the authorized_keys file via Redis. At that point it's full server compromise, not just a data breach.&lt;/p&gt;

&lt;p&gt;In my friend's case, the logs showed connection attempts and some key enumeration. We couldn't confirm with certainty whether sessions were read. We had to assume they were, force-logout every active user, rotate all credentials, and notify the affected accounts. Not a fun conversation to have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Bind to localhost, Not the World
&lt;/h2&gt;

&lt;p&gt;The cleanest solution is to stop publishing internal services to &lt;code&gt;0.0.0.0&lt;/code&gt; in the first place.&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;docker-compose.yml&lt;/code&gt;, any service that doesn't need to be publicly accessible should bind only to localhost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:6379:6379"&lt;/span&gt; &lt;span class="c1"&gt;# localhost only — not 0.0.0.0&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass your_strong_password_here&lt;/span&gt;

&lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:3306:3306"&lt;/span&gt; &lt;span class="c1"&gt;# same principle&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_root_password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;127.0.0.1:&lt;/code&gt; prefix tells Docker to bind the host-side port only to the loopback interface. Docker still creates its iptables rules, but they only accept connections from localhost not from external IPs.&lt;/p&gt;

&lt;p&gt;For services that don't need to be exposed on the host at all (Redis accessed only by other containers in the same Compose stack), just remove the &lt;code&gt;ports&lt;/code&gt; mapping entirely and use Docker's internal networking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-app&lt;/span&gt;
&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
&lt;span class="c1"&gt;# No ports needed here for internal communication&lt;/span&gt;

&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
&lt;span class="c1"&gt;# No ports: mapping — only accessible within Docker network&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass your_strong_password_here&lt;/span&gt;

&lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
&lt;span class="c1"&gt;# No ports: mapping&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers in the same Compose project can reach each other by service name (&lt;code&gt;redis:6379&lt;/code&gt;, &lt;code&gt;mysql:3306&lt;/code&gt;) without any host port mapping. There's nothing to expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing Docker's iptables Behavior at the Engine Level
&lt;/h2&gt;

&lt;p&gt;If you want UFW to actually control Docker traffic, there's a configuration option in the Docker daemon that stops it from touching iptables directly:&lt;/p&gt;

&lt;p&gt;Create or edit &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"iptables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart Docker:&lt;/p&gt;

&lt;p&gt;systemctl restart docker&lt;/p&gt;

&lt;p&gt;The problem with this approach: now Docker's internal container-to-container networking can break too, because Docker relies on its own iptables rules for routing between containers and for NAT. You'd need to manage that routing manually with your own iptables or nftables rules. For most people this creates more problems than it solves. So please ingore it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Audit You Should Run Right Now
&lt;/h2&gt;

&lt;p&gt;If you have Docker running on any server, check what's actually exposed at the iptables level, not just what UFW thinks is exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See all open ports on the host (Docker and non-Docker)&lt;/span&gt;
ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;

&lt;span class="c"&gt;# See specifically what Docker has added to iptables&lt;/span&gt;
iptables &lt;span class="nt"&gt;-L&lt;/span&gt; DOCKER &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--line-numbers&lt;/span&gt;

&lt;span class="c"&gt;# Or just scan yourself from outside&lt;/span&gt;
nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 1-65535 your.server.ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the most honest. Run it from a machine outside your network and see what actually responds. If you see ports you didn't intentionally open, you have the same problem I found on that my friend's server.&lt;/p&gt;

&lt;p&gt;Also check your Compose files for any service with a &lt;code&gt;ports:&lt;/code&gt; mapping that uses only a port number or &lt;code&gt;0.0.0.0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="c"&gt;# Find potentially dangerous port mappings in your Compose files&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'"[0-9]*:[0-9]*"'&lt;/span&gt; /path/to/your/compose/files
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"'[0-9]*:[0-9]*'"&lt;/span&gt; /path/to/your/compose/files&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One More Thing That Makes This Worse
&lt;/h2&gt;

&lt;p&gt;The default Docker network (&lt;code&gt;172.17.0.0/16&lt;/code&gt;) is also reachable from other containers, even across different Compose projects on the same host. If you're running multiple client applications on one server, a compromised container in one project can potentially reach services in another project if they're all on the default bridge network.&lt;/p&gt;

&lt;p&gt;Use named networks and keep projects isolated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't stop everything, but it reduces the blast radius significantly if something does get compromised.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Changed After This
&lt;/h2&gt;

&lt;p&gt;On every server where I deploy Docker now, my checklist before going live:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;nmap -p 1-65535&lt;/code&gt; against the public IP from an external machine&lt;/li&gt;
&lt;li&gt;Verify every &lt;code&gt;ports:&lt;/code&gt; mapping in every Compose file has &lt;code&gt;127.0.0.1:&lt;/code&gt; prefix or is removed entirely&lt;/li&gt;
&lt;li&gt;Redis and MySQL never have host port mappings unless there's an explicit reason&lt;/li&gt;
&lt;li&gt;All services that only talk to each other get put on a named internal network with no host exposure&lt;/li&gt;
&lt;li&gt;Redis always runs with &lt;code&gt;--requirepass&lt;/code&gt; even on internal networks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Most of this takes five minutes to get right. The problem is that most Docker tutorials skip it entirely, they show you &lt;code&gt;ports: "6379:6379"&lt;/code&gt; and move on, and nobody mentions that you just published your cache server to the planet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run the nmap scan. Not later, now. It takes thirty seconds and you might not like what you find.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>docker</category>
      <category>firewall</category>
      <category>networking</category>
    </item>
    <item>
      <title>How to Install WireGuard VPN on Ubuntu and Configure It as a Server — Using Port 443 to Bypass ISP Throttling</title>
      <dc:creator>Fatih Şennik</dc:creator>
      <pubDate>Tue, 12 May 2026 12:52:39 +0000</pubDate>
      <link>https://forem.com/fatihsennik/how-to-install-wireguard-vpn-on-ubuntu-and-configure-it-as-a-server-using-port-443-to-bypass-isp-17jg</link>
      <guid>https://forem.com/fatihsennik/how-to-install-wireguard-vpn-on-ubuntu-and-configure-it-as-a-server-using-port-443-to-bypass-isp-17jg</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://fatihsennik.com/blog/how-to-install-wireguard-vpn-on-ubuntu-and-configure-it-as-a-server-using-port-443-to-bypass-isp-throttling" rel="noopener noreferrer"&gt;fatihsennik.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is &lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;WireGuard&lt;/a&gt; VPN ?
&lt;/h2&gt;

&lt;p&gt;WireGuard is a secure network tunnel operating at Layer 3, built directly into the Linux kernel as a virtual network interface. Its goal is straightforward: replace both IPsec and TLS-based solutions such as OpenVPN — and do it better. More secure, more performant, and significantly easier to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cleaner mental model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At its core, WireGuard is built around a simple principle: a tunnel is an association between a peer's public key and a tunnel source IP. No certificates, no certificate authorities, no complex configuration hierarchies. If you've used OpenSSH, the model will feel familiar — short, static Curve25519 keys handle mutual authentication, and that's it. No central server required. it's peer-to-peer by design, though you can use a hub-and-spoke topology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast handshakes, strong privacy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Session creation is handled transparently using a single round-trip key exchange based on the NoiseIK protocol — fast and invisible to the end user. The protocol provides strong perfect forward secrecy and a high degree of identity hiding, so even if keys are later compromised, past sessions stay protected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance-first design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Data in transit is encrypted using ChaCha20Poly1305, a modern authenticated-encryption cipher that's fast even on hardware without dedicated AES acceleration. Packets are encapsulated in UDP, and the kernel-level implementation takes full advantage of Linux's queue and parallelism primitives. Crucially, WireGuard is designed to allocate no resources in response to incoming packets — a key factor in its resilience under load. So, it runs over UDP, which is faster than TCP-based VPNs but can be easliy blocked or throttled by some networks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better DoS protection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WireGuard improves on the IP-binding cookie mechanisms used in IKEv2 and DTLS by adding encryption and authentication to the cookie itself — making denial-of-service mitigation significantly more robust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small enough to audit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Perhaps the most striking aspect of WireGuard is its size: the entire Linux implementation fits in under 4,000 lines of code. Compare that to OpenVPN's ~100,000+ lines and the security implications become obvious. A smaller codebase means a smaller attack surface, and one that's actually feasible to audit and verify.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18rdfy2cozupwgubex0w.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18rdfy2cozupwgubex0w.jpeg" alt="WireGuard VPN" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Install WireGuard VPN on Ubuntu and Configure it as a server.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1) Update packages and install WireGuard.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wireguard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2) Generate server private and public key pair.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg genkey | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/wireguard/private.key

&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;&lt;span class="nv"&gt;go&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; /etc/wireguard/private.key

&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /etc/wireguard/private.key | wg pubkey | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/wireguard/public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3) View the generated private &amp;amp; public keys — you will need them in the WireGuard config.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /etc/wireguard/private.key

&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /etc/wireguard/public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4) Find your actual network interface name — it will be the one associated with your server's public IP such as ens160 and eth0.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5) Create your WireGuard server configuration file. You can name the virtual network interface anything you like, such as wg0.conf or custom-name.conf. Let's name it as name0.conf.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/wireguard/name0.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Copy /etc/wireguard/private.key to here&lt;/span&gt;
&lt;span class="py"&gt;ListenPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;443&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.50.1/24&lt;/span&gt;

&lt;span class="c"&gt;## Enable IP forwarding (for routing)
## Please check your network interface name such as ens160.
## Please check that -i name0 same as your config file name.
&lt;/span&gt;
&lt;span class="py"&gt;PostUp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -A FORWARD -i name0 -j ACCEPT; iptables -t nat -A POSTROUTING -o ens160 -j MASQUERADE&lt;/span&gt;
&lt;span class="py"&gt;PostDown&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -D FORWARD -i name0 -j ACCEPT; iptables -t nat -D POSTROUTING -o ens160 -j MASQUERADE&lt;/span&gt;

&lt;span class="c"&gt;## Client 1
&lt;/span&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Paste your mac client's public key here.&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.50.2/32&lt;/span&gt;

&lt;span class="c"&gt;## Client xN
&lt;/span&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Paste your widows or any client's public key here.&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.50.3/32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6) Enable IP forwarding in the kernel so that server acts as a router, passing traffic between your VPN clients and the outside network.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"net.ipv4.ip_forward=1"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/sysctl.conf 

&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-p&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7) Start WireGuard and enable on boot and verify the interface is up.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;wg-quick@name0 

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start wg-quick@name0

&lt;span class="nb"&gt;sudo &lt;/span&gt;wg show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8) If UFW is enabled, open the WireGuard port in the firewall.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw allow 443/udp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9) Every time you update the WireGuard configuration file, remember to restart the WireGuard service for the changes to take effect.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart wg-quick@name0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Install WireGuard VPN on Mac and Configure it as a client.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install the official WireGuard app from the Mac App Store: &lt;a href="https://apps.apple.com/us/app/wireguard/id1451685025?mt=12" rel="noopener noreferrer"&gt;Download&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;Click&lt;/span&gt; &lt;span class="err"&gt;'Add&lt;/span&gt; &lt;span class="err"&gt;Empty&lt;/span&gt; &lt;span class="err"&gt;Tunnel'&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;app&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;paste&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;client&lt;/span&gt; &lt;span class="err"&gt;config&lt;/span&gt; &lt;span class="err"&gt;below.&lt;/span&gt; &lt;span class="err"&gt;Make&lt;/span&gt; &lt;span class="err"&gt;sure&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;client&lt;/span&gt; &lt;span class="err"&gt;IP&lt;/span&gt; &lt;span class="err"&gt;address&lt;/span&gt; &lt;span class="err"&gt;(e.g.&lt;/span&gt; &lt;span class="err"&gt;192.168.50.2/24)&lt;/span&gt; &lt;span class="err"&gt;matches&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;AllowedIPs&lt;/span&gt; &lt;span class="err"&gt;value&lt;/span&gt; &lt;span class="err"&gt;set&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;this&lt;/span&gt; &lt;span class="err"&gt;peer&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;your&lt;/span&gt; &lt;span class="err"&gt;server's&lt;/span&gt; &lt;span class="err"&gt;/etc/wireguard/name0.conf.&lt;/span&gt;

&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;This is auto generated. Do not share it with anyone.&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.50.2/24&lt;/span&gt;
&lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8.8.8.8, 1.1.1.1&lt;/span&gt;

&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Copy vpn server /etc/wireguard/public.key to here&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0, ::/0&lt;/span&gt;
&lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;VPN_SERVER_IP:443&lt;/span&gt;
&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the connection is established, the &lt;strong&gt;AllowedIPs = 0.0.0.0/0, ::/0&lt;/strong&gt;  setting will route all IPv4 and IPv6 traffic through your VPN server, changing your Mac's public IP to your server's IP.&lt;/p&gt;

&lt;p&gt;If you only want a private network without changing your public IP, set &lt;strong&gt;AllowedIPs&lt;/strong&gt; to your VPN subnet (e.g. &lt;strong&gt;192.168.50.0/24&lt;/strong&gt;) and restart the WireGuard client.&lt;/p&gt;

&lt;p&gt;Make sure you have added your Mac client's public key to your VPN server config at &lt;strong&gt;/etc/wireguard/name0.conf&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;## Client 1
&lt;/span&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Paste your mac client's public key here.&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.50.2/32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the VPN server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart wg-quick@name0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That's it — enjoy your self-hosted, free, and open-source VPN!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>security</category>
      <category>ubuntu</category>
      <category>wireguard</category>
    </item>
    <item>
      <title>How to Auto-Unlock LUKS2 Encrypted Disks at Boot with Clevis and Tang</title>
      <dc:creator>Fatih Şennik</dc:creator>
      <pubDate>Tue, 12 May 2026 12:21:45 +0000</pubDate>
      <link>https://forem.com/fatihsennik/how-to-auto-unlock-luks2-encrypted-disks-at-boot-with-clevis-and-tang-3b31</link>
      <guid>https://forem.com/fatihsennik/how-to-auto-unlock-luks2-encrypted-disks-at-boot-with-clevis-and-tang-3b31</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://fatihsennik.com/blog/how-to-auto-unlock-luks2-encrypted-disks-at-boot-with-clevis-and-tang" rel="noopener noreferrer"&gt;fatihsennik.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Full disk encryption is great — until you reboot a headless server at 3am and realize you need to type a passphrase with no keyboard attached. Every reboot now requires manual passphrase entry. That's... not great when your server is a headless VM sitting in a datacenter rack in another city.&lt;/p&gt;

&lt;p&gt;Enter Clevis and Tang. Together they let your server auto-unlock its LUKS2 volume at boot — but only when it can reach your Tang server on the network. No Tang server reachable? No unlock. It's elegant and your data is safe even if someone walks off with the physical server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Clevis talks to Tang (and why it's clever)
&lt;/h2&gt;

&lt;p&gt;During boot, Clevis contacts Tang and initiates a JOSE/JWK key exchange. What makes this secure is what doesn't happen — your LUKS passphrase is never transmitted, Tang gains zero knowledge of the disk key, and the derived secret exists only in RAM long enough to unlock the volume. The wire traffic reveals nothing useful to an attacker and the Tang server never sees your LUKS passphrase; it just participates in the math.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3ox1yeqx8hrwf5tebpx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3ox1yeqx8hrwf5tebpx.jpg" alt="Clevis and Tang" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's say that Tang server is unreachable, then Clevis gets no response, the key exchange fails, and the disk stays locked. You can still fall back to a manual passphrase, which is a separate LUKS keyslot you keep as a backup.&lt;/p&gt;

&lt;p&gt;Keep a backup passphrase keyslot! Clevis adds its own keyslot but doesn't touch your existing passphrase. Keep it. Store it in your password manager. If Tang ever goes down permanently you'll need it. LUKS supports multiple keyslots for exactly this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Tang on your secure key server which is located in different location.
&lt;/h2&gt;

&lt;p&gt;Tang runs as a simple systemd socket service. It's lightweight — It functions solely as a key exchange endpoint, requiring no database and no configuration files other than the key material it generates automatically. it automatically generates its key material in /var/db/tang/. That's it. No config needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update

apt &lt;span class="nb"&gt;install &lt;/span&gt;tang jose

&lt;span class="c"&gt;# Enable and start&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; tangd.socket

&lt;span class="c"&gt;# Change its port to any. Example: 9102&lt;/span&gt;
nano /lib/systemd/system/tangd.socket

systemctl daemon-reload

&lt;span class="c"&gt;# Verify it's running&lt;/span&gt;
curl 127.0.0.1:9102/adv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the server security hardened via a separated network segment and a secure random port. The /adv endpoint returns Tang's public key advertisement as JSON. If you can curl it, Clevis can reach it during boot. If you can't, neither can Clevis — fix your firewall first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Clevis on your luks encrypted server.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update

apt &lt;span class="nb"&gt;install &lt;/span&gt;clevis clevis-luks clevis-initramfs clevis-systemd

&lt;span class="c"&gt;# Find your LUKS2 device&lt;/span&gt;
&lt;span class="c"&gt;# Replace /dev/sda3 with your actual LUKS partition&lt;/span&gt;
cryptsetup luksDump /dev/sda3

&lt;span class="c"&gt;# Check your manual passphrase before reboot!&lt;/span&gt;
cryptsetup &lt;span class="nt"&gt;--test-passphrase&lt;/span&gt; &lt;span class="nt"&gt;--key-slot&lt;/span&gt; 0 open /dev/sda3

&lt;span class="c"&gt;# Bind your LUKS2 device to Tang key server&lt;/span&gt;
&lt;span class="c"&gt;# it will ask for your existing LUKS passphrase&lt;/span&gt;
&lt;span class="c"&gt;# Then will fetch Tang's public key and add a new keyslot for LUKS partition.&lt;/span&gt;
clevis luks &lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/sda3 tang &lt;span class="s1"&gt;'{"url":"http://your-tang-server-ip:9102"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run that bind command, Clevis contacts Tang, fetches its public key, generates a random key, encrypts it using Tang's key material, stores the encrypted blob in the LUKS2 token metadata, and registers it as a new keyslot. The actual decryption key never leaves your machine unencrypted.&lt;/p&gt;

&lt;p&gt;Before embedding Clevis into your boot process, check your network interface name — it could be ens192, eth0, or something similar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip a | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^[0-9]+:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚠️ Important Gotcha: To avoid the "network isn't up yet" issue during boot, check how your server receives its IP address.
&lt;/h2&gt;

&lt;p&gt;Clevis needs to reach Tang before the root filesystem mounts — but if your network interface isn't up yet, the whole thing silently fails and drops you to a passphrase prompt.&lt;/p&gt;

&lt;p&gt;Open /etc/netplan/50-cloud-init.yaml and verify whether your server is configured for a static IP or DHCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open initramfs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/initramfs-tools/initramfs.conf

&lt;span class="c"&gt;# and add this to end of the file if your server is configured for static ip&lt;/span&gt;
&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SERVER_IP::SERVER_GATEWAY_IP:SERVER_SUBNET::NETWORK_INTERFACE_NAME:none

&lt;span class="c"&gt;# if your server is configured for dhcp then&lt;/span&gt;
&lt;span class="nv"&gt;BOOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network
&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NETWORK_INTERFACE_NAME &lt;span class="o"&gt;(&lt;/span&gt;ens192&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dhcp

&lt;span class="c"&gt;# Update your boot process&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-initramfs &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; all

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wait before rebooting. Test !&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Try to unlock manually using Clevis &lt;/span&gt;
clevis luks unlock &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/sda3 
&lt;span class="c"&gt;# Check if Tang server is reachable&lt;/span&gt;
curl http://your-tang-server:9102/adv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all there is to it. Your disk will now decrypt and unlock automatically during boot, no manual intervention required.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>networking</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
