<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Opensource on alikhil</title>
    <link>https://alikhil.dev/categories/opensource/</link>
    <description>Recent content in Opensource on alikhil</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>Alik Khilazhev</managingEditor>
    <lastBuildDate>Sat, 01 Aug 2026 23:51:12 +0300</lastBuildDate><atom:link href="https://alikhil.dev/categories/opensource/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to control RGB on RAM sticks, motherboards, coolers, graphics cards programmatically with OpenRGB.</title>
      <link>https://alikhil.dev/posts/control-rgb-lights-with-openrgb/</link>
      <pubDate>Sat, 01 Aug 2026 23:51:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/control-rgb-lights-with-openrgb/</guid>
      <description>Install OpenRGB on Linux and use a systemd service to turn off RGB lighting on every boot.</description>
      <content:encoded><![CDATA[<p>When I was building my gaming PC a few years ago, I bought a second-hand pair of RAM sticks with RGB lights. It was entertaining to see them glowing and changing colors. I did not bother myself to control them.</p>
<p>Some time later, I got sick of gaming and made a decision to rebuild it into a self-hosting server. The PC moved from my table to an under-TV shelf in the living room. And here, glowing lights became distracting during watching TV or when we have guests sleeping in the living room. Therefore, I started searching for ways to control/disable RGB lights.</p>
<p>After some search, I encountered <a href="https://openrgb.org/">OpenRGB</a>. It&rsquo;s open-source RGB control software that is independent from manufacturer software. It supports color syncing across several devices and has a community-managed <a href="https://gitlab.com/OpenRGBDevelopers/OpenRGBEffectsPlugin">effects library</a>. It has a long list of more than 2,500 <a href="https://openrgb.org/devices.html">supported devices</a> and, hopefully for me, my Corsair Vengeance was there!</p>
<p>What a great piece of software, right?</p>
<p>It&rsquo;s a shame that I needed this software only to <strong>disable RGB</strong>.</p>
<h2 id="installing-openrgb">Installing OpenRGB</h2>
<p>OpenRGB runs on Linux, macOS, and Windows. You can download the package for your operating system from the <a href="https://openrgb.org/releases.html">releases page</a>.</p>
<p>On my Debian-based server, I downloaded the <code>.deb</code> package and installed it with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install ./OpenRGB-&lt;version&gt;-x86_64.deb
</span></span></code></pre></div><p>The package includes the required udev rules. If you use an AppImage, Flatpak, or a build from source, install the <a href="https://openrgb.org/udev.html">udev rules</a> too. Without them, OpenRGB may not be able to access the RGB hardware without running as root.</p>
<p>Before creating the service, check that OpenRGB can detect your devices:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo openrgb --list-devices
</span></span></code></pre></div><p>Make a note of the device numbers you want to control. Mine were <code>0</code> and <code>1</code>.</p>
<h2 id="configuring">Configuring</h2>
<p>I needed the RGB to turn off every time the system starts. So I ended up with the following systemd service.</p>
<p>First, test the command manually. <code>000000</code> is black, which turns the LEDs off. The <code>direct</code> mode is supported by my devices; use the output of <code>--list-devices</code> to check which modes yours support.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo openrgb -d <span class="m">0</span> -c <span class="m">000000</span> -m direct -d <span class="m">1</span> -c <span class="m">000000</span> -m direct
</span></span></code></pre></div><p>When that worked, I created the service below.</p>
<p><code>/etc/systemd/system/openrgb.service</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[Unit]
</span></span><span class="line"><span class="cl">Description=OpenRGB control
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[Service]
</span></span><span class="line"><span class="cl">Type=oneshot
</span></span><span class="line"><span class="cl">ExecStart=/usr/bin/openrgb -d 0 -c 000000 -m direct -d 1 -c 000000 -m direct
</span></span><span class="line"><span class="cl">User=root
</span></span><span class="line"><span class="cl">Group=i2c
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[Install]
</span></span><span class="line"><span class="cl">WantedBy=multi-user.target
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> openrgb
</span></span><span class="line"><span class="cl">systemctl start openrgb
</span></span></code></pre></div><p>Now the RAM lights turn off automatically on every boot. No proprietary RGB application, no running desktop session, and no bright lights in the living room.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>If you have devices with RGB lights, check whether they are <a href="https://openrgb.org/devices.html">supported by OpenRGB</a>. You might find a more creative use for it than I did and build some nice RGB animations.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Forward SMS to Telegram with Android</title>
      <link>https://alikhil.dev/posts/forward-sms-to-telegram-with-android/</link>
      <pubDate>Sat, 11 Jul 2026 12:00:00 +0200</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/forward-sms-to-telegram-with-android/</guid>
      <description>How I forward SMS to Telegram using a dedicated Android phone.</description>
      <content:encoded><![CDATA[<p>A few years ago, I wrote a <a href="/posts/forwarding-sms-to-telegram/">post</a> on how I automated SMS forwarding using second-hand 3G modems and a Gammu SMS daemon running on a Linux server.</p>
<p>This setup worked well for a while, but then became outdated due to the instability of USB dongles and the planned deprecation of 2G and 3G networks in Europe.</p>
<p>One day, I got tired of replugging USB modems and manually restarting containers, so I decided to look for a new solution.</p>
<p>First, I thought I would build my own device using existing 4G modules for an ESP32 or a Raspberry Pi, but I quickly discovered it would cost me a fortune to support two or more SIM cards.</p>
<p>Then I looked for off-the-shelf solutions. I found some cool 8/16/32+ SIM support boxes from Chinese vendors. But again, they were pricey, and I didn’t need so many SIM cards.</p>
<p>After that letdown, I returned to the old, forgotten idea of using a dedicated Android device for my task.</p>
<h2 id="device">Device</h2>
<p>I didn’t have a spare Android device at home, so I looked for a second-hand working one with two SIM slots on Wallapop.</p>
<p>I ended up buying a Moto E14 for €25.</p>
<p>I tried to root it as soon as I received it, mostly out of curiosity and to gain better control over my device. But I was unable to find a working solution for that.</p>
<p>So I gave up and tried to configure everything without root access.</p>
<p>Spoiler: I succeeded. No root was needed.</p>
<h2 id="software">Software</h2>
<p>I had two SIM cards, and the current setup forwarded SMS messages to different Telegram channels.</p>
<p>I looked for an app that would allow me to do so. I considered the <a href="https://github.com/telegram-sms/telegram-sms">telegram-sms</a> app, but after a quick look, it <a href="https://config.telegram-sms.com/">seemed not to support</a> splitting SMS messages into separate chats.</p>
<p>In the end, I chose to go with an <a href="https://github.com/bogkonstantin/android_income_sms_gateway_webhook">android_income_sms_gateway_webhook</a>. It does not support Telegram specifically, but it lets you configure a webhook that can call any service, including Telegram.</p>
<p>However, to split SMS messages from different SIM cards into separate channels, I needed to implement a webhook handler, which took Codex about five minutes.</p>
<p>Here is a link to the GitHub repository: <a href="https://github.com/alikhil/sms-forwarder-telegram-webhook">github.com/alikhil/sms-forwarder-telegram-webhook</a></p>
<p>It is a simple, stupid Python web server that parses the JSON and sends two JSON requests to Telegram servers for the right channel.</p>
<p>I deployed the webhook handler in my homelab using Docker Compose, and it became the bridge between the Android app and Telegram:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sms-webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">https://github.com/alikhil/sms-forwarder-telegram-webhook.git#main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DEFAULT_TARGET_CHAT_ID</span><span class="p">:</span><span class="w"> </span>-<span class="m">88005553535</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SIM1_TARGET_CHAT_ID</span><span class="p">:</span><span class="w"> </span>-<span class="m">100500</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SIM2_TARGET_CHAT_ID</span><span class="p">:</span><span class="w"> </span>-<span class="m">100500800</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">.env</span><span class="w"> </span><span class="c"># includes TELEGRAM_BOT_TOKEN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8000:8000&#34;</span><span class="w">
</span></span></span></code></pre></div><p>I configured the app to send SMS messages:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th></th>
					<th><img loading="lazy" src="/posts/forward-sms-to-telegram-with-android/images/sms-config.png" type="" alt="android_income_sms_gateway_webhook app configuration"  /></th>
					<th></th>
					<th></th>
			</tr>
	</thead>
	<tbody>
	</tbody>
</table>
<p>That’s it. The SMS forwarding path was working end-to-end.</p>
<p><img loading="lazy" src="/posts/forward-sms-to-telegram-with-android/images/sms-tg.png" type="" alt="forwarded sms notification in telegram"  /></p>
<h2 id="monitoring">Monitoring</h2>
<p>After the pain of previous solutions, I wanted to be notified if something failed.</p>
<p>If I did not receive an SMS that I was expecting, I would not know that I had missed it. The app might crash, or the battery might run low.</p>
<p>Fortunately, the SMS gateway app supports sending heartbeats.</p>
<p>I set up a Uptime Kuma monitor and configured the app to send heartbeats every 5 minutes. Kuma expects a heartbeat every 5 hours.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th></th>
					<th><img loading="lazy" src="/posts/forward-sms-to-telegram-with-android/images/sms-kuma.png" type="" alt="configuring heartbeats"  /></th>
					<th></th>
					<th></th>
			</tr>
	</thead>
	<tbody>
	</tbody>
</table>
<p>The values were tuned empirically because the app does not strictly follow the schedule. As I understand it, this is a technical limitation of the app running in the background.</p>
<p>But it is okay for me, even if I learn about a failure after twice the normal interval.</p>
<p><img loading="lazy" src="/posts/forward-sms-to-telegram-with-android/images/kuma.png" type="" alt="kuma dashboard"  /></p>
<h2 id="power-management">Power management</h2>
<p>I didn’t want my Android device connected to a power supply all the time because I believed the battery health would suffer as a result.</p>
<p>Also, I didn’t want to connect and disconnect it manually every time. I’m an engineer, after all. It must be automated.</p>
<p>I wanted it to work like this:</p>
<ul>
<li>If the battery level is below 20%, connect the power.</li>
<li>If the battery level is above 90%, disconnect the power.</li>
</ul>
<p>I had a spare smart and controllable Shelly Plug that I could use for such automation.</p>
<p>I could probably use one of the Home Assistant plugins to implement it, but I don’t use Home Assistant and didn’t want to install it just for this small use case. That would be overkill.</p>
<p>The next step was to find Android apps that could send a webhook when the battery reached a certain level.</p>
<p>There were surprisingly few decent apps for this. It seems like a very niche use case.</p>
<p>The only thing I found was <a href="https://github.com/hossain-khan/android-remote-notify">Android Remote Notify</a>. And, of course, I couldn’t configure Shelly automation directly there.</p>
<p>So, Codex handled <a href="https://github.com/alikhil/android-remote-notify-webhook-handler">another simple webhook handler</a> that calls the Shelly Plug API when the battery reaches a certain level.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">battery-webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">https://github.com/alikhil/android-remote-notify-webhook-handler.git#main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SHELLY_PLUG_URL</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.99.88</span><span class="w"> </span><span class="c"># Your shelly plug IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PORT</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">LOW_BATTERY_THRESHOLD</span><span class="p">:</span><span class="w"> </span><span class="m">20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HIGH_BATTERY_THRESHOLD</span><span class="p">:</span><span class="w"> </span><span class="m">90</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><p>Only after I had configured and deployed everything did I realize the app had a major limitation: it did not have a way to trigger an alert when the battery level exceeded 90%. It only supported a lower-bound threshold.</p>
<p>I didn’t want to search for another solution when I was so close to completing the current one. Luckily for me, the app was open source.</p>
<p>I concluded that a simpler approach would be to send battery level periodically, and the webhook handler controlled the plug based on those reports.</p>
<p>I prompted Codex in the repository to develop the new feature.</p>
<p>After 15 minutes of hacking, it produced a solution. I didn’t have a full Android development setup, and I didn’t want to bloat my Mac with it.</p>
<p>So I asked Codex to build the APK inside Docker. Five more minutes and it was done.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th></th>
					<th><img loading="lazy" src="/posts/forward-sms-to-telegram-with-android/images/battery-periodic.png" type="" alt="configuring periodic checks"  /></th>
					<th></th>
					<th></th>
			</tr>
	</thead>
	<tbody>
	</tbody>
</table>
<p>I replaced the app with the custom patched build, and it worked.</p>
<p>I went ahead and created a <a href="https://github.com/hossain-khan/android-remote-notify/pull/451">pull request to upstream</a>, and it was accepted. As a nice bonus, I no longer need to maintain a fork.</p>
<p>To be fair, heartbeats and battery-level webhooks were not coming on a strict schedule.</p>
<p>To monitor the webhook handler, I built a page that reports things like:</p>
<ul>
<li>When the last state was reported</li>
<li>What the battery level was at that time</li>
<li>The last command sent to Shelly.</li>
<li>Etc.</li>
</ul>
<p><img loading="lazy" src="/posts/forward-sms-to-telegram-with-android/images/status.png" type="" alt="Webhook handler status"  /></p>
<h2 id="caveats">Caveats</h2>
<p>During webhook testing, I encountered an issue where both Android apps struggled to send requests to an insecure HTTP endpoint.</p>
<p>There is probably a configuration option somewhere that allows such behavior.</p>
<p>I simply routed the traffic through my custom domain with Let’s Encrypt certificates.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>It was my first time tinkering with Android, and I enjoyed it. I expected it to require more hassle.</p>
<p>Happy hacking!</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Kubernetes In-Place Pod Resize</title>
      <link>https://alikhil.dev/posts/in-place-pod-resize/</link>
      <pubDate>Sun, 28 Dec 2025 21:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/in-place-pod-resize/</guid>
      <description>&lt;p&gt;About six years ago, while operating a large Java-based platform in Kubernetes, I noticed a recurring problem: our services required significantly higher CPU and memory during application startup. Heavy use of Spring Beans and AutoConfiguration forced us to set inflated resource requests and limits just to survive bootstrap, even though those resources were mostly unused afterwards.&lt;/p&gt;
&lt;p&gt;This workaround never felt right. As an engineer, I wanted a solution that reflected the actual lifecycle of an application rather than its worst moment.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>About six years ago, while operating a large Java-based platform in Kubernetes, I noticed a recurring problem: our services required significantly higher CPU and memory during application startup. Heavy use of Spring Beans and AutoConfiguration forced us to set inflated resource requests and limits just to survive bootstrap, even though those resources were mostly unused afterwards.</p>
<p>This workaround never felt right. As an engineer, I wanted a solution that reflected the actual lifecycle of an application rather than its worst moment.</p>
<p>I opened an <a href="https://github.com/kubernetes/kubernetes/issues/83111">issue</a>  in the Kubernetes repository describing the problem and proposing an approach to adjust pod resources dynamically without restarts. The issue received little discussion but quietly accumulated interest over time (13 👍 emoji reaction). Every few months, an automation bot attempted to mark it as stale, and every time, I removed the label. This went on for nearly six years&hellip;</p>
<p>Until the release of Kubernetes 1.35 where In-Place Pod Resize feature was <a href="https://kubernetes.io/blog/2025/12/19/kubernetes-v1-35-in-place-pod-resize-ga/">marked as stable</a>.</p>
<h2 id="what-in-place-pod-resize-brings">What In-Place Pod Resize Brings</h2>
<p>In-Place Pod Resize allows Kubernetes to update CPU and memory requests and limits without restarting pods, whenever it is safe to do so. This significantly reduces unnecessary restarts caused by resource changes, leading to fewer disruptions and more reliable workloads.</p>
<p>For applications whose resource needs evolve over time, especially after startup, this feature provides a long-missing building block.</p>
<h2 id="impact-on-verticalpodautoscaler">Impact on VerticalPodAutoscaler</h2>
<p>The new <code>resizePolicy</code> field is configured at the pod spec level. While it is technically possible to change pod resources manually, doing so does not scale. In practice, this feature should be driven by a workload controller.</p>
<p>At the moment, the only controller that supports in-place pod resize is the Vertical Pod Autoscaler (VPA).</p>
<p>There are two enhancement proposals enable this behavior:</p>
<ul>
<li>
<p><a href="https://github.com/kubernetes/autoscaler/tree/455d29039bf6b1eb9f784f498f28769a8698bc21/vertical-pod-autoscaler/enhancements/4016-in-place-updates-support">AEP-4016: Support for in place updates in VPA</a> which introduces <code>InPlaceOrRecreate</code> update mode</p>
</li>
<li>
<p><a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/enhancements/7862-cpu-startup-boost">AEP-7862: CPU Startup Boost</a> which is about temporarily boosting pod by giving more cpu during pod startup. This is conceptually similar to the approach proposed in my original issue.</p>
</li>
</ul>
<p>Here is an example of Deployment and VPA using both AEP features:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">my-heavy-java-app:stable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">1000m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1024Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">2000m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">2048Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;autoscaling.k8s.io/v1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">VerticalPodAutoscaler</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example-vpa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">targetRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;apps/v1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">updatePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">updateMode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;InPlaceOrRecreate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">resourcePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">containerPolicies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">containerName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;app&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">minAllowed</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;250m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;512Mi&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">maxAllowed</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3000m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;8192Mi&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># The CPU boosted resources can go beyond maxAllowed.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">startupBoost</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpu</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Factor&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">quantity</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="w">
</span></span></span></code></pre></div><p>With such configuration pod will have doubled cpu requests and limits during startup. During the boost period no resizing will happen.</p>
<p>Once the pod reaches the <code>Ready</code> state, the VPA controller scales CPU down to the currently recommended value.</p>
<p>After that, VPA continues operating normally, with the key difference that resource updates are applied in place whenever possible.</p>
<h2 id="limitations">Limitations</h2>
<p>Does this feature fully solve the problem described above? Only partially.</p>
<p>First, most application runtimes still impose fundamental constraints. Java and Python runtimes do not currently support resizing memory limits without a restart. This limitation exists outside of Kubernetes itself and is tracked in the OpenJDK project via <a href="https://bugs.openjdk.org/browse/JDK-8359211">an open ticket</a>.</p>
<figure class="align-center ">
    <picture>
        <source type="image/webp" srcset="/images/posts/in-place-pod-resize/runtimes-supporting-in-place-memory-resize_hu_b0b138508605bfb9.webp">
        <img loading="lazy" src="/images/posts/in-place-pod-resize/runtimes-supporting-in-place-memory-resize.png#center"
         alt="In-Place Pod Resize in Kubernetes: Dynamic Resource Management Without Restarts - Tim Allclair &amp; Mofi Rahman, Google"/>
    </picture><figcaption>
            In-Place Pod Resize in Kubernetes: Dynamic Resource Management Without Restarts - Tim Allclair &amp; Mofi Rahman, Google
        </figcaption>
</figure>
<p>Second, Kubernetes does not yet support decreasing memory limits, even with in-place Pod Resize enabled. This is a known limitation documented in the enhancement proposal for <a href="https://github.com/kubernetes/enhancements/tree/758ea034908515a934af09d03a927b24186af04c/keps/sig-node/1287-in-place-update-pod-resources#memory-limit-decreases">memory limit decreases</a>.</p>
<p>As a result, while in-place Pod Resize effectively addresses CPU-related startup spikes, memory resizing remains an open problem.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>In place Pod Resize gives a foundation for cool new features like StartupBoost and makes use of VPA more reliable. While important gaps remain, such as <a href="https://github.com/kubernetes/kubernetes/issues/135670">memory decrease support</a> and <a href="https://github.com/kubernetes/kubernetes/issues/126891">scheduling race condition</a>, this change represents a meaningful step forward.</p>
<p>For workloads with distinct startup and steady-state phases, Kubernetes is finally beginning to model reality more closely.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Contributing to Open Source: Why It Matters and How to Start</title>
      <link>https://alikhil.dev/posts/contributing-to-open-source-why-it-matters-and-how-to-start/</link>
      <pubDate>Thu, 18 Dec 2025 04:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/contributing-to-open-source-why-it-matters-and-how-to-start/</guid>
      <description>&lt;p&gt;Whether you’re curious about open source or wondering how to make a meaningful impact, this post guides you through the process. You’ll learn why contributing is important, discover the different ways to get involved, and find practical steps to take your first contribution.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Whether you’re curious about open source or wondering how to make a meaningful impact, this post guides you through the process. You’ll learn why contributing is important, discover the different ways to get involved, and find practical steps to take your first contribution.</p>
<h2 id="why">Why?</h2>
<p>First of all, why should you contribute to open source?</p>
<h3 id="giving-back">Giving back</h3>
<p>Everyone, from freelance engineers to Big Tech companies and even governments, uses Open Source Software (OSS). Some use it less, others more, but almost everyone depends on it in one way or another. Most of us are <em>consumers</em> of open source.</p>
<p>Contributing to OSS means giving something back. This is especially important given the many cases of projects (e.g <a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/#history-and-challenges">nginx-ingress</a>, <a href="https://github.com/external-secrets/external-secrets/issues/5084">external-secrets</a>) being deprecated due to maintainer burnout, lack of community support, or overwhelming workloads.</p>
<p>It is true that some OSS projects are backed by large companies and maintained by engineers who are paid to work on them. However, the half of open source projects are still maintained by individuals in their spare time for free (<a href="https://www.linuxfoundation.org/blog/open-source-maintainers-what-they-need-and-how-to-support-them">source</a>).</p>
<p>In this sense, contributing to OSS can be seen as a form of digital volunteering. Some companies (<a href="https://www.linkedin.com/posts/matthieublumberg_when-running-a-platform-using-open-source-activity-7382301443698892800-3St-?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABsF7nEBwwglPayi0aSCflAk2mD2nv-HVCA">Criteo</a>, <a href="https://www.futurice.com/blog/year-2015-in-company-sponsored-open-sourcehttps://www.futurice.com/blog/year-2015-in-company-sponsored-open-source">Futurice</a>) even offer paid volunteer time (VPTO) specifically for open-source contributions.</p>
<h3 id="learning">Learning</h3>
<p>Another strong reason to contribute to open source is personal growth.</p>
<p>Every contribution becomes a learning opportunity because it places you inside a real, production-grade codebase rather than a controlled tutorial environment. You learn how projects are structured, how architectural decisions are made, how backward compatibility is maintained, and how trade-offs are handled in practice. Often this means working with unfamiliar tools, languages, or ecosystems, which naturally expands your technical range.</p>
<p>At the same time, open source strongly develops communication and collaboration skills. Issues and pull requests force you to articulate problems clearly, propose solutions in a way others can evaluate, and explain <em>why</em> a particular approach makes sense. Feedback from maintainers and contributors exposes you to different perspectives and constraints, requiring you to adapt, clarify, and sometimes rethink your ideas.</p>
<p>Because most collaboration happens asynchronously and in writing, you also improve your ability to communicate precisely and concisely. Over time, this structured, public collaboration sharpens how you discuss technical topics, handle reviews, and work effectively with distributed teams. These are the same skills required in modern engineering organizations, making open source a highly practical training ground.</p>
<h3 id="networking-and-professional-reputation">Networking and professional reputation</h3>
<p>Open source contribution naturally leads to networking, even if you are not actively trying to “network.” By participating in issues, code reviews, and pull requests, you start interacting with maintainers and contributors from different companies, countries, and levels of seniority. Over time, these repeated interactions build familiarity and trust. People begin to recognize your name, your areas of expertise, and the quality of your work.</p>
<p>Regular contributions can turn these lightweight interactions into professional relationships. Maintainers may invite you to collaborate more closely, grant you additional responsibilities, or even recommend you for roles on their teams. In many cases, job opportunities arise not from formal applications, but from someone already knowing how you work.</p>
<p>Another important, and often underestimated, benefit of open source contribution is visibility. Most professional work is hidden behind NDAs and internal repositories, making it difficult to demonstrate your real impact. Open source work, on the other hand, is public by default. Your commits, pull requests, discussions, and design decisions are all visible and attributable to you.</p>
<p>This public track record allows you to clearly show not only <em>what</em> you built or improved, but also <em>how</em> you collaborate, communicate, and respond to feedback. For recruiters and hiring managers, this is far more convincing than a list of skills on a résumé. In practice, open source contributions often function as a living portfolio and a long-term investment in your professional reputation.</p>
<h2 id="how">How?</h2>
<p>Now that you understand why contributing matters, let’s look at how to get started.</p>
<h3 id="the-natural-way">The Natural way</h3>
<p>You might think that contributing to open source requires skills you do not have, a brilliant idea for a new library, or deep expertise in a specific domain. These beliefs often make OSS contribution feel unreachable.</p>
<p>That is not the case.</p>
<p>You do not need a revolutionary idea or special credentials. If you are an engineer who already writes code, you are capable of contributing.</p>
<p>Here is a simple approach: the next time you are solving a problem using an open source tool and notice that it does not work as expected (a bug) or lacks a feature you need, do not immediately abandon the tool. Use your skills, and an LLM if needed, to investigate and try to fix the issue.</p>
<p>If you succeed, open a pull request to the upstream repository. If you do not, create an issue and share your findings. That is still a contribution, and you will have learned something in the process.</p>
<h3 id="good-first-issue">Good first issue</h3>
<p>If everything you use works perfectly and you do not notice any gaps, you can take a more deliberate approach.</p>
<p>Make a list of projects you like, use, or want to learn more about. Browse their open issues and look for ones labeled “good first issue” or similar. Pick something that matches your current skill level and try to tackle it.</p>
<p>If your list is short or you cannot find suitable issues, there are also curated lists of projects actively looking for contributors.</p>
<ul>
<li><a href="https://goodfirstissue.dev/">https://goodfirstissue.dev/</a></li>
<li><a href="https://forgoodfirstissue.github.com/">https://forgoodfirstissue.github.com/</a></li>
<li><a href="https://goodfirstissues.com/">https://goodfirstissues.com/</a></li>
</ul>
<h2 id="summary">Summary</h2>
<p>Open source is great, but keeping it that way requires people to contribute.
It is not hard or unreachable. Anyone can do it, and the community needs more people like you.
Start small, pick a project you love, and take your first step</p>]]></content:encoded>
    </item>
    
    <item>
      <title>kubectl-find - UNIX-find-like plugin to find resources and perform action on them</title>
      <link>https://alikhil.dev/posts/kubectl-find-plugin-50-stars-anniversary/</link>
      <pubDate>Fri, 17 Oct 2025 19:27:24 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/kubectl-find-plugin-50-stars-anniversary/</guid>
      <description>&lt;p&gt;Recently, I have developed a plugin for &lt;code&gt;kubectl&lt;/code&gt; inspired by UNIX &lt;code&gt;find&lt;/code&gt; utility to find and perform action on resources. And few days ago number of stars in the repo reached 50! I think it&amp;rsquo;s a good moment to tell more about the project.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Recently, I have developed a plugin for <code>kubectl</code> inspired by UNIX <code>find</code> utility to find and perform action on resources. And few days ago number of stars in the repo reached 50! I think it&rsquo;s a good moment to tell more about the project.</p>
<h2 id="the-problem">The problem</h2>
<p>As engineer who works with kubernetes everyday I use kubectl a lot. Actually, more than 50% of my terminal history commands are related to kubernetes. Here is a top 10 commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">1	<span class="m">2079</span>  46.7611%    kubectl
</span></span><span class="line"><span class="cl">2	<span class="m">425</span>   9.55915%    git
</span></span><span class="line"><span class="cl">3	<span class="m">324</span>   7.28745%    helm
</span></span><span class="line"><span class="cl">4	<span class="m">156</span>   3.50877%    <span class="nb">cd</span>
</span></span><span class="line"><span class="cl">5	<span class="m">146</span>   3.28385%    ssh
</span></span><span class="line"><span class="cl">6	<span class="m">130</span>   2.92398%    kctx
</span></span><span class="line"><span class="cl">7	<span class="m">114</span>   2.5641%     kns
</span></span><span class="line"><span class="cl">8	<span class="m">80</span>    1.79937%    gcloud-auth
</span></span><span class="line"><span class="cl">9	<span class="m">78</span>    1.75439%    curl
</span></span><span class="line"><span class="cl">10	<span class="m">66</span>    1.48448%    docker
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><blockquote>
<p>Run <a href="https://superuser.com/a/250230">this command</a> if you are curious what about yours the most popular commands in terminal history.</p>
</blockquote>
<p>I use kubectl to check status of the pods, delete orphaned resources, trigger sync on <code>ExternalSecrets</code> and much more. When I realized half my terminal history was just kubectl commands, I thought — there must be a better way to find things in Kubernetes without chaining pipes with <code>grep</code> / <code>awk</code> / <code>xargs</code>. And I imagined how nice it would be to have a UNIX <code>find</code>-like tool — something that lets you search for exactly what you need in the cluster and then perform actions directly on the matching resources. I searched for a krew plugin like this but there was not any. For that reason, I decided to develop <a href="https://github.com/alikhil/kubectl-find">one</a>!</p>
<h2 id="development">Development</h2>
<p>I used <a href="https://github.com/kubernetes/sample-cli-plugin">sample-cli-plugin</a> as a starting point. Its clean repository structure and straightforward design make it a great reference for working with the Kubernetes API. Additionally, it allows easy reuse of the extensive Kubernetes client libraries.</p>
<p>Almost everything in the Kubernetes ecosystem is written in Go, and this plugin is no exception — which is great, as it allows building binaries for a wide range of CPU architectures and operating systems.</p>
<h2 id="features-overview">Features overview</h2>
<h3 id="filters">Filters</h3>
<h4 id="find-by-resource-name-using-regex">Find by resource name using regex</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods -r dev
</span></span></code></pre></div><h4 id="find-pods-with-restarted-containers">Find pods with restarted containers</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --restarted
</span></span></code></pre></div><h4 id="find-pods-with-image-matching-regex">Find pods with image matching regex</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --image bitnami
</span></span></code></pre></div><h4 id="find-pods-by-status">Find pods by status</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --status Pending
</span></span></code></pre></div><h4 id="find-pods-running-on-nodes-matching-regex">Find pods running on nodes matching regex</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl fd pods --node <span class="s1">&#39;*spot&#39;</span>
</span></span></code></pre></div><h4 id="find-any-resource-by-custom-condition">Find any resource by custom condition</h4>
<p>Use <code>jq</code> filter to find any resource by any custom condition. <code>kubectl-find</code> uses <a href="https://github.com/itchyny/gojq">gojq</a> implementation of <code>jq</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># find Services with trafficDistribution set to PreferClose</span>
</span></span><span class="line"><span class="cl">kubectl fd svc -j <span class="s1">&#39;.spec.trafficDistribution == &#34;PreferClose&#34;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># find pods with unset nodeSelector</span>
</span></span><span class="line"><span class="cl">kubectl fd pods -j <span class="s1">&#39;.spec.nodeSelector == null&#39;</span> -A
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># find pods with undefined resources</span>
</span></span><span class="line"><span class="cl">kubectl fd pods -j <span class="s1">&#39;any( .spec.containers[]; .resources == {} )&#39;</span> -A
</span></span></code></pre></div><h3 id="actions">Actions</h3>
<p>By default, <code>fd</code> will print found resources to Stdout. However, there flags that you can provide to perform action on found resources:</p>
<ul>
<li><code>--delete</code> - to delete them</li>
<li><code>--patch</code> - to patch with provided JSON</li>
<li><code>--exec</code> - to run command on pods</li>
</ul>
<h2 id="getting-started">Getting started</h2>
<p>Use <a href="https://krew.sigs.k8s.io/">krew</a> to install the plugin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">krew install fd
</span></span></code></pre></div><h2 id="whats-next">What’s next</h2>
<p>I’m currently working on adding:</p>
<ul>
<li>JSON/YAML output format</li>
<li>More filters</li>
<li>Saved queries</li>
</ul>
<p>If you’re tired of writing long <code>kubectl | grep | xargs</code> chains, give <code>kubectl fd</code> a try — it’s already saved me countless keystrokes.</p>
<p>Check out the repo ⭐ <a href="https://github.com/alikhil/kubectl-find">github.com/alikhil/kubectl-find</a>
and share your ideas or issues — I’d love to hear how you use it!</p>
<p><a href="https://github.com/alikhil/kubectl-find">
  <img loading="lazy" src="https://api.star-history.com/svg?repos=alikhil/kubectl-find&amp;type=date&amp;legend=bottom-right" alt="Star History Chart"  /></a></p>]]></content:encoded>
    </item>
    
    <item>
      <title>Protect Kubernetes Services with OAuth2 Proxy, ingress-nginx, and Pocket ID</title>
      <link>https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/</link>
      <pubDate>Tue, 05 Aug 2025 21:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/</guid>
      <description>Step-by-step guide to protecting internal Kubernetes services using OAuth2 Proxy, ingress-nginx auth annotations, and Pocket ID as the OIDC provider. Covers Helm values, cookie config, ingress annotations, and troubleshooting.</description>
      <content:encoded><![CDATA[<p>The original post wrote <a href="https://alikhil.dev/posts/oauth2-proxy-for-kubernetes-services/">about oauth2-proxy</a> over seven years ago was quite popular at the time and attracted a lot of organic traffic to my blog, which still benefits my SEO today. Since the tutorial had become outdated, I decided to rewrite it.</p>
<blockquote>
<p><strong>Update (August 2026):</strong> ingress-nginx is being retired. For a new installation, use the <a href="/posts/oauth2-proxy-gateway-api-traefik/">Gateway API, Traefik, OAuth2 Proxy, and Pocket ID version of this guide</a>. Keep reading only if you are maintaining an existing ingress-nginx setup.</p>
</blockquote>
<h2 id="what-we-have">What we have</h2>
<p>We have a Kubernetes cluster with several web services deployed for internal use.</p>
<h2 id="what-we-want-to-achieve">What we want to achieve</h2>
<p>We want to expose our internal web services to the Internet, but restrict access by requiring authorization. Access should be granted only to users authenticated through our Identity Provider (such as Google, GitHub, Keycloak, etc.).</p>
<h2 id="assumptions">Assumptions</h2>
<p>For simplicity, let&rsquo;s assume that both <a href="https://github.com/kubernetes/ingress-nginx">ingress-nginx</a> and <a href="https://cert-manager.io/">cert-manager</a> are already deployed in the cluster.</p>
<p>I will use <a href="https://github.com/pocket-id/pocket-id">Pocket ID</a> as Identity Provider in this tutorial. Configuration slightly differs for different providers. Check the <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/">official documentation</a> for your provider.</p>
<p>For the examples in this guide, I’ll use my <code>alikhil.dev</code> domain:</p>
<p>– <code>pocket-id.k8s.alikhil.dev</code> - will be used for Pocket ID</p>
<ul>
<li>
<p><code>k8s.alikhil.dev</code> - will be used for oauth2-proxy. I recommend to have higher domain for oauth2-proxy service for easier cookie setup.</p>
</li>
<li>
<p><code>*.k8s.alikhil.dev</code> - reserved for services deployed for internal usage</p>
</li>
</ul>
<h2 id="preparation">Preparation</h2>
<h3 id="dns">DNS</h3>
<p>I have added two DNS records:</p>
<ol>
<li><code>A</code> record for k8s.alikhil.dev pointing to ingress-nginx <code>LoadBalancer</code> IP address in the cluster (<code>kubectl get svc -n ingress-nginx | grep LoadBalancer</code>)</li>
<li><code>CNAME</code> record for <code>*.k8s.alikhil.dev</code> pointing to <code>k8s.alikhil.dev</code></li>
</ol>
<h3 id="install-pocket-id">Install Pocket ID</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm repo add anza-labs https://anza-labs.github.io/charts
</span></span><span class="line"><span class="cl">helm upgrade --install pocket-id anza-labs/pocket-id -f ./values/pocket-id.yaml
</span></span></code></pre></div><details>
    <summary>Values for pocket-id</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Specifies whether ingress should be enabled.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Ingress class name.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Annotations to add to the ingress.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Ingress host configuration.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">pocket-id.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- List of TLS configurations for the ingress.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span>- <span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">pocket-id-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span>- <span class="l">pocket-id.k8s.alikhil.dev</span><span class="w">
</span></span></span></code></pre></div></details>
<h3 id="configure-pocketid">Configure PocketID</h3>
<p>Go to <a href="https://pocket-id.k8s.alikhil.dev/signup/setup">https://pocket-id.k8s.alikhil.dev/signup/setup</a> and set initial configuration for Pocket ID.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-setup.png" type="" alt="Initial setup page"  /></p>
<p>Then add your passkey.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-pass.png" type="" alt="Add passkey"  /></p>
<p>Create <strong>developers</strong> group and add yourself to the list of members.
<img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-groups.png" type="" alt="Create group"  /></p>
<p>After that, go to <strong>OIDC clients</strong> page and create one for oauth2-proxy. Set proper callback url.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-client.png" type="" alt="PocketID client creation"  /></p>
<p>Save generated <strong>Client ID</strong> and <strong>Client Secret</strong> for later use.</p>
<h2 id="installing-oauth2-proxy">Installing oauth2-proxy</h2>
<p>I am using raw k8s secrets in this tutorial, but I highly recommend storing secrets in Vault or similar services and use <a href="https://external-secrets.io/latest/">External Secretes Operator</a> to deliver them to kubernetes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create secret generic oauth2-proxy-secrets --from-literal<span class="o">=</span>client-id<span class="o">=</span><span class="nv">$CLIENT_ID</span> --from-literal<span class="o">=</span>client-secret<span class="o">=</span><span class="nv">$CLIENT_SECRET</span> --from-literal<span class="o">=</span>cookie-secret<span class="o">=</span><span class="k">$(</span>openssl rand -base64 <span class="m">32</span> <span class="p">|</span> head -c <span class="m">32</span> <span class="p">|</span> base64<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
</span></span><span class="line"><span class="cl">helm install oauth2-proxy oauth2-proxy/oauth2-proxy -f values/oauth2-proxy.yaml
</span></span></code></pre></div><details>
    <summary>Adjust domains in values for oauth2-proxy</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Oauth client configuration specifics</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cookieName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;general-oauth2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default configuration, to be overridden</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">configFile</span><span class="p">:</span><span class="w"> </span><span class="p">|-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    email_domains = [ &#34;*&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    upstreams = [ &#34;file:///dev/null&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    skip_provider_button = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    allowed_groups = [ &#34;developers&#34;, &#34;admins&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_secure = false
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_domains = [&#34;.k8s.alikhil.dev&#34;, &#34;k8s.alikhil.dev&#34;]
</span></span></span><span class="line"><span class="cl"><span class="sd">    whitelist_domains = [ &#34;*.k8s.alikhil.dev&#34;, &#34;k8s.alikhil.dev&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_samesite = &#34;lax&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_csrf_per_request = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_csrf_expire = &#34;15m&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    pass_access_token = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    pass_authorization_header = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    provider = &#34;oidc&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    provider_display_name = &#34;PocketID&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    reverse_proxy = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    scope = &#34;openid profile email groups&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    session_store_type = &#34;redis&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    set_xauthrequest = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    set_authorization_header = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    silence_ping_logging = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    skip_auth_preflight = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    ssl_insecure_skip_verify = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    ssl_upstream_insecure_skip_verify = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    insecure_oidc_allow_unverified_email = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    oidc_issuer_url = &#34;https://pocket-id.k8s.alikhil.dev&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    redirect_url = &#34;https://k8s.alikhil.dev/oauth2/callback&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    # to reduce log amount
</span></span></span><span class="line"><span class="cl"><span class="sd">    request_logging = false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Only used if API capabilities (networking.k8s.io/v1) allow it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Used to create an Ingress record.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/cors-allow-origin</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/enable-cors</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;true&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Secrets must be manually created in the namespace.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Configure the session storage type, between cookie and redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sessionStorage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Can be one of the supported session storage cookie|redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">passwordKey</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;redis-password&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">clientType</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;standalone&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Enables and configure the automatic deployment of the redis subchart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># provision an instance of the redis sub-chart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">architecture</span><span class="p">:</span><span class="w"> </span><span class="l">standalone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">master</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">100m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">128Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1Gi</span><span class="w">
</span></span></span></code></pre></div></details>
<h2 id="testing">Testing</h2>
<h3 id="install-whoami">Install whoami</h3>
<p>To check oauth2-proxy we need a dummy service. I will use <a href="https://artifacthub.io/packages/helm/cowboysysop/whoami">whoami</a> helm chart for this.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm repo add cowboysysop https://cowboysysop.github.io/charts/
</span></span><span class="line"><span class="cl">helm install whoami cowboysysop/whoami
</span></span></code></pre></div><details>
    <summary>Values for whoami helm chart</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># put oauth2-proxy domain here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://k8s.alikhil.dev/oauth2/start?rd=https://$host$request_uri$is_args$args&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># service-name.namespace-name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-example.svc.cluster.local:80/oauth2/auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">whoami.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">whoami.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">whoami-cert</span><span class="w">
</span></span></span></code></pre></div></details>
<h3 id="perform-test">Perform test</h3>
<p>Go to whoami url and check if oauth2-proxy redirects you to Pocket ID like in the demo:</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/demo.gif" type="" alt="Demo"  /></p>
<h2 id="troubleshooting">Troubleshooting</h2>
<table>
	<thead>
			<tr>
					<th>Symptom</th>
					<th>Likely cause</th>
					<th>Fix</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Redirect loops back to login after successful auth</td>
					<td><code>redirect_url</code> in oauth2-proxy config doesn&rsquo;t match the callback URL registered in Pocket ID</td>
					<td>Set both to exactly <code>https://k8s.alikhil.dev/oauth2/callback</code></td>
			</tr>
			<tr>
					<td><code>cookie_secret</code> error on startup</td>
					<td>Cookie secret is not exactly 16, 24, or 32 bytes after base64 decode</td>
					<td>Regenerate: <code>openssl rand -base64 32 | head -c 32 | base64</code></td>
			</tr>
			<tr>
					<td>401 on every request despite logging in</td>
					<td><code>cookie_domains</code> / <code>whitelist_domains</code> doesn&rsquo;t cover the protected service domain</td>
					<td>Add <code>.k8s.alikhil.dev</code> to both <code>cookie_domains</code> and <code>whitelist_domains</code> in the config</td>
			</tr>
			<tr>
					<td>Protected service returns 401, not redirecting</td>
					<td><code>auth-url</code> annotation points to wrong service name or namespace</td>
					<td>Use the in-cluster DNS form: <code>http://oauth2-proxy.&lt;namespace&gt;.svc.cluster.local:80/oauth2/auth</code></td>
			</tr>
			<tr>
					<td>Auth works but Pocket ID group check fails</td>
					<td><code>allowed_groups</code> set but <code>scope</code> doesn&rsquo;t include <code>groups</code></td>
					<td>Add <code>groups</code> to the <code>scope</code> config option and ensure the Pocket ID client returns group claims</td>
			</tr>
	</tbody>
</table>
<h2 id="takeaways">Takeaways</h2>
<p>Later, when you need to protect any service in Kubernetes with oauth2-proxy, you simply need to add two annotations to your Ingress resource:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://k8s.alikhil.dev/oauth2/start?rd=https://$host$request_uri$is_args$args&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-example.svc.cluster.local:80/oauth2/auth</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Remote LAN access with WireGuard and Mikrotik</title>
      <link>https://alikhil.dev/posts/remote-lan-access-with-wireguard-and-mikrotik/</link>
      <pubDate>Sun, 13 Apr 2025 14:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/remote-lan-access-with-wireguard-and-mikrotik/</guid>
      <description>Step-by-step guide to setting up remote LAN access using WireGuard on a MikroTik hEX S router and a cloud VM. Covers RouterOS CLI, firewall rules, routing, and client configuration.</description>
      <content:encoded><![CDATA[<p>Recently I have configured out how to access my home and cloud network remotely using WireGuard and Mikrotik Hex S router. With this step-by-step tutorial I will show you (and perhaps my future self) how to do it.</p>
<p><strong>Tested with</strong>: MikroTik hEX S (RB760iGS) · RouterOS 7.x · Ubuntu 22.04 LTS · WireGuard hub-and-spoke topology</p>
<h2 id="requirements">Requirements</h2>
<h3 id="what-i-have">What I have</h3>
<ul>
<li>
<p>A Mikrotik Hex S router with a dynamic public IP address.</p>
</li>
<li>
<p>Services in a cloud VM (Ubuntu 22) with a static public IP address.</p>
</li>
<li>
<p>Services in a VM on my home network.</p>
</li>
<li>
<p>Clients - laptops and phones - that need to access the services in my home and cloud network.</p>
</li>
</ul>
<h3 id="what-i-want">What I want</h3>
<ul>
<li>
<p>Clients outside of my home network should be able to access services both on my home and cloud network.</p>
</li>
<li>
<p>Only traffic to my home and cloud network should be routed through the VPN.</p>
</li>
<li>
<p>Clients inside my home network should be able to access services on my cloud network without additional configuration.</p>
</li>
<li>
<p>No external centralized service should be used.</p>
</li>
<li>
<p>No open ports on my home router.</p>
</li>
</ul>
<p><img loading="lazy" src="/images/posts/mikrotik-wg/final.png" type="" alt="Final configuration"  /></p>
<h2 id="implementation">Implementation</h2>
<blockquote>
<p>Nowadays, there are plenty of VPN solutions like zero-tier and tailscale. However, I think they are too bloated for my humble needs and WireGuard is more than enough for that.</p>
</blockquote>
<p>Because of last requirement, it&rsquo;s obvious that traffic to home network should be routed though my cloud server.<br>
So, I will use WireGuard to create a tunnel between mikrotik router and cloud server.<br>
This way, I can access my home network from anywhere without exposing any ports on my home router.</p>
<h3 id="i-initial-configuration-of-wireguard-on-cloud-vm">I. Initial configuration of WireGuard on cloud VM</h3>
<blockquote>
<p><em>For the sake of reproducibility and simplicity I will use <a href="https://www.wireguard.com/">vanilla wireguard</a> and configure it on OS level, not in docker container.</em></p>
</blockquote>
<h4 id="1-install-wireguard-on-ubuntu-2204">1. Install wireguard on Ubuntu 22.04</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install wireguard -y
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Edit the configuration file</span>
</span></span><span class="line"><span class="cl">sudo nano /etc/sysctl.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Find and uncomment the line:</span>
</span></span><span class="line"><span class="cl">net.ipv4.ip_forward<span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then apply the changes:</span>
</span></span><span class="line"><span class="cl">sudo sysctl -p
</span></span></code></pre></div><h4 id="2-create-public-and-private-key">2. Create public and private key</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo su -
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /etc/wireguard
</span></span><span class="line"><span class="cl"><span class="c1"># Generate private and public keys</span>
</span></span><span class="line"><span class="cl">wg genkey <span class="p">|</span> tee privatekey <span class="p">|</span> wg pubkey &gt; publickey
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check the keys</span>
</span></span><span class="line"><span class="cl">cat privatekey
</span></span><span class="line"><span class="cl">cat publickey
</span></span></code></pre></div><h4 id="3-create-the-configuration-file">3. Create the configuration file</h4>
<p>I will use <code>172.16.10.0/24</code> subnet for Wireguard network.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Add the following lines:</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Interface<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">Address</span> <span class="o">=</span> 172.16.10.1/24
</span></span><span class="line"><span class="cl"><span class="nv">ListenPort</span> <span class="o">=</span> <span class="m">27277</span> <span class="c1"># You can choose any port you want</span>
</span></span><span class="line"><span class="cl"><span class="nv">Privatekey</span> <span class="o">=</span> &lt;your_private_key&gt; <span class="c1"># from previous step</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># These are placeholder lines for next steps, keep them commented for now</span>
</span></span><span class="line"><span class="cl"><span class="c1"># # Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [Peer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AllowedIPs = 172.16.10.0/24,192.168.0.0/16</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PublicKey = mikrotik public key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># # client peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [Peer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AllowedIPs = 172.16.10.2/32</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PublicKey = first client device public key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CRTL+X: save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change the permissions of the configuration file</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> wg0.conf
</span></span></code></pre></div><h4 id="4-start-the-wireguard-interface">4. Start the WireGuard interface</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable service to start on boot</span>
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> wg-quick@wg0.service
</span></span><span class="line"><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start the service</span>
</span></span><span class="line"><span class="cl">systemctl start wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Check wg status</span>
</span></span><span class="line"><span class="cl">wg
</span></span></code></pre></div><p>It should show the interface is up like this</p>
<p><img loading="lazy" src="/images/posts/mikrotik-wg/wg.png" type="" alt="WireGuard status"  /></p>
<p>More commands to check the status of the service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check the status of the service</span>
</span></span><span class="line"><span class="cl">systemctl status wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the service if needed</span>
</span></span><span class="line"><span class="cl">systemctl restart wg-quick@wg0
</span></span></code></pre></div><h3 id="ii-mikrotik-hex-s-wireguard-setup">II. MikroTik hEX S WireGuard setup</h3>
<p>I will use Mikrotik command line interface (CLI) to configure the router. You can use Winbox or WebFig if you prefer.</p>
<h4 id="1-create-wireguard-interface">1. Create WireGuard interface</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create WireGuard interface and it will automatically generate private and public keys</span>
</span></span><span class="line"><span class="cl">/interface wireguard add listen-port<span class="o">=</span><span class="m">13231</span> <span class="nv">mtu</span><span class="o">=</span><span class="m">1420</span> <span class="nv">name</span><span class="o">=</span>cloud-wg
</span></span><span class="line"><span class="cl"><span class="c1"># Add the IP address to the interface</span>
</span></span><span class="line"><span class="cl">/ip add <span class="nv">address</span><span class="o">=</span>172.16.10.3/24 <span class="nv">interface</span><span class="o">=</span>cloud-wg <span class="nv">network</span><span class="o">=</span>172.16.10.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Print the keys, you will need only the public key later</span>
</span></span><span class="line"><span class="cl">/interface/wireguard/print
</span></span></code></pre></div><h4 id="2-create-the-peer">2. Create the peer</h4>
<p>Here we add cloud vm as a peer to the Mikrotik router&rsquo;s wireguard. The public key of the cloud server is needed here.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># In allowed-address we add IP ranges which will be routed through this peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 172.21.0.0/16 - subnet of docker containers on cloud server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 172.16.10.0/16 - subnet of wireguard network</span>
</span></span><span class="line"><span class="cl">/interface wireguard peers add allowed-address<span class="o">=</span>172.16.10.0/24,172.21.0.0/16 <span class="se">\
</span></span></span><span class="line"><span class="cl">    endpoint-address<span class="o">=</span>cloud.vm.ip.address <span class="se">\
</span></span></span><span class="line"><span class="cl">    endpoint-port<span class="o">=</span><span class="m">27277</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">interface</span><span class="o">=</span>cloud-wg <span class="se">\
</span></span></span><span class="line"><span class="cl">    public-key<span class="o">=</span><span class="s2">&#34;PUT CLOUD SERVER PUBLIC KEY HERE&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span>peer1 persistent-keepalive<span class="o">=</span>25s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Here is a tricky part</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Routing rule for subnet of wireguard network is added automatically on adding IP address to the interface</span>
</span></span><span class="line"><span class="cl"><span class="c1"># But we need to add explicitly the route for docker containers subnet</span>
</span></span><span class="line"><span class="cl"><span class="c1"># You may need to adjust the distance and routing-table values</span>
</span></span><span class="line"><span class="cl">/ip route <span class="nv">disabled</span><span class="o">=</span>no <span class="nv">distance</span><span class="o">=</span><span class="m">1</span> dst-address<span class="o">=</span>172.21.0.0/16 <span class="nv">gateway</span><span class="o">=</span>cloud-wg routing-table<span class="o">=</span>main <span class="nv">scope</span><span class="o">=</span><span class="m">30</span> suppress-hw-offload<span class="o">=</span>no target-scope<span class="o">=</span><span class="m">10</span>
</span></span></code></pre></div><h3 id="iii-add-wireguard-peer-vm">III. Add WireGuard peer (VM)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Uncomment the lines for Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Peer<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">AllowedIPs</span> <span class="o">=</span> 172.16.10.0/24,192.168.0.0/16
</span></span><span class="line"><span class="cl"><span class="nv">PublicKey</span> <span class="o">=</span> mikrotik public key
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the WireGuard service</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Check if changes are applied</span>
</span></span><span class="line"><span class="cl">wg show
</span></span></code></pre></div><p>You should see the Mikrotik peer in the list of peers.
<img loading="lazy" src="/images/posts/mikrotik-wg/wg-show-2.png" type="" alt="WireGuard peers"  /></p>
<details>
<summary>More commands to tests connection between cloud vm and mikrotik wg</summary>
<h5 id="run-in-mikrotik-cli">Run in mikrotik CLI</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of cloud server</span>
</span></span><span class="line"><span class="cl">ping 172.16.10.1
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mikrotik-ping.png" type="" alt="WireGuard ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on cloud vm</span>
</span></span><span class="line"><span class="cl">put <span class="o">[</span>resolve google.com server 172.21.0.114<span class="o">]</span>
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mikrotik-resolve.png" type="" alt="Wireguard resolve"  /></p>
<h5 id="run-in-cloud-server">Run in cloud server</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of Mikrotik router</span>
</span></span><span class="line"><span class="cl">ping 172.168.10.3
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/vm-ping.png" type="" alt="VM ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on home adguard</span>
</span></span><span class="line"><span class="cl">dig google.com @192.168.11.2
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/vm-resolve.png" type="" alt="VM resolve"  /></p>
<h5 id="run-on-my-mac-connected-to-home-network">Run on my Mac connected to home network</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of cloud server</span>
</span></span><span class="line"><span class="cl">ping 172.16.10.1
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mac-ping.png" type="" alt="mac ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on cloud adguard</span>
</span></span><span class="line"><span class="cl">dig google.com @172.121.0.114
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mac-resolve.png" type="" alt="mac resolve"  /></p>
</details>
<h3 id="iv-add-first-client-device-as-a-peer">IV. Add first client device as a peer</h3>
<p>I recommend you to use your smartphone as first client device, because it can work from both home WiFi and mobile data. This way you can test the connection from both networks.</p>
<p>Also, install on your smartphone:</p>
<ul>
<li>WireGuard app (<a href="https://apps.apple.com/us/app/wireguard/id1441195209">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.wireguard.android&amp;hl=en&amp;pli=1">Android</a>)</li>
<li>Network debugging app (<a href="https://apps.apple.com/bo/app/inettools-ping-dns-port-scan/id561659975">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.ulfdittmer.android.ping&amp;hl=en">Android</a>)</li>
</ul>
<h4 id="1-create-keys-for-the-client">1. Create keys for the client</h4>
<p>Install Wireguard app for your client OS.</p>
<p>Then, generate public and private keys on the device, for that create config from scratch in the app and then click on <strong>Generate keypair</strong> button.</p>
<p>Or you can generate keys on the cloud server and then copy them to the client device.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wg genkey <span class="p">|</span> tee peer-privatekey <span class="p">|</span> wg pubkey &gt; peer-publickey
</span></span></code></pre></div><h4 id="2-add-the-client-as-a-peer-to-wireguard-config-on-cloud-server">2. Add the client as a peer to WireGuard config on cloud server</h4>
<p>On the cloud server, edit the WireGuard config file and add the client as a peer.</p>
<p>Each time increment previous peer address by 1.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Uncomment the lines for client peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># client peer</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Peer<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">AllowedIPs</span> <span class="o">=</span> 172.16.10.2/32
</span></span><span class="line"><span class="cl"><span class="nv">PublicKey</span> <span class="o">=</span> put public key
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CRTL+X: save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the WireGuard service</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart wg-quick@wg0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check if changes are applied</span>
</span></span><span class="line"><span class="cl">wg show
</span></span></code></pre></div><h4 id="3-continue-configuring-the-client-device">3. Continue configuring the client device</h4>
<p>You already have public and private keys for the client device, other configuration parameters are:</p>
<h5 id="interface">Interface</h5>
<ul>
<li><strong>Address</strong> - Its address in the WireGuard subnet. Put the same address you set in the <code>AllowedIPs</code> field in the previous step.</li>
<li><strong>DNS servers</strong> - if you have adguard/pihole running on the cloud server, you can use it as a DNS server. Put its IP address here.</li>
<li><strong>MTU</strong> and <strong>ListenPort</strong> - you can leave them empty, they will be set automatically.</li>
</ul>
<h5 id="peer">Peer</h5>
<ul>
<li><strong>Endpoint</strong> - cloud server IP address and port (27277)</li>
<li><strong>Public key</strong> - public key of the cloud server</li>
<li><strong>AllowedIPs</strong> - here we put all subnets that we want to access from this current client device</li>
</ul>
<p>Here is an example of the configuration file for the client device:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Interface]</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateKey</span> <span class="o">=</span> <span class="s">private-key-of-client-device</span>
</span></span><span class="line"><span class="cl"><span class="na">Address</span> <span class="o">=</span> <span class="s">172.16.10.2/32</span>
</span></span><span class="line"><span class="cl"><span class="na">DNS</span> <span class="o">=</span> <span class="s">172.21.0.114 # IP of adguard container on cloud server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Peer]</span>
</span></span><span class="line"><span class="cl"><span class="na">PublicKey</span> <span class="o">=</span> <span class="s">public-key-of-cloud-server</span>
</span></span><span class="line"><span class="cl"><span class="na">AllowedIPs</span> <span class="o">=</span> <span class="s">192.168.0.0/16, 172.16.10.0/24, 172.21.0.0/16 # subnets of home, cloud and wireguard network</span>
</span></span><span class="line"><span class="cl"><span class="na">Endpoint</span> <span class="o">=</span> <span class="s">ip.of.cloud.servner:27277</span>
</span></span></code></pre></div><p>That&rsquo;s it! Disconnect your device from the home Wi-Fi, switch to mobile data and connect to the VPN.</p>
<p>Then try to:</p>
<ul>
<li>ping the cloud server and Mikrotik router IP addresses in wireguard subnet.</li>
<li>check ports of services in docker containers on cloud and home server VM.</li>
</ul>
<style>
td, th {
   border: none!important;
}
</style>
<table>
	<thead>
			<tr>
					<th>ping cloud</th>
					<th>ping home</th>
					<th>check adguard port</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-ping-vm.jpeg" type="" alt="ping cloud"  /></td>
					<td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-ping-home.jpeg" type="" alt="ping home"  /></td>
					<td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-port-scan.jpeg" type="" alt="port scan"  /></td>
			</tr>
	</tbody>
</table>
<h3 id="v-troubleshooting">V. Troubleshooting</h3>
<table>
	<thead>
			<tr>
					<th>Symptom</th>
					<th>Likely cause</th>
					<th>Fix</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Handshake shows &ldquo;never&rdquo;</td>
					<td>Firewall blocks UDP port on cloud VM, or wrong public key</td>
					<td>Check <code>ufw</code>/<code>iptables</code> allows the WireGuard UDP port; verify both peers have each other&rsquo;s correct public key</td>
			</tr>
			<tr>
					<td>Peer handshakes OK but home LAN is unreachable</td>
					<td>Missing route or <code>AllowedIPs</code> too narrow</td>
					<td>On cloud VM, ensure MikroTik peer <code>AllowedIPs</code> includes <code>192.168.0.0/16</code>; on MikroTik, add a route for cloud subnets via <code>cloud-wg</code></td>
			</tr>
			<tr>
					<td>Handshake works but DNS fails</td>
					<td>Wrong DNS IP in client config, or AdGuard not listening on WireGuard interface</td>
					<td>Confirm DNS container IP is reachable over WireGuard (<code>ping</code> it); check AdGuard binds to <code>0.0.0.0</code> not just <code>localhost</code></td>
			</tr>
			<tr>
					<td><code>AllowedIPs</code> routing mistake</td>
					<td>Client routes all traffic through VPN</td>
					<td>Set <code>AllowedIPs</code> to specific subnets only (split-tunnel), not <code>0.0.0.0/0</code>, unless you want full-tunnel</td>
			</tr>
			<tr>
					<td>MikroTik shows peer but no traffic</td>
					<td><code>persistent-keepalive</code> not set, NAT drops idle UDP</td>
					<td>Add <code>persistent-keepalive=25s</code> to the MikroTik peer config</td>
			</tr>
	</tbody>
</table>
<h3 id="vi-bonus-dns-configuration">VI. Bonus. DNS configuration</h3>
<p>Since I have 2 adguard instances and I use them as DNS servers everywhere, I will add DNS records for accessing my services:</p>
<ul>
<li>*<strong>.cloud.domain.com</strong> - pointing to traefik docker container on cloud server</li>
<li>*<strong>.home.domain.com</strong> - pointing to traefik docker container on home server</li>
</ul>
<p>Thus, I can access my services using domain names instead of IP addresses.</p>
<img src="/images/posts/mikrotik-wg/dns.png" width="30%">
<h2 id="final-thoughts">Final thoughts</h2>
<p>I hope this tutorial was helpful for you. I will keep it updated if I find any issues or improvements.</p>
<p>If you have any questions or suggestions, feel free to leave a comment below.</p>
<p>Credits to <a href="https://www.laroberto.com/">@laroberto</a> for their <a href="https://www.laroberto.com/remote-lan-access-with-wireguard/">guide on LAN access with WireGuard</a>. I followed it to set up the initial configuration and then adapted it to my needs.</p>
<h2 id="recommended-reading">Recommended reading</h2>
<ul>
<li><a href="/posts/saving-and-using-secrets-in-mikrotitk-routeros/">Storing and Using Secrets in MikroTik RouterOS</a> — how to avoid hardcoding passwords and tokens in your RouterOS scripts.</li>
</ul>]]></content:encoded>
    </item>
    
    <item>
      <title>Unintended Side Effects of Using http.DefaultClient in Go</title>
      <link>https://alikhil.dev/posts/go-global-http-client-gotcha/</link>
      <pubDate>Wed, 19 Mar 2025 19:20:45 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/go-global-http-client-gotcha/</guid>
      <description>&lt;p&gt;The Internet is plenty of articles that telling why you should not be using &lt;code&gt;http.DefaultClient&lt;/code&gt; in Golang (&lt;a href=&#34;https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779&#34;&gt;one&lt;/a&gt;, &lt;a href=&#34;https://vishnubharathi.codes/blog/know-when-to-break-up-with-go-http-defaultclient/&#34;&gt;two&lt;/a&gt;) but they refer to &lt;code&gt;Timeout&lt;/code&gt; and &lt;code&gt;MaxIdleConns&lt;/code&gt; settings.&lt;/p&gt;
&lt;p&gt;Today I want to share with you another reason why you should avoid using &lt;code&gt;http.DefaultClient&lt;/code&gt; in your code.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The Internet is plenty of articles that telling why you should not be using <code>http.DefaultClient</code> in Golang (<a href="https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779">one</a>, <a href="https://vishnubharathi.codes/blog/know-when-to-break-up-with-go-http-defaultclient/">two</a>) but they refer to <code>Timeout</code> and <code>MaxIdleConns</code> settings.</p>
<p>Today I want to share with you another reason why you should avoid using <code>http.DefaultClient</code> in your code.</p>
<h2 id="the-story">The Story</h2>
<p>As an SRE at Criteo, I both read and write code. Last week, I worked on patching <a href="https://github.com/updatecli/updatecli">Updatecli</a> — an upgrade automation tool written in Go.</p>
<p>The <a href="https://github.com/updatecli/updatecli/pull/4432">patch</a> itself was just ~15 lines of code. But then I spent three days debugging a strange authorization bug in an unrelated part of the code.</p>
<p>It happened because of code like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">client</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">Transport</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">transport</span><span class="p">.</span><span class="nx">PrivateToken</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Token</span><span class="p">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Token</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Base</span><span class="p">:</span><span class="w">  </span><span class="nx">client</span><span class="p">.</span><span class="nx">Client</span><span class="p">.</span><span class="nx">Transport</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Since <code>http.DefaultClient</code> is a reference, not a value:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">DefaultClient</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">Client</span><span class="p">{}</span><span class="w">
</span></span></span></code></pre></div><p>The code above is effectively the same as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="p">.</span><span class="nx">Transport</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">transport</span><span class="p">.</span><span class="nx">PrivateToken</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Token</span><span class="p">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Token</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Base</span><span class="p">:</span><span class="w">  </span><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="p">.</span><span class="nx">Transport</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Later, in a third-party library, I found this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">opts</span><span class="p">.</span><span class="nx">Client</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">opts</span><span class="p">.</span><span class="nx">Client</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="the-fix">The Fix</h2>
<p>To prevent this, I had to change the code to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">client</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Client</span><span class="p">{}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">Transport</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">transport</span><span class="p">.</span><span class="nx">PrivateToken</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Token</span><span class="p">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">Token</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">Base</span><span class="p">:</span><span class="w">  </span><span class="nx">client</span><span class="p">.</span><span class="nx">Transport</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>As a result, the patched client with the authorization transport got injected into the third-party library, causing unexpected failures.</p>
<p>Bugs like this are hard to catch just by reading the code, since they involve global state mutation. But could they be detected by linters?</p>
<p>What do you think? How do you find or prevent such issues in your projects?</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Forwarding SMS to Telegram</title>
      <link>https://alikhil.dev/posts/forwarding-sms-to-telegram/</link>
      <pubDate>Sat, 26 Oct 2024 14:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/forwarding-sms-to-telegram/</guid>
      <description>&lt;p&gt;After extensive travel, I’ve accumulated several mobile numbers and, naturally, physical SIM cards. Switching them out each time became tedious, even after buying a basic Nokia with two SIM slots, which only helped temporarily. When a friend asked if I could set up a Spanish number for account registrations, I realized it was time to automate the process.&lt;/p&gt;
&lt;p&gt;If you’re dealing with multiple SIM cards and want to receive SMS in Telegram, I have a straightforward approach. You’ll need a Linux machine that’s always online, connected to the internet, and about $10.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>After extensive travel, I’ve accumulated several mobile numbers and, naturally, physical SIM cards. Switching them out each time became tedious, even after buying a basic Nokia with two SIM slots, which only helped temporarily. When a friend asked if I could set up a Spanish number for account registrations, I realized it was time to automate the process.</p>
<p>If you’re dealing with multiple SIM cards and want to receive SMS in Telegram, I have a straightforward approach. You’ll need a Linux machine that’s always online, connected to the internet, and about $10.</p>
<h2 id="what-youll-need">What You’ll Need</h2>
<ul>
<li>A physical SIM card</li>
<li>A USB modem that’s supported by the <a href="https://wammu.eu/phones/">Gammu library</a></li>
<li>A Telegram bot token, chat or channel ID</li>
<li>A Linux machine with a free USB port</li>
<li>Docker and Docker Compose installed</li>
</ul>
<h2 id="finding-the-right-modem">Finding the Right Modem</h2>
<blockquote>
<p>If you have a USB modem at home, check if it’s supported by <a href="https://wammu.eu/phones/">Gammu</a>.</p>
</blockquote>
<p>For our purposes, we don’t need an expensive 4G modem with advanced features. Any basic 2G/3G modem will work, and these are easy to find at a discounted price on sites like eBay or Wallapop.</p>
<p>Search for “Huawei USB modem,” sort by price, and look for unlocked options or ones with compatible firmware.</p>
<p>For instance:
<img loading="lazy" src="/images/posts/sms-to-telegram/ebay.png" type="" alt="Ebay Screenshot"  /></p>
<p>Next, go to the <a href="https://wammu.eu/phones/">Gammu</a> website and look up the device. Make sure it appears on the list and that “SMS” is included in the &ldquo;Supported features&rdquo; column:</p>
<p><img loading="lazy" src="/images/posts/sms-to-telegram/e3131.png" type="" alt="E3131"  /></p>
<p>If the device meets these requirements, it’s good to go!</p>
<h2 id="setup-instructions">Setup Instructions</h2>
<blockquote>
<p>Before starting the setup, it’s best to connect the modem with the SIM card already inserted to your PC and check that it’s functioning properly.</p>
</blockquote>
<h3 id="identify-device-path">Identify Device Path</h3>
<p>Run the following command to identify the device path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tree /dev/serial/by-id/
</span></span></code></pre></div><p>You should see a paths similar to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">/dev/serial/by-id
</span></span><span class="line"><span class="cl">├── usb-HUAWEI_HUAWEI_Mobile-if00-port0 -&gt; ../../ttyUSB0
</span></span><span class="line"><span class="cl">├── usb-HUAWEI_HUAWEI_Mobile-if02-port0 -&gt; ../../ttyUSB1
</span></span><span class="line"><span class="cl">├── usb-HUAWEI_HUAWEI_Mobile-if03-port0 -&gt; ../../ttyUSB2
</span></span></code></pre></div><p>Choose a path that ends with <code>ttyUSB0</code>, in my case it&rsquo;s <code>/dev/serial/by-id/usb-HUAWEI_HUAWEI_Mobile-if00-port0</code>.</p>
<h3 id="running-the-service">Running the Service</h3>
<p>Using Docker Compose, set up your configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">gammu</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/alikhil/sms-to-telegram:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">bind</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/serial/by-id/usb-HUAWEI_HUAWEI_Mobile-if00-port0</span><span class="w"> </span><span class="c"># Change this to your device path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/modem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">privileged</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">BOT_TOKEN=&lt;put your telegram bot token here&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PIN=&lt;your sim card pin&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">CHAT_ID=&lt;telegram chat/channel ID&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DEVICE=/dev/modem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PROTOCOL=at</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">NET_ADMIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">SYS_MODULE</span><span class="w">
</span></span></span></code></pre></div><p>Save the configuration to a <code>docker-compose.yml</code> file and run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs -f gammu
</span></span></code></pre></div><p>If everything is set up correctly, you should see the following log messages:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:50:56 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Created POSIX RW shared memory at 0x7fcf90b21000
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:50:56 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Starting phone communication...
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:30 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Ignoring incoming SMS info as not a Status Report in SR memory.
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Read <span class="m">1</span> messages
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Received IN20241004_195517_00_Celerity_00.txt
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>13<span class="o">]</span>: Starting run on receive: /etc/sms_to_telegram.sh IN20241004_195517_00_Celerity_00.txt
</span></span><span class="line"><span class="cl">gammu-1  <span class="p">|</span> Fri 2024/10/04 17:55:33 gammu-smsd<span class="o">[</span>12<span class="o">]</span>: Process finished successfully
</span></span></code></pre></div><p>To test SMS reception, you can use free online SMS-sending services (search for &ldquo;send SMS for free&rdquo;) or try logging into Telegram, your bank account, etc.</p>
<p><img loading="lazy" src="/images/posts/sms-to-telegram/screenshot.png" type="" alt="Telegram Screenshot"  /></p>
<h2 id="how-it-works">How It Works</h2>
<p>The <a href="https://wammu.eu/libgammu/">Gammu library</a> provides a unified interface for working with phones and modems from various manufacturers.</p>
<p>On top of that, there&rsquo;s the <a href="https://wammu.eu/smsd/">Gammu SMS Daemon</a>, which receives SMS messages and triggers a custom script—in our case, <a href="https://github.com/alikhil/sms-to-telegram/blob/main/sms_to_telegram.sh">script</a> to send the messages to Telegram.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Thanks to <a href="https://github.com/kutovoys">@kutovoys</a> for the idea and <a href="https://github.com/kutovoys/sms-to-telegram">Docker image</a>!</p>
<p>This is a simple, affordable, and scalable solution—especially if you’re into self-hosting.</p>
<blockquote>
<p>This post was originally written for <a href="https://vas3k.club/post/25926/">vas3k.club</a>.</p>
</blockquote>]]></content:encoded>
    </item>
    
  </channel>
</rss>
