<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>MacOS - Kamil Banach</title><link>https://banach.net.pl/tags/macos/</link><description> in MacOS Kamil Banach posts</description><language>en-us</language><copyright>This work is licensed under a CC BY-NC 4.0 License.</copyright><lastBuildDate>Wed, 01 Jul 2026 19:58:23 +0000</lastBuildDate><atom:link href="https://banach.net.pl/tags/macos/index.xml" rel="self" type="application/rss+xml"/><item><title>Building multi-architecture container images on GitLab CI</title><link>https://banach.net.pl/posts/2026/building-multi-architecture-container-images-on-gitlab-ci/</link><pubDate>Wed, 01 Jul 2026 19:58:23 +0000</pubDate><guid>https://banach.net.pl/posts/2026/building-multi-architecture-container-images-on-gitlab-ci/</guid><description>
&lt;p&gt;Turns out that macOS on Apple Silicon is pretty popular right now. Especially among software engineers.
From a ‘works on my machine’ perspective - one operating system among many developers - it greatly simplifies support.
However Apple Silicon is not x86-64, but ARM64.
Fortunately, many of the widely used images, especially published in public registries like Docker Hub, provide variants for different architectures.&lt;/p&gt;
&lt;p&gt;Unfortunately, for internal images, we only had the &lt;code&gt;amd64&lt;/code&gt; version because those were built on the &lt;code&gt;amd64&lt;/code&gt; GitLab runners.
As a result, performance for macOS users was… bad due to emulation.&lt;/p&gt;
&lt;p&gt;While the easiest solution would be to add a new runner with an arm64 processor and build images on it, I wanted to avoid that.
I did not want to spin up a new runner that would be used very rarely (a couple of times a year).
I did not want to build them locally and push them to our registry either - that would be boring and ugly, and require somebody with a dedicated device to build them.
And allowing that person to push images directly to the registry is a no-go from a security perspective as this would bypass the proper code review and CI process.&lt;/p&gt;
&lt;p&gt;So I searched for a solution using emulation.&lt;/p&gt;
&lt;p&gt;And I found one! Using the cross-platform emulator collection distributed using Docker images available at &lt;a href="https://github.com/tonistiigi/binfmt" target="_blank" &gt;tonistiigi/binfmt&lt;/a&gt;. It registers QEMU emulators with the host kernel through &lt;code&gt;binfmt_misc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So, below are the steps needed to run building multi-platform container images in GitLab CI (GitLab Runner with the Docker executor) with &lt;a href="https://buildah.io/" target="_blank" &gt;Buildah&lt;/a&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id="1-install-the-required-emulation-support"&gt;1. Install the required emulation support&lt;/h2&gt;
&lt;p&gt;To install emulation that will be also available to containers, I needed to run (to install only the emulation support for ARM64&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --privileged --rm tonistiigi/binfmt --install arm64
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After that, I verified if everything is working using the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm --platform linux/arm64 alpine uname -m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should print the following:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;aarch64
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that registration is not persisted between reboots. The Docker image does not install any host service to reapply registration on boot.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id="2-update-gitlab-runner-configuration"&gt;2. Update GitLab Runner configuration&lt;/h2&gt;
&lt;p&gt;The next step is to update the configuration of the GitLab Runner using the Docker executor and relax some of its security restrictions.&lt;/p&gt;
&lt;p&gt;Since this weakens container isolation, do it at your own risk.
At minimum, make sure this is a controlled runner that executes only trusted jobs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ...other configuration ommited...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# To allow running buildah in the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;security_opt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;seccomp:unconfined&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;apparmor:unconfined&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Is it 100% secure? Nope.&lt;/p&gt;
&lt;p&gt;However, for our self-hosted runner it was an acceptable trade-off compared to building images manually on a developer&amp;rsquo;s machine.&lt;/p&gt;
&lt;p&gt;Spinning up a dedicated ARM64 runner would provide native builds (which would be faster).
Yet considering that it would be used at most a couple of times a year, it would also add operational overhead and increase infrastructure costs.&lt;/p&gt;
&lt;h2 id="3-configure-the-image-step-using-buildah"&gt;3. Configure the image step using Buildah&lt;/h2&gt;
&lt;p&gt;Last but not least, the a GitLab CI job.
As mentioned at the beginning I used Buildah and its support for building images for different CPU architectures&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I store the images in GitLab Registry, so the first step (in &lt;code&gt;before_script&lt;/code&gt;) is to log in to the registry.
Then the job builds the image for specified CPU platforms and pushes manifest with platform-specific images to the registry.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;images:multiplatform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;images&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;quay.io/buildah/stable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;IMAGE_DIRECTORY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;myimage&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;IMAGE_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;corp/myimage&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;IMAGE_VERSION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;1.2.3&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;STORAGE_DRIVER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;vfs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;BUILDAH_FORMAT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;BUILDAH_ISOLATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;chroot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;before_script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;echo &amp;#34;$CI_REGISTRY_PASSWORD&amp;#34; | buildah login -u &amp;#34;$CI_REGISTRY_USER&amp;#34; --password-stdin &amp;#34;$CI_REGISTRY&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;buildah build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="l"&gt;platform=linux/amd64,linux/arm64&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="l"&gt;manifest &amp;#34;$CI_REGISTRY_IMAGE/${IMAGE_NAME}:${IMAGE_VERSION}&amp;#34; &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${IMAGE_DIRECTORY}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;buildah manifest push --all &amp;#34;$CI_REGISTRY_IMAGE/${IMAGE_NAME}:${IMAGE_VERSION}&amp;#34; &amp;#34;docker://$CI_REGISTRY_IMAGE/${IMAGE_NAME}:${IMAGE_VERSION}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You may ask: why so many variables?
To make the job easy to reuse.
All I need to do to add a new image is to create a new directory with &lt;code&gt;Dockerfile&lt;/code&gt; or &lt;code&gt;Containerfile&lt;/code&gt; and create a new entry in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is all - now all developers can benefit from prebuilt images that have native performance on Apple Silicon. :-)&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Previously I used kaniko, but it is not maintained anymore. There was even a situation where, for a few hours, the images disappeared from the public registry.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;All available emulators can also be installed using &lt;code&gt;all&lt;/code&gt; instead of specifying the architecture.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;This small exercise is left to the reader (yes - you!). :-)&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;As long as the host provides emulation support for a different architecture. More details are available &lt;a href="https://github.com/podman-container-tools/buildah/blob/main/docs/buildah-build.1.md#:~:text=When%20building%20images%20for%20a%20different%20architecture%2C%20the%20RUN%20instructions%20require%20emulation%20software%20installed%20on%20the%20host" target="_blank" &gt;in the documentation&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Using Cloudflare Tunnel to expose a local dev app</title><link>https://banach.net.pl/posts/2026/using-cloudflare-tunnel-to-expose-a-local-dev-app/</link><pubDate>Tue, 06 Jan 2026 20:35:52 +0000</pubDate><guid>https://banach.net.pl/posts/2026/using-cloudflare-tunnel-to-expose-a-local-dev-app/</guid><description>
&lt;p&gt;Since I reinstalled the OS and have fewer apps installed, it is a good moment to rebuild my workflow and drop a couple of old habits.&lt;/p&gt;
&lt;p&gt;Previously, to access the local app from the internet (for example, to receive a webhook), I used the ngrok service&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.
But on a fresh system, I did not have the app installed, so I have looked at alternatives - what I can use instead of it&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.
Especially since I have Podman already installed.&lt;/p&gt;
&lt;p&gt;In the end, I discovered that I can use &lt;a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/" target="_blank" &gt;Cloudflare Tunnel&lt;/a&gt; with containers, so I gave it a spin - and it worked flawlessly!&lt;/p&gt;
&lt;p class="blue-info-box"&gt;
tl;dr: Magic sauce is to set &lt;code&gt;host.containers.internal&lt;/code&gt; (or &lt;code&gt;host.docker.internal&lt;/code&gt; for Docker) to target host.
&lt;/p&gt;
&lt;p&gt;First things first - we need a Cloudflare account with some domain and &lt;a href="https://one.dash.cloudflare.com/" target="_blank" &gt;Cloudflare One&lt;/a&gt;.
Free plan for both is okay.&lt;/p&gt;
&lt;p&gt;Once we have the required accounts, we can navigate to Network -&amp;gt; Connectors in Cloudflare One to create a new tunnel.
Select Cloudflared as a tunnel type (as we need only one direction access - from the internet to our service) and name the new tunnel.&lt;/p&gt;
&lt;p&gt;Then can select the environment as Docker to have a command like this (&lt;code&gt;THE_TOKEN&lt;/code&gt; will be replaced with your token - this is secret, do not share!):
&lt;code&gt;docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token THE_TOKEN&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;We can run it to see if it is working (a message will appear in the UI).
After that, we can go to &amp;lsquo;Route tunnel&amp;rsquo; and &amp;lsquo;Published applications&amp;rsquo; (when we edit, it will be &amp;lsquo;Published application routes&amp;rsquo;) and configure it to your needs.&lt;/p&gt;
&lt;p&gt;In the screenshot below, I have a configuration that will point &lt;code&gt;my-subdomain.example.com&lt;/code&gt; to &lt;code&gt;localhost:5000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/cloudflare-tunnel/published-route.png" alt="Cloudflare Tunnel configuration - Route tunnel with hostname &amp;lsquo;my-subdomain.example.com&amp;rsquo; that points to &amp;lsquo;http://host.containers.internal:5000&amp;rsquo;"&gt;&lt;/p&gt;
&lt;p&gt;And that is all. After starting the container (with a proper token), my service is exposed as needed.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;ngrok is nice, but I wanted to have something without an additional executable installed&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;I could also set up &lt;a href="https://banach.net.pl/posts/2022/how-to-expose-local-service-using-nginx-and-ssh/" &gt;nginx and use SSH for that purpose&lt;/a&gt;, but this time I wanted to not set up anything on the VMs.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Export and import secret GPG keys from backup</title><link>https://banach.net.pl/posts/2026/export-and-import-secret-gpg-keys-from-backup/</link><pubDate>Mon, 05 Jan 2026 12:33:34 +0000</pubDate><guid>https://banach.net.pl/posts/2026/export-and-import-secret-gpg-keys-from-backup/</guid><description>
&lt;p&gt;When installing a new distro&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, I installed it on a new disk - leaving the previous disk as a &amp;lsquo;backup&amp;rsquo;.
What I forgot to do (to be honest, I hadn&amp;rsquo;t thought about it) was exporting the GPG keys that I use, e.g. to sign commits.&lt;/p&gt;
&lt;p&gt;I did not want to copy the &lt;code&gt;.gnupg&lt;/code&gt; directory from the old disk, as I imported a lot of public keys for example, to verify downloaded files.
I no longer need them - I want only my private keys.&lt;/p&gt;
&lt;p&gt;I figured out a way to export them from the backup and import them to the local keyring. Below are the steps and the explanation. :-)&lt;/p&gt;
&lt;p class="blue-info-box"&gt;
tl;dr: the magic sauce is &lt;code&gt;--homedir&lt;/code&gt; parameter.
It tells GnuPG to use that directory as its home (keyring, config, etc.) instead of the default one.
This way, we can point GnuPG at the backed-up files and use them as if they were in place for a regular setup.
&lt;/p&gt;
&lt;h3 id="1-copy-from-the-backup-to-the-home-directory"&gt;1. Copy from the backup to the home directory&lt;/h3&gt;
&lt;p&gt;Copy the &lt;code&gt;.gnupg&lt;/code&gt; directory from backup to the home directory.
This is to ensure that these will not be overridden by mistake.&lt;/p&gt;
&lt;p&gt;For the latter steps, I assume that those are copied to &lt;code&gt;~/gnupg/.gnupg&lt;/code&gt;
(&lt;code&gt;gnupg&lt;/code&gt; is a directory in the home directory).&lt;/p&gt;
&lt;h3 id="2-verify-that-keys-are-there"&gt;2. Verify that keys are there&lt;/h3&gt;
&lt;p&gt;Then verify that the secret keys are there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gpg --homedir&lt;span class="o"&gt;=&lt;/span&gt;~/gnupg/.gnupg --list-secret-keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should show secret keys that are available in the keyring.&lt;/p&gt;
&lt;h3 id="3-export-keys-from-the-backup"&gt;3. Export keys from the backup&lt;/h3&gt;
&lt;p&gt;After that, I can export them to a file &lt;code&gt;keys.key&lt;/code&gt; in the current directory.
A passphrase for each private key needs to be provided for successful export.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gpg --homedir&lt;span class="o"&gt;=&lt;/span&gt;~/gnupg/.gnupg --export-secret-keys &amp;gt; keys.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-import-keys-from-the-file"&gt;4. Import keys from the file&lt;/h3&gt;
&lt;p&gt;As those are exported, they can now be imported. Same as previously, passphrases need to be provided:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gpg --import keys.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every key will be printed out (public and secret pair) with the summary. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;(...details are removed ;-)...)
gpg: Total number processed: 3
gpg: imported: 3
gpg: secret keys read: 3
gpg: secret keys imported: 3
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="5-verify-and-cleanup"&gt;5. Verify and cleanup&lt;/h3&gt;
&lt;p&gt;To verify, we can run &lt;code&gt;gpg --list-secret-keys&lt;/code&gt; - it should print out the details of the keys that we just imported. As for cleanup - remove the copied data and &lt;code&gt;keys.key&lt;/code&gt; file - those are not needed anymore!&lt;/p&gt;
&lt;p&gt;And that is all - now I can use my GPG keys to sign Git commits, just like I did on the previous machine!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I installed Fedora KDE 43. I must say that it works quite well!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Fixing suspend after login in KDE in clamshell mode</title><link>https://banach.net.pl/posts/2025/fixing-suspend-after-login-in-kde-in-clamshell-mode/</link><pubDate>Tue, 30 Dec 2025 19:55:27 +0000</pubDate><guid>https://banach.net.pl/posts/2025/fixing-suspend-after-login-in-kde-in-clamshell-mode/</guid><description>
&lt;p&gt;My working setup is basically clamshell mode - the laptop is closed and placed vertically, with two external monitors, a mouse, and a keyboard.&lt;/p&gt;
&lt;p&gt;On a fresh Fedora KDE 43 install, this caused a weird issue - right after logging in, the system immediately suspends, and I have to wake it up.
Tweaking the UI settings (Power Management) did not resolve the issue.&lt;/p&gt;
&lt;p&gt;I tried a bunch of solutions I found online, but most of them did not work. The main reason was that many comments pointed to &lt;code&gt;/etc/systemd/logind.conf&lt;/code&gt;, but on my setup, that file did not exist. The default config lives under &lt;code&gt;/usr/lib/systemd/logind.conf&lt;/code&gt;, and the recommended way to override it is via a drop-in in &lt;code&gt;/etc/systemd/logind.conf.d/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The working solution is to create &lt;code&gt;/etc/systemd/logind.conf.d/ignore-lid-switch.conf&lt;/code&gt; and add the following lines:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Login]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;HandleLidSwitch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;HandleLidSwitchExternalPower&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;HandleLidSwitchDocked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While searching, I also found a neat way to inspect the effective systemd configuration: &lt;code&gt;systemd-analyze&lt;/code&gt;.
For example, to check what the configuration is for &lt;code&gt;logind.conf&lt;/code&gt;, I can run &lt;code&gt;systemd-analyze cat-config systemd/logind.conf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And that is all - after restarting &lt;code&gt;systemd-logind&lt;/code&gt; or rebooting, everything works as expected.&lt;/p&gt;</description></item><item><title>Adding SSH keys on autostart in KDE (for IntelliJ)</title><link>https://banach.net.pl/posts/2025/adding-ssh-keys-on-autostart-in-kde-for-intellij/</link><pubDate>Mon, 29 Dec 2025 11:09:03 +0000</pubDate><guid>https://banach.net.pl/posts/2025/adding-ssh-keys-on-autostart-in-kde-for-intellij/</guid><description>
&lt;p&gt;Recently, I wanted to give Fedora (KDE flavour) a try, so I installed it on a new disk.
After copying all dotfiles (including SSH keys and Git configuration&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;),
turning on saving SSH keys passwords to KWallet (and unblocking it on login) and checking if everything works - and it worked flawlessly&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; - I turned off the PC.&lt;/p&gt;
&lt;p&gt;The next day, as soon as I wanted to push the changes to the project, I was greeted by an unexpected pop-up:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/add-ssh-keys-on-login/intellij-ssh-passphrase.png" alt="Intellij popup to enter SSH key passphrase for ~/.ssh/id_rsa"&gt;&lt;/p&gt;
&lt;p&gt;The what? I have checked git from the console - everything is working as expected.
So what happened?
Turns out that IntelliJ (along with running git) expects the key to be loaded, but it is not.
And it cannot load it without providing a password - it does not use the &amp;lsquo;ask pass&amp;rsquo; tool (in my case - &lt;code&gt;ksshaskpass&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;So I decided to fix it in the simplest way ever - by adding a small script to autostart that will add keys that I expect to be loaded.
Adding them works without providing passwords, as those are provided by KWallet and &lt;code&gt;ksshaskpass&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To do so, I did the following - first, I created a directory to store all my &amp;lsquo;autostart scripts&amp;rsquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create .run directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p ~/.run/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I have created the script &lt;code&gt;add-ssh-keys.sh&lt;/code&gt; in &lt;code&gt;.run&lt;/code&gt; using following snippet&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Add create add-ssh-keys.sh to add ssh keys&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF &amp;gt; ~/.run/add-ssh-keys.sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SSH_ASKPASS=/usr/bin/ksshaskpass
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;ssh-add ~/.ssh/id_ed25519 &amp;lt;/dev/null 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By default, it adds only the &lt;code&gt;.ssh/id_ed25519&lt;/code&gt; key, but it can have more lines - just add another &lt;code&gt;ssh-add ~/.ssh/file_with_private_key&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To make sure that it will be runnable, let&amp;rsquo;s make it executable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x ~/.run/add-ssh-keys.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After creating the script using the code above, I can add it to autostart using Settings → Autostart (using &amp;lsquo;Login Script&amp;hellip;&amp;rsquo; option):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/add-ssh-keys-on-login/kde-autostart.png" alt="KDE Settings - Autostart with opened menu to Add New"&gt;&lt;/p&gt;
&lt;p&gt;And that is all. After the login, the SSH keys that I want to be available are added (and passwords are fetched from KWallet) to the SSH agent.
Thanks to that, IntelliJ is not asking me for a password anymore!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;My Git configuration uses subfolders to select proper emails and user names - all described in &lt;a href="https://banach.net.pl/posts/2020/git-config-based-on-current-subfolder/" &gt;one of the previous posts&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;I&amp;rsquo;m quite amazed how nice it looks and how smoothly Fedora KDE works!&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Snipped uses &lt;code&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; file&lt;/code&gt; that allows putting multiline data into a file - all content until &lt;code&gt;EOF&lt;/code&gt; is put into the file. Which is nice.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Creating bucket automatically on local MinIO with Docker Compose</title><link>https://banach.net.pl/posts/2025/creating-bucket-automatically-on-local-minio-with-docker-compose/</link><pubDate>Tue, 01 Apr 2025 20:15:00 +0100</pubDate><guid>https://banach.net.pl/posts/2025/creating-bucket-automatically-on-local-minio-with-docker-compose/</guid><description>
&lt;p&gt;I&amp;rsquo;m a fan of making things easier. If something can be automated, we should do it, as it simplifies things and prevents errors.
And questions like &amp;lsquo;why it is not working?&amp;rsquo; or &amp;lsquo;have you set up X/Y/Z?&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s see how ~10 lines of YAML in Docker Compose can simplify configuring (creating buckets and directories) local S3-compatible storage!&lt;/p&gt;
&lt;p class="blue-info-box"&gt;
You can omit the step-by-step description and go &lt;a href="#final-docker-compose"&gt;straight to the final code&lt;/a&gt;.
&lt;/p&gt;
&lt;h3 id="base-docker-compose"&gt;Base Docker Compose&lt;/h3&gt;
&lt;p&gt;For the S3-compatible storage, my friends selected MinIO, as it works out of the box for the simple case.
The Docker Compose file with configuration looked like this&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; (yes, the user and password are so obvious!):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;## Below &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;minio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;quay.io/minio/minio:RELEASE.2025-03-12T18-04-18Z&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:9900:9000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:9901:9001&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--console-address&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;:9001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/data&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;MINIO_ROOT_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;minioAccessKey&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;MINIO_ROOT_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;minioSecretKey&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, after starting it up, the creation of the buckets was manual.
Which takes time and is easily to forget about - until you want to run stuff and it fails to work due to lack of the bucket.&lt;/p&gt;
&lt;p&gt;I was pretty sure that it is possible to automate (and did not want to create the buckets from the app code).
And indeed it is - there is a &lt;code&gt;mc&lt;/code&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; (which seems like an abbreviation of &lt;strong&gt;M&lt;/strong&gt;inIO &lt;strong&gt;C&lt;/strong&gt;lient - its name).
The question remains - how to run it locally, after the start of storage?&lt;/p&gt;
&lt;p&gt;Fortunately &lt;code&gt;mc&lt;/code&gt; has its own docker image available and I can use &lt;code&gt;depends_on&lt;/code&gt; to make sure that it will start after the storage is set up.&lt;/p&gt;
&lt;h3 id="service-configuration-for-creating-buckets"&gt;Service configuration for creating buckets&lt;/h3&gt;
&lt;p&gt;The below configuration creates a new service &lt;code&gt;createbuckets&lt;/code&gt; that depends on &lt;code&gt;minio&lt;/code&gt; service.
It has overwritten &lt;code&gt;entrypoint&lt;/code&gt;&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; that runs command (using &lt;code&gt;bin/sh -c&lt;/code&gt;) that does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sleeps for 5 seconds - just to make sure that &lt;code&gt;minio&lt;/code&gt; service started;&lt;/li&gt;
&lt;li&gt;adds configuration for the local MinIO;&lt;/li&gt;
&lt;li&gt;creates bucket &lt;code&gt;my-bucket&lt;/code&gt; with &lt;code&gt;some-directory&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;creates bucket &lt;code&gt;my-other-bucket&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;exits with &lt;code&gt;0&lt;/code&gt; - exit code that means basically &amp;ldquo;ok, exited with success&amp;rdquo;;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Addiotionally if it fails (so exit with non-0 exit code) it will restart, thanks to &lt;code&gt;on-failure&lt;/code&gt; restart policy.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# &amp;lt;chomp&amp;gt; - here is the above mino service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;createbuckets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;quay.io/minio/mc:RELEASE.2025-03-12T17-29-24Z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;minio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;on&lt;/span&gt;-&lt;span class="l"&gt;failure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;entrypoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /bin/sh -c &amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; sleep 5;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /usr/bin/mc alias set dockerminio http://minio:9000 minioAccessKey minioSecretKey;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /usr/bin/mc mb dockerminio/my-bucket/some-directory;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /usr/bin/mc mb dockerminio/my-other-bucket;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; exit 0;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And basically, that is all that is required to create the buckets and folders within them.
&lt;code&gt;mc&lt;/code&gt; tool can be also used with other S3-compatible storages.&lt;/p&gt;
&lt;h3 id="final-docker-compose"&gt;Final Docker Compose&lt;/h3&gt;
&lt;p&gt;The final Docker Compose looks like this - feel free to copy and adjust. ;-)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;minio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;quay.io/minio/minio:RELEASE.2025-03-12T18-04-18Z&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:9900:9000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:9901:9001&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--console-address&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;:9001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/data&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;MINIO_ROOT_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;minioAccessKey&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;MINIO_ROOT_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;minioSecretKey&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;createbuckets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;quay.io/minio/mc:RELEASE.2025-03-12T17-29-24Z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;minio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;on&lt;/span&gt;-&lt;span class="l"&gt;failure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;entrypoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /bin/sh -c &amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; sleep 5;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /usr/bin/mc alias set dockerminio http://minio:9000 minioAccessKey minioSecretKey;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /usr/bin/mc mb dockerminio/my-bucket/some-directory;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /usr/bin/mc mb dockerminio/my-other-bucket;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; exit 0;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After starting it with &lt;code&gt;docker compose up&lt;/code&gt; it begins &lt;code&gt;minio&lt;/code&gt; service, then &lt;code&gt;createbuckets&lt;/code&gt; which waits 5 seconds and creates two buckets - &lt;code&gt;my-bucket&lt;/code&gt; with directory &lt;code&gt;some-directory&lt;/code&gt; and &lt;code&gt;my-other-bucket&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/creating-bucket-automatically-on-local-minio-with-docker-compose/result.png" alt="Screenshot from console after running &amp;lsquo;docker compose up&amp;rsquo; - MinIO started and buckets are created"&gt;&lt;/p&gt;
&lt;p&gt;That is all - small configuration (a little more than 10 lines of code), but great improvement in setting up local environment!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Remember to put the hostname in port mappings, otherwise it will open this port in your firewall! For details see &lt;a href="https://docs.docker.com/engine/network/#published-ports" target="_blank" &gt;Networking overview
→ Published ports manual page&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;A pretty nice tool to work with the s3-compatible storage like MinIO, documentation available here: &lt;a href="https://min.io/docs/minio/linux/reference/minio-mc.html" target="_blank" &gt;https://min.io/docs/minio/linux/reference/minio-mc.html&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Overriding entrypoint allows us to run anything we want in the context of the Docker image.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Restricting access to specific Docker images in Nexus</title><link>https://banach.net.pl/posts/2024/restricting-access-to-specific-docker-images-in-nexus/</link><pubDate>Thu, 21 Nov 2024 12:32:00 +0100</pubDate><guid>https://banach.net.pl/posts/2024/restricting-access-to-specific-docker-images-in-nexus/</guid><description>
&lt;p&gt;Once upon a time&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, the company I work for decided to migrate all Docker images from a hosted registry to a self-hosted one.
The main requirement, in line with the Principle of Least Privilege&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;, was to restrict service accounts to access only their designated images.
This ensures that every project could only access its own images.&lt;/p&gt;
&lt;p&gt;Since we were already using Nexus &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; as our private NPM registry, we decided to configure it for this use case as well.
I had also configured something similar before, using the raw repository type.
In the mentioned case, we published runnable JARs (builds per customer) to Nexus and allowed end users to download them with dedicated accounts&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Setting up a new registry was straightforward, but restricting access to specific images required more effort - in fact a deeper understanding of Content Selectors&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id="steps-and-assumptions"&gt;Steps and assumptions&lt;/h2&gt;
&lt;p&gt;The steps below assume that we have admin access to Nexus and a working Docker registry:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configure Content Selectors for login and image access.&lt;/li&gt;
&lt;li&gt;Create privileges based on the created Content Selectors.&lt;/li&gt;
&lt;li&gt;Create the role with new privileges and apply them to the user.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And that’s it! :-)&lt;/p&gt;
&lt;h2 id="1-configuring-content-selectors-for-docker-login-and-accessing-image"&gt;1. Configuring Content Selectors for Docker Login and accessing image&lt;/h2&gt;
&lt;p&gt;To achieve this, we need to configure Content Selectors&lt;sup id="fnref1:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; that will allow us to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use the &lt;code&gt;docker login&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;pull only the selected images (any tag).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To allow logging into the repo we will need the following content selector - it matches the path that is used by the &lt;code&gt;docker login&lt;/code&gt; command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;path == &amp;#34;/v2/&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the content selector could look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/nexus/csel-docker-login.png" alt="Docker Login - Content Selector in Nexus Sonatype Repository OSS"&gt;&lt;/p&gt;
&lt;p&gt;Apart from allowing us to log in, we need to allow access to selected images.
To allow all images in the following path we will use &lt;code&gt;=^&lt;/code&gt;&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt; to match all paths that start. Assuming that our image is &lt;code&gt;sonatype/nexus3&lt;/code&gt; then we need to create a selector:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;path =^ &amp;#34;/v2/sonatype/nexus3&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which translates to the following Content Selector:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/nexus/csel-access-to-image.png" alt="Access to sonatype/nexus3 - Content Selector in Nexus Sonatype Repository OSS"&gt;&lt;/p&gt;
&lt;p&gt;We could merge both into the one with the &lt;code&gt;or&lt;/code&gt; operator.
Keeping them separate allows the first selector (for login) to be reused multiple times.&lt;/p&gt;
&lt;h2 id="2-creating-privileges"&gt;2. Creating privileges&lt;/h2&gt;
&lt;p&gt;As a next step, we need to create new privileges - each for the newly created content selector.
Both privileges should be configured for the &lt;code&gt;read&lt;/code&gt; action, as we only need read-only access.&lt;/p&gt;
&lt;p&gt;In the screenshots below I select my Docker registry with the name &lt;code&gt;docker&lt;/code&gt;, but nothing stops us from selecting here &lt;code&gt;(All docker repositories)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/nexus/privilege-docker-login.png" alt="Access to docker login - Privilege in Nexus Sonatype Repository OSS"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/nexus/privilege-access-to-image.png" alt="Access to sonatype/nexus - Privilege in Nexus Sonatype Repository OSS"&gt;&lt;/p&gt;
&lt;p&gt;After that, we can go to the last step which is&amp;hellip; creating a role and assigning it to our service account!&lt;/p&gt;
&lt;h2 id="3-creating-the-role-and-assigning-it-to-the-user"&gt;3. Creating the role and assigning it to the user&lt;/h2&gt;
&lt;p&gt;Now we will create the role (with type &lt;code&gt;Nexus role&lt;/code&gt;) and we will apply previously created privileges:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/nexus/docker-role-create.png" alt="Role creation in Sonatype Nexus Repository OSS - contains two privileges created in previous steps"&gt;&lt;/p&gt;
&lt;p&gt;Then, cherry on the top, we will add that role to our service account:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/nexus/docker-role-assign.png" alt="Added role with name &amp;lsquo;docker-access-sonatype-nexus3&amp;rsquo; to service account with name &amp;lsquo;k8s-sa&amp;rsquo;"&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all. Now we can grab the login and password for our service account, log in and pull the images.
Which is perfect for pulling images from the registry on the Kubernetes cluster. :-)&lt;/p&gt;
&lt;h2 id="end-notes"&gt;End notes&lt;/h2&gt;
&lt;p&gt;In a few steps, we have configured limited access to selected Docker images in our self-hosted registry.
Even if that solution was nothing extraordinary, it allowed us to migrate from the hosted repository and have more control over image access.&lt;/p&gt;
&lt;p&gt;Worth noting that it was a prerequisite to set up Nexus as a proxy in front of our GitLab instance - but this is the story for another time.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;6 months ago, but in IT it is like decades, riiight?&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Principe Of Least Privilege means that the access shall be as limited as can be.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Yes, &lt;a href="https://www.sonatype.com/products/sonatype-nexus-oss" target="_blank" &gt;Sonatype Nexus Repository OSS&lt;/a&gt;, as it is free to use and allows to store maven and NPM artifacts!&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;code&gt;raw&lt;/code&gt; repository as we do not want to expose the Maven repo structure. In the end, we dropped that way of distribution as we moved to providing Docker images.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;The documentation for Content Selectors can be found &lt;a href="https://help.sonatype.com/en/content-selectors.html" target="_blank" &gt;here&lt;/a&gt;&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;That operator &lt;code&gt;=^&lt;/code&gt; looks like an emoticon, isn&amp;rsquo;t it?&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Exposing local app (like Home Assistant) with autossh</title><link>https://banach.net.pl/posts/2024/exposing-local-app-like-home-assistant-with-autossh/</link><pubDate>Fri, 09 Feb 2024 23:20:00 +0100</pubDate><guid>https://banach.net.pl/posts/2024/exposing-local-app-like-home-assistant-with-autossh/</guid><description>
&lt;p&gt;For the last 3 years to manage my &amp;lsquo;smart home&amp;rsquo; (a couple of lights, heating, some sensors) I have been using Home Assistant running on Dell Wyse Z90D7&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.
And to be honest - it is a great piece of code which works without issues since then.
But after the month of using it&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; I wanted to have it available through a mobile app - for example, to turn on the heating before I come back.
Or make sure that, after leaving for groceries, a bathroom door is open so my cat can use his litter box. ;-)&lt;/p&gt;
&lt;p&gt;I looked over the ways to expose Home Assistant online and found some services&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;, but in the end decided to make something my own.
To do it I decided to use the things I already know and have - SSH, nginx and remote server in the cloud.&lt;/p&gt;
&lt;p&gt;Despite the fact I wanted to tinker with it by myself I was lazy and searched for tools that would allow me to maintain a stable SSH tunnel.
After a short research, I found one that fits perfectly my needs - &lt;code&gt;autossh&lt;/code&gt;&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;autossh&lt;/code&gt; (that runs on top of the SSH) I can be sure that the SSH tunnel will be there.
Even if the terminal restarts or it will lose network for a couple of minutes (or days) - &lt;code&gt;autossh&lt;/code&gt; will restart the connection.&lt;/p&gt;
&lt;p&gt;So I set it up and connected all the required stuff and it has worked without any issues&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; since then.&lt;/p&gt;
&lt;p&gt;So, no more introduction - below are step-by-step instructions on how to do it by yourself!&lt;/p&gt;
&lt;h2 id="steps-outline-and-assumptions"&gt;Steps outline and assumptions&lt;/h2&gt;
&lt;p&gt;To access service using SSH tunnel we will need a service and remote server that allow us&lt;/p&gt;
&lt;p&gt;Exposing the application is pretty straightforward and can be described in a couple of steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;install &lt;code&gt;autossh&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;create a user on the local server (this is the server you have at home) and create a pair of SSH keys,&lt;/li&gt;
&lt;li&gt;create a user on the remote server (this is the server on the public internet, like VPS) and add the previously created key as an authorized one,&lt;/li&gt;
&lt;li&gt;test SSH connection,&lt;/li&gt;
&lt;li&gt;create a service to run and restart &lt;code&gt;autossh&lt;/code&gt; on the local server,&lt;/li&gt;
&lt;li&gt;configure reverse proxy on the remote server.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the post, I assume the Linux distribution is Ubuntu Server (both local and remote server) and you have root rights on it (to configure accounts and reverse proxy).&lt;/p&gt;
&lt;h3 id="1-install-autossh"&gt;1. Install &lt;code&gt;autossh&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Installing &lt;code&gt;autossh&lt;/code&gt; on Ubuntu is straightforward:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ sudo apt install autossh
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2-terminal-create-user-and-key-pair"&gt;2. Terminal: Create user and key pair&lt;/h3&gt;
&lt;p&gt;Create a new account with username &lt;code&gt;autotunnel&lt;/code&gt; on the local server, then log in to this account:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo useradd -m -s /bin/bash autotunnel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo su -l autotunnel
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then log in to it on the server and create a new SSH key pair - it needs to be created without a password:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ssh-keygen -t ed25519 -C &lt;span class="s2"&gt;&amp;#34;autotunnel&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With default options public key will be by default saved in &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="3-remote-create-a-user-and-add-the-public-key-to-the-authorized-ones"&gt;3. Remote: Create a user and add the public key to the authorized ones&lt;/h3&gt;
&lt;p&gt;Commands as above:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo useradd -m -s /bin/bash autotunnel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo su -l autotunnel
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After logging in you need to copy the value from terminal &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; into remote &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;, and set proper permission to that file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ssh-ed25519 AAAAlotofletters autotunnel@tarkin&amp;#34;&lt;/span&gt; &amp;gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-test-connection"&gt;4. Test connection!&lt;/h3&gt;
&lt;p&gt;After setting accounts and keys you need to test the connection. You can do that by simply connecting using SSH from the local server to the remote server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ssh autotunnel@remote-tld
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you are trying to log in for the first time - and as this is a fresh account it probably will be the first time - you will need to
confirm the authenticity of the remote host:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;The authenticity of host &amp;#39;remote-server.example.com (XX.XX.XX.XX)&amp;#39; can&amp;#39;t be established.
ED25519 key fingerprint is SHA256:anotherAlotOfLetters.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Validate the key &lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt; and if it is correct - continue connecting.
After a couple of seconds, you should be logged into the server.&lt;/p&gt;
&lt;p&gt;If not - something bad happens or something is misconfigured - for sure it is worth checking if the SSH server allows authenticating with public keys!&lt;/p&gt;
&lt;h3 id="5-create-a-service-to-run-and-manage-the-connection"&gt;5. Create a service to run and manage the connection&lt;/h3&gt;
&lt;p&gt;To make sure that the connection will start on the local server restart we can create a service that will be enabled and started.
To edit file &lt;code&gt;/etc/systemd/system/autotunnel.service&lt;/code&gt; (&lt;code&gt;$ sudo nano /etc/systemd/system/autotunnel.service&lt;/code&gt;) and type the following content into it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-service" data-lang="service"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Keep a tunnel to &amp;#39;remote-server.example.com&amp;#39; to expose service from localhost:8123&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;autotunnel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/autossh -f -M 8124 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -N -R 32100:localhost:8123 autotunnel@remote-server.example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/pkill -9 -u autotunnel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The above configuration assumes that we will run the service with user &lt;code&gt;autotunnel&lt;/code&gt;.
Because you already tested the connection and the key does not require a password there are no additional, manual, steps needed to make a connection.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;autossh&lt;/code&gt; in the same way as plain &lt;code&gt;ssh&lt;/code&gt; will create a tunnel from port &lt;code&gt;32100&lt;/code&gt; on &lt;code&gt;remote-server.example.com&lt;/code&gt; to local &lt;code&gt;8123&lt;/code&gt; port
(it is defined by &lt;code&gt;-R 32100:localhost:8123&lt;/code&gt; part of &lt;code&gt;ExecStart&lt;/code&gt;).
The only (but crucial!) difference is that it will make sure that the connection will be there.
If your local server suddenly becomes unavailable then it will try, continuously, to connect again.&lt;/p&gt;
&lt;p&gt;Then reload services, enable and run service type:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; autotunnel.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo systemctl start autotunnel.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After a couple of seconds, we should be able to see that the service is running by typing &lt;code&gt;$ sudo systemctl status autotunnel.service&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="6-reverse-proxy"&gt;6. Reverse proxy&lt;/h3&gt;
&lt;p&gt;To expose the service on the remote server we can use for example Caddy or nginx.
For the latter, the full example is available in a post &lt;a href="https://banach.net.pl/posts/2022/how-to-expose-local-service-using-nginx-and-ssh/" &gt;How to expose local service using nginx and SSH&lt;/a&gt;.
The only required change is the port change - in the linked example we are using &lt;code&gt;8080&lt;/code&gt; and above we need &lt;code&gt;32000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In the case of the Home Assistant, it is required to allow Internet access in &lt;code&gt;System -&amp;gt; Network&lt;/code&gt; based on the external domain and port (configured e.g. with nginx as above).&lt;/p&gt;
&lt;h2 id="end-notes"&gt;End notes&lt;/h2&gt;
&lt;p&gt;And that is all - thanks to that solution I can expose any local service (like the mentioned Home Assistant) without requiring static IP, dynamic DNS and so on.
I am very happy to have the possibility to turn on the heating before coming home or have an instant notification about water leaks.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;A beast with an AMD processor, 4GB of RAM and 120GB SSD! But it cost me less than 200 Złoty (~40 Euro).&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;That is correct - writing this post took me&amp;hellip; a few hours typing and three years as an &amp;lsquo;&lt;em&gt;I should write about that&lt;/em&gt;&amp;rsquo; idea.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;For Home Assistant there is a service provided by the creators of Home Assistant called &lt;a href="https://www.nabucasa.com/" target="_blank" &gt;Nabu Casa&lt;/a&gt;. If you want to offload this to somebody and support the authors - this is the way to go!&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Description, download and other stuff available &lt;a href="https://www.harding.motd.ca/autossh/" target="_blank" &gt;here&lt;/a&gt;. It should be also available in the repository of any Linux distro.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;Well, the remote server (VPS) is hosted by OVH and was turned off during the fire in a data centre (SBG), but that is &lt;em&gt;a force majeure&lt;/em&gt;.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;For example using the command &lt;code&gt;ssh-keygen -lf &amp;lt;(ssh-keyscan localhost 2&amp;gt;/dev/null)&lt;/code&gt; on the remote server and verifying the checksum (source: &lt;a href="https://unix.stackexchange.com/a/268692" target="_blank" &gt;StackExchange&lt;/a&gt;)&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>When JavaScript regex can surprise you - a silly bug story</title><link>https://banach.net.pl/posts/2023/when-javascript-regex-can-surprise-you-a-silly-bug-story/</link><pubDate>Wed, 15 Feb 2023 10:25:00 +0100</pubDate><guid>https://banach.net.pl/posts/2023/when-javascript-regex-can-surprise-you-a-silly-bug-story/</guid><description>
&lt;p&gt;Recently one of my customers let me know that they have probably a bug in their app.
They want me to look into it - so I jump into &amp;ldquo;Sherlock, the bug hunter&amp;rdquo; mode and start snooping!&lt;/p&gt;
&lt;p&gt;The misbehaving feature was some kind of search.
It takes an input, a Javascript regular expression and as an output returns &lt;em&gt;true&lt;/em&gt; if regex was satisfied - and &lt;em&gt;false&lt;/em&gt; otherwise.&lt;/p&gt;
&lt;p&gt;The feature was pretty simple on the code side - the search function looked similar to the one below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So we can see that the heart of it was a simple &lt;code&gt;regex.test&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;The bug report stated that misbehaving example was regex &lt;code&gt;/pony/g&lt;/code&gt; and text &lt;code&gt;this is a pony and not a horse&lt;/code&gt; - we have 100% sure that &lt;code&gt;pony&lt;/code&gt; is in this sentence.
To check if everything was working correctly I opened console in Chrome Developer Tools, loaded the search function and check its result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Below returns true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/pony/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;this is a pony and not a horse&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// this also returns true!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/pony/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;this is a pony and not a horse&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ... five runs later
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// eh? still returns true!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/pony/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;this is a pony and not a horse&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What is going on?!
Every run of this function returns the correct and expected value.&lt;/p&gt;
&lt;p&gt;But wait&amp;hellip; the &lt;code&gt;regex&lt;/code&gt; and &lt;code&gt;text&lt;/code&gt; was passed using variables from &lt;em&gt;somewhere else&lt;/em&gt;.
Let&amp;rsquo;s put them in the variables and try to run the function multiple times again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/pony/g&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;this is a pony and not a horse&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pony&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returns true as expected
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pony&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returns false :O
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pony&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returns true - what a trickery!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pony&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returns false - wut?!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now we have a problem - despite the fact we should get &lt;code&gt;true&lt;/code&gt; every time we sometimes get &lt;code&gt;false&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;The answer to this weird behaviour is in the&amp;hellip; documentation.
Quick look at the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test" target="_blank" &gt;RegExp.prototype.test() on mdn web docs&lt;/a&gt;
and we have found the culprit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JavaScript RegExp objects are stateful when they have the global or sticky flags set (e.g., &lt;code&gt;/foo/g&lt;/code&gt; or &lt;code&gt;/foo/y&lt;/code&gt;). They store a &lt;code&gt;lastIndex&lt;/code&gt; from the previous match&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And the &lt;code&gt;latIndex&lt;/code&gt; property is described as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;lastIndex&lt;/code&gt; data property of a RegExp instance specifies the index at which to start the next match.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We have the global option in our regex - and that was the reason that it behaves in a not expected manner.
Similar results will be if our test text were &lt;em&gt;this is a pony and second pony&lt;/em&gt; - it will give us &lt;code&gt;true&lt;/code&gt; for the first and the second search (as we have two ponies!) and &lt;code&gt;false&lt;/code&gt; for the third one.&lt;/p&gt;
&lt;p&gt;The solution for this issue was simply to reset the &lt;code&gt;lastIndex&lt;/code&gt; property of regex if something was found&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// reset lastIndex property to make sure
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// that next test will start from beginning
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s all folks!
One line, resetting one property, is all that was needed to fix the issue.&lt;/p&gt;
&lt;p&gt;What is the takeaway from this issue?
When in doubt and especially when working with regexes - read documentation.
It can save you a lot of time. :-)&lt;/p&gt;</description></item><item><title>Accessing Kubernetes cluster using SSH tunnel</title><link>https://banach.net.pl/posts/2022/accessing-kubernetes-cluster-using-ssh-tunnel/</link><pubDate>Mon, 12 Dec 2022 20:35:00 +0100</pubDate><guid>https://banach.net.pl/posts/2022/accessing-kubernetes-cluster-using-ssh-tunnel/</guid><description>
&lt;p&gt;Whether we want it or not adoption of Kubernetes is growing.
It can be set up as a managed solution (all major cloud providers provide such products) or we can set it up by yourselves.
No matter if we select the former or the latter - we would like to make it as secure as it can be.&lt;/p&gt;
&lt;p&gt;One of the solutions to make the Kubernetes cluster more secure is to hide a control plane (to be more specific - &lt;code&gt;kube-apiserver&lt;/code&gt;) behind a firewall.
That means cluster management is not available from the Internet.&lt;/p&gt;
&lt;p&gt;That creates problems with accessing it.
We can SSH to a server that is in the same network and run &lt;code&gt;kubectl&lt;/code&gt; commands from there, but this is a nuisance which we want to avoid.
Fortunately, SSH tunnels came with help in that case (&lt;a href="https://banach.net.pl/posts/2022/how-to-expose-local-service-using-nginx-and-ssh/" &gt;not for the first time!&lt;/a&gt;) -
we can create a tunnel to the server in the same network and pass all traffic to the cluster through it!&lt;/p&gt;
&lt;p&gt;To do it we can run the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ssh our-gate.example.com -L 16443:10.0.10.2:443
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the command, we&amp;rsquo;re assuming that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;our server is available with the domain &lt;code&gt;our-gate.example.com&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;the cluster is in the same local network as the server and its control plane is available at &lt;code&gt;10.0.10.2&lt;/code&gt; IP address,&lt;/li&gt;
&lt;li&gt;we want the tunnel to be available at port &lt;code&gt;16443&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we need to replace IP address / hostname of cluster in &lt;code&gt;~/.kube/config&lt;/code&gt; to point to local tunnel.
To do this we will open aforementioned file and replace put &lt;code&gt;https://127.0.0.1:16443&lt;/code&gt; in &lt;code&gt;clusters.cluster.server&lt;/code&gt; property:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;certificate-authority-data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;long string with CA data&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# line below was something like https://our-public-address-of-cluster.example.com or just local IP like https://10.0.10.2 &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://127.0.0.1:16443 &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-k8s-cluster&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After that we can try to run a command like &lt;code&gt;kubectl cluster-info&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~ &amp;gt; kubectl cluster-info
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To further debug and diagnose cluster problems, use &lt;span class="s1"&gt;&amp;#39;kubectl cluster-info dump&amp;#39;&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Unable to connect to the server: x509: certificate is valid &lt;span class="k"&gt;for&lt;/span&gt; our-public-address-of-cluster.example.com, 10.0.10.2, not 127.0.0.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It fails, but why?
It is because we want to connect to &lt;code&gt;127.0.0.1&lt;/code&gt; but API (securely server over https) is responding with a certificate issued for different domain / IP address.
Fortunately solution for that is simple which is to put &lt;code&gt;clusters.cluster.tls-server-name&lt;/code&gt; property with one of the valid values from the error message -
in our case it can be &lt;code&gt;10.0.10.2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Our final configuration will look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;certificate-authority-data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;long string with CA data&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://127.0.0.1:16443&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tls-server-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10.0.10.2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-k8s-cluster&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-k8s-cluster&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-k8s-cluster&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;current-context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-k8s-cluster&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{}&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;some-fancy-token&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now running &lt;code&gt;cluster-info&lt;/code&gt; command will gives us what we expect:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~ &amp;gt; kubectl cluster-info
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Kubernetes control plane is running at https://10.0.10.2:16443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GLBCDefaultBackend is running at https://10.0.10.2:16443/api/v1/namespaces/kube-system/services/default-http-backend:http/proxy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;KubeDNS is running at https://10.0.10.2:16443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Metrics-server is running at https://10.0.10.2:16443/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yay! Now we can use &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;helm&lt;/code&gt; and other software that uses configuration from &lt;code&gt;~/.kube/config&lt;/code&gt; from our local machine!&lt;/p&gt;
&lt;p&gt;And a small &amp;ldquo;bonus&amp;rdquo; (and also describing it to the future myself) - accessing Kubernetes in GCP (GKE) through Identity Aware Proxy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ gcloud compute ssh &lt;span class="s2"&gt;&amp;#34;some-instance&amp;#34;&lt;/span&gt; --zone &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$ZONE&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --project &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --tunnel-through-iap --ssh-flag&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-L 16443:10.0.10.2:443&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is it - the SSH tunnel again saves the day! :-)&lt;/p&gt;</description></item><item><title>MongoDB sequences (auto-increment) - The Java Way</title><link>https://banach.net.pl/posts/2022/mongodb-sequences-autoincrement-the-java-way/</link><pubDate>Fri, 02 Dec 2022 22:30:00 +0100</pubDate><guid>https://banach.net.pl/posts/2022/mongodb-sequences-autoincrement-the-java-way/</guid><description>
&lt;p&gt;One of the missing elements while using MongoDB is lack of the auto-increment fields,
commonly called sequences (e.g. in PostgreSQL).
For MongoDB Atlas (managed MongoDB solution) there is a posibility to mimic this
feature using database triggers (as mentioned on &lt;a href="https://www.mongodb.com/basics/mongodb-auto-increment" target="_blank" &gt;MongoDB page&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tl;dr: Full code available at my Github - &lt;a href="https://github.com/elwin013/mongodb-sequences" target="_blank" &gt;elwin013/mongodb-sequences&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But what about self-hosted database?
Unfortunately, there are no triggers there (or I&amp;rsquo;m not aware of them :-)), so we need to improvise and make that happen with custom code.&lt;/p&gt;
&lt;p&gt;To make this work we will create a collection called &lt;code&gt;sequence&lt;/code&gt; which will have documents with two fields describing the sequence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_id&lt;/code&gt; with a name,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt; with a value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code will be relying on MongoDB&amp;rsquo;s &lt;a href="https://www.mongodb.com/docs/manual/core/write-operations-atomicity/" target="_blank" &gt;document write atomicity&lt;/a&gt;.
Linked documentation says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A findAndModify operation on a document is atomic&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the case of the Java Mongo driver, our operation is &lt;code&gt;findOneAndUpdate&lt;/code&gt;.
We will be searching for exactly one document with the selected id (name of the sequence), increment value and return that document
after the update. Let&amp;rsquo;s do it!&lt;/p&gt;
&lt;p&gt;Below is &lt;code&gt;Sequence&lt;/code&gt; record (it can be replaced with the classic POJO if we need to run code on e.g. Java 11 or 8).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.bson.codecs.pojo.annotations.BsonId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@BsonId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Worth mentioning is the &lt;code&gt;@BsonId&lt;/code&gt; annotation - this annotation will make field &lt;code&gt;name&lt;/code&gt; an id field (&lt;code&gt;_id&lt;/code&gt;) for document in collection.
So our document will have two fields - &lt;code&gt;_id&lt;/code&gt; (which will contain sequence name and will be mapped to &lt;code&gt;name&lt;/code&gt; field in code) and &lt;code&gt;value&lt;/code&gt; with value.&lt;/p&gt;
&lt;p&gt;Now we can write code to update (increment) and return sequence value.
We also want to be sure that the document always is created (update and insert, so upsert) and will be returned after creation.
To achieve that we will customize options of &lt;code&gt;findOneAndUpdate&lt;/code&gt; request:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FindOneAndUpdateOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;returnDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReturnDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AFTER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the cherry on the top - &lt;code&gt;findOneAndUpdate&lt;/code&gt; operation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sequence&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findOneAndUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;important_sequence&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;value&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code above will search for a document with id &lt;code&gt;important_sequence&lt;/code&gt;, increment the document&amp;rsquo;s &lt;code&gt;value&lt;/code&gt; field by one and return the document - exactly what we need.
And if the document doesn&amp;rsquo;t exist - it will be created and &lt;code&gt;seq.value()&lt;/code&gt; will give us &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can always change the value of sequence using similar code (&lt;code&gt;Updates.set&lt;/code&gt; instead of &lt;code&gt;Updates.inc&lt;/code&gt;) - but we need to set it to &lt;code&gt;0&lt;/code&gt; if the next value needs to be &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And that is all - we have sequences in MongoDB without any additional tools! Feels great! :-)&lt;/p&gt;
&lt;p&gt;Full code available at my Github - &lt;a href="https://github.com/elwin013/mongodb-sequences" target="_blank" &gt;elwin013/mongodb-sequences&lt;/a&gt; - including record class, DAO and some fancy
tests using &lt;a href="https://www.testcontainers.org/" target="_blank" &gt;TestContainers&lt;/a&gt; to test code using MongoDB on Docker.&lt;/p&gt;</description></item><item><title>Reading JWT token claims without a library</title><link>https://banach.net.pl/posts/2022/reading-jwt-token-claims-without-a-library/</link><pubDate>Fri, 14 Oct 2022 14:49:00 +0100</pubDate><guid>https://banach.net.pl/posts/2022/reading-jwt-token-claims-without-a-library/</guid><description>
&lt;p&gt;Almost everybody who works on web apps knows what JWT (JSON Web Token) is.
They become pretty popular and are now widely used.&lt;/p&gt;
&lt;p&gt;Without going into details (which can be read e.g. at &lt;a href="https://jwt.io/introduction" target="_blank" &gt;jwt.io/introduction&lt;/a&gt;) JWT is built from three parts divided by dots:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The middle part, payload, is interesting for us.
It contains claims, which are statements about an entity (e.g. the user) and some additional data.&lt;/p&gt;
&lt;p&gt;For purpose of this post we will use an example token from &lt;a href="https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" target="_blank" &gt;jwt.io&lt;/a&gt; (click on the link to open it in the debugger!) which looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The payload in the token is just a &lt;a href="https://en.wikipedia.org/wiki/Base64" target="_blank" &gt;base64&lt;/a&gt; encoded JSON object.
That means we can decode it and load this JSON string into the dictionary to have
easy access to properties.&lt;/p&gt;
&lt;p&gt;There is only one tricky part here - base64 padding!
The payload in the JWT is alfa-numeric only, there is no &lt;code&gt;=&lt;/code&gt; character.
To be sure that it will be decoded properly we need to add max padding (&lt;code&gt;==&lt;/code&gt;) to the payload string - it will be ignored if not needed.
But if we will not have it there (but it is expected to be) - decoder will throw an error.&lt;/p&gt;
&lt;p&gt;To sum up all above stuff in simple lines of code in Python:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;base64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Assuming the token is in the token variable &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.&amp;#34;&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.&amp;#34;&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Split by dot and get middle, payload, part;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;token_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Payload is base64 encoded, let&amp;#39;s decode it to plain string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# To make sure decoding will always work - we&amp;#39;re adding max padding (&amp;#34;==&amp;#34;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# to payload - it will be ignored if not needed.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;token_payload_decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_payload&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;==&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Payload is JSON - we can load it to dict for easy access&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_payload_decoded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# And now we can access its&amp;#39; elements - e.g. name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Let&amp;#39;s print it - it should show &amp;#34;John Doe&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can run it in the browser on Replit (&lt;a href="https://replit.com/@elwin013/Reading-JWT-token-without-a-library#main.py" target="_blank" &gt;Reading JWT token without library on Replit&lt;/a&gt;) or locally with Python 3.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s all.
It is pretty simple, isn&amp;rsquo;t it?&lt;/p&gt;
&lt;p&gt;And it feels great to be able to do it without any additional tools! :-)&lt;/p&gt;</description></item><item><title>How to customize Jetty in Spring - Custom Error Handler</title><link>https://banach.net.pl/posts/2022/how-to-customize-jetty-in-spring-custom-error-handler/</link><pubDate>Tue, 21 Jun 2022 14:41:00 +0100</pubDate><guid>https://banach.net.pl/posts/2022/how-to-customize-jetty-in-spring-custom-error-handler/</guid><description>
&lt;p&gt;Last week we found out that in some cases our application is showing redundant data on error - for example stacktrace.
It happens when there was an error which was handled by the application container which was Jetty.
For example, sending a header like &lt;code&gt;X-FORWARDED-PORT: some-not-numeric-value&lt;/code&gt; causes
&lt;code&gt;NumberFormatException&lt;/code&gt; and shows a full stacktrace.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve looked over the documentation and see that we could hide stacktrace in default error handler.
Instead of that, we decided to replace it with a custom one.
That allows us to put an additional logging logic and customize the output.
And this is a short description of how to do it. ;-)&lt;/p&gt;
&lt;p&gt;To implement the custom error handler we can extend &lt;code&gt;org.eclipse.jetty.server.handler.AbstractHandler&lt;/code&gt; and override &lt;code&gt;handle&lt;/code&gt; method.
On the code snippet below there is an example handler.
For sake of clarity it returns always simple text (content-type &lt;code&gt;text/plain&lt;/code&gt;) with code, error message and URL used.&lt;/p&gt;
&lt;p&gt;We can extend it by adding additional logic like different responses based on headers from the client, additional logging and so on. :-)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pl.net.banach.customizeJetty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.IOException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.OutputStreamWriter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.PrintWriter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.nio.BufferOverflowException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.nio.ByteBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.nio.charset.StandardCharsets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.servlet.ServletException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.servlet.http.HttpServletRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.servlet.http.HttpServletResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.eclipse.jetty.http.MimeTypes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.eclipse.jetty.io.ByteBufferOutputStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.eclipse.jetty.server.Dispatcher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.eclipse.jetty.server.Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.eclipse.jetty.server.handler.AbstractHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.eclipse.jetty.util.StringUtil&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomJettyErrorHandler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;extends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AbstractHandler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baseRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HttpServletRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HttpServletResponse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ServletException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Get error message, sanitize it, just in case.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;StringUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sanitizeXmlString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Get error code that will returned&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;charset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;StandardCharsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Get writer used&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baseRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getHttpOutput&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ByteBufferOutputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PrintWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OutputStreamWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Set content type, encoding and write response&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setContentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MimeTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEXT_PLAIN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCharacterEncoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;HTTP ERROR &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\nMessage: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\nURI: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestURI&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BufferOverflowException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baseRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resetContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baseRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHttpChannel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;sendResponseAndComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we have the error handler ready then it registering it is easy.
To do this we will create a custom configuration which implements &lt;code&gt;WebServerFactoryCustomizer&lt;/code&gt; with &lt;code&gt;@Configuration&lt;/code&gt; annotation.
In a &lt;code&gt;customize&lt;/code&gt; method we will create a customizer and add a newly created error handler.&lt;/p&gt;
&lt;p&gt;Seems simple, but be sure to put this configuration class in a package that is scanned by Spring &lt;code&gt;@ComponentScan&lt;/code&gt; annotation!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pl.net.banach.customizeJetty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.springframework.boot.web.embedded.jetty.JettyServerCustomizer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.springframework.boot.web.server.WebServerFactoryCustomizer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;org.springframework.context.annotation.Configuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JettyConfiguration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WebServerFactoryCustomizer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JettyServletWebServerFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;customize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JettyServletWebServerFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JettyServerCustomizer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;customizer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setErrorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CustomJettyErrorHandler&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addServerCustomizers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customizer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s all - from now all errors that go to Jetty (application server) will be handled by our custom handler. :-)&lt;/p&gt;</description></item><item><title>How to expose local service using nginx and SSH</title><link>https://banach.net.pl/posts/2022/how-to-expose-local-service-using-nginx-and-ssh/</link><pubDate>Fri, 25 Mar 2022 20:20:00 +0100</pubDate><guid>https://banach.net.pl/posts/2022/how-to-expose-local-service-using-nginx-and-ssh/</guid><description>
&lt;p&gt;One of my colleagues needs to expose the local running app to an external service.
There were simple constraints - the domain name under which service was exposed cannot
change (because it needs to be whitelisted) and need to be a subdomain of the company domain.&lt;/p&gt;
&lt;p&gt;They already have some virtual machines for developers with external IP adresses and
SSH access running. I’ve proposed using them to solve this task - below you can find
the solution that can be applied.&lt;/p&gt;
&lt;p&gt;What will be needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;publicly available server (external IP) with (GNU/)Linux and nginx (for this post I&amp;rsquo;m assuming it is Ubuntu),&lt;/li&gt;
&lt;li&gt;user and permissions to change nginx configuration (sudo/root),&lt;/li&gt;
&lt;li&gt;SSH access for that server,&lt;/li&gt;
&lt;li&gt;some domain name, for example - &lt;code&gt;some.domain.tld&lt;/code&gt; - pointing to the server external IP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s start with configuration - in &lt;code&gt;/etc/nginx/sites-available/some.domain.tld&lt;/code&gt; write:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
gzip on;
server_name some.domain.tld;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
proxy_set_header Host 127.0.0.1;
proxy_set_header X-Original-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This configuration will proxy requests for &lt;code&gt;some.domain.tld&lt;/code&gt; to the local port 8080 on the server.&lt;/p&gt;
&lt;p&gt;To enable it we need to link this configuration to &lt;code&gt;sites-enabled&lt;/code&gt; directory using command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ln -s /etc/nginx/sites-available/some.domain.tld /etc/nginx/sites-enabled/some.domain.tld
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and restart nginx - &lt;code&gt;systemctl restart nginx&lt;/code&gt;. Now under domain &lt;code&gt;some.domain.tld&lt;/code&gt; visitors will
see an error page with the message &amp;ldquo;bad gateway&amp;rdquo; - all because on 8080 port there is nothing.&lt;/p&gt;
&lt;p&gt;Now, with the power of a ssh tunnel, we will make this 8080 port on remote server will point
to some port on our local machine.&lt;/p&gt;
&lt;p&gt;To expose our local service we can use the command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh -N -R $PORT_ON_SERVER:localhost:$LOCAL_PORT $LOGIN@$SERVER
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$PORT_ON_SERVER is the port which we forward request to on server,&lt;/li&gt;
&lt;li&gt;$LOCAL_PORT - local port on dev machine where something is running,&lt;/li&gt;
&lt;li&gt;$LOGIN and $SERVER are the username and address of the server running proxy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Assuming that username is &lt;code&gt;alice&lt;/code&gt;, the server is available under &lt;code&gt;some.domain.tld&lt;/code&gt; domain and our local service
is available under port 4502:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh -N -R 8080:localhost:4502 alice@some.domain.tld
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that&amp;rsquo;s all - after opening the domain in the browser we will see our local service. And anybody
on the Internet can also view it, so watch out! ;-)&lt;/p&gt;</description></item><item><title>How to prevent running script if there are changes in local Git working space</title><link>https://banach.net.pl/posts/2021/how-to-prevent-running-script-if-there-are-changes-in-git-working-space/</link><pubDate>Fri, 30 Jul 2021 09:49:00 +0100</pubDate><guid>https://banach.net.pl/posts/2021/how-to-prevent-running-script-if-there-are-changes-in-git-working-space/</guid><description>
&lt;p&gt;Recently I spent some time making improvements around deploying an application.
Some of them are bash scripts automating some boring actions. All of them has
one common part - should not be run if there are any changes (staged or not) in the
local Git repository (working tree). Fortunately, it is simple to incorporate checks
like this into scripts!&lt;/p&gt;
&lt;p&gt;To get the information about staged (added with &lt;code&gt;git add&lt;/code&gt;) and not staged files we
will use &lt;code&gt;git diff&lt;/code&gt; command. Without any parameters, it will give us detailed
information of what changed. That is a bit too much than our needs, so we will
use &lt;code&gt;--stat&lt;/code&gt; flag to get only statistics - path to file and the number of lines
added and removed:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# ~/repository&amp;gt; git diff --stat
unstagedFile.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For staged files we will use very similar command - it will be &lt;code&gt;git diff --stat&lt;/code&gt; but with
&lt;code&gt;--cached&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# ~/repository&amp;gt; git diff --cached --stat
stagedFile.txt | 1 +
1 file changed, 1 insertion(+)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Both commands will return empty strings when no changes. We will use that information to
show the message and stop (exit) script. In the end, script will look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Fill up variables&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;STAGED_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;git diff --cached --stat&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;NOT_STAGED_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;git diff --stat&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check if there are any staged but not committed changes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; ! -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$STAGED_CHANGES&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Show message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;You have staged, but not committed changes. Not releasing!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# And exit with non-zero code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check if there are any not staged&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; ! -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$NOT_STAGED_CHANGES&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Show message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;You have not staged and not committed changes. Not releasing!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# And exit with non-zero code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;## Anything else we need to do! ##&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This script:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run commands mentioned above,&lt;/li&gt;
&lt;li&gt;store results in variables,&lt;/li&gt;
&lt;li&gt;if any of those variables are not empty - show the message and exit script with non-zero
return value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why non-zero return value? Because by default &lt;code&gt;0&lt;/code&gt; is treated as &amp;ldquo;everything was OK&amp;rdquo; and
any other value means &amp;ldquo;error&amp;rdquo;. This allows to chain commands like &lt;code&gt;doSomething &amp;amp;&amp;amp; doMagic&lt;/code&gt; -
in that case, &lt;code&gt;doMagic&lt;/code&gt; will run only if &lt;code&gt;doSomething&lt;/code&gt; return exited with &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Bonus stuff - we can make output red using some magic numbers which we can put in a
script (in echo command):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RED_START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;\033[1;31m&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RED_END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;\e[0m&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;Normal message!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RED_START&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Normal message but in RED!&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RED_END&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The result of the script above will look like this:
&lt;img src="https://banach.net.pl/images/2021-07-30-how-to-prevent-running-script-if-there-are-changes.png" alt="The result of the colourful script"&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s all folks! If you find this post useful - let me know! :-)&lt;/p&gt;</description></item><item><title>How to put pom.xml version and build date in properties?</title><link>https://banach.net.pl/posts/2021/how-to-put-pom.xml-version-and-build-date-in-properties/</link><pubDate>Fri, 04 Jun 2021 21:27:55 +0100</pubDate><guid>https://banach.net.pl/posts/2021/how-to-put-pom.xml-version-and-build-date-in-properties/</guid><description>
&lt;p&gt;While working on webapp API in Java I need to send the app version and the build date to the frontend. But how to make this?&lt;/p&gt;
&lt;p&gt;As my API build is based on maven I thought about getting version from &lt;code&gt;pom.xml&lt;/code&gt; file. So, let&amp;rsquo;s make it this way!&lt;/p&gt;
&lt;h2 id="solution-almost"&gt;Solution! Almost&amp;hellip;&lt;/h2&gt;
&lt;p&gt;I need to put in the &lt;code&gt;pom.xml&lt;/code&gt; file configuration responsible for resources filtering:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;&amp;lt;!-- clipped --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;resources&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;resource&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;directory&amp;gt;&lt;/span&gt;src/main/resources&lt;span class="nt"&gt;&amp;lt;/directory&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;filtering&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/filtering&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/resource&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/resources&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And in the &lt;code&gt;app.properties&lt;/code&gt; properties which will be replaced during build:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-properties" data-lang="properties"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;build.version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${pom.version}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;build.date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${timestamp}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After the build my &lt;code&gt;app.properties&lt;/code&gt; is filled with expected data:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://banach.net.pl/images/2021-06-04-how-to-put-pomxml-version-and-build-date-in-properties-image-1.png" alt="Version and build date in properties"&gt;&lt;/p&gt;
&lt;p&gt;Now I can access version from POM and last build in Java app easily!&lt;/p&gt;
&lt;h2 id="and-finhh-another-problem"&gt;And fin^H^H&amp;hellip; another problem?&lt;/h2&gt;
&lt;p&gt;Everything works smoothly, I can get the version from the &lt;code&gt;pom.xml&lt;/code&gt; and also the build date. But&amp;hellip; a favicon is now broken! What?!&lt;/p&gt;
&lt;p&gt;Frontend is deployed along with backend (using &lt;a href="https://javalin.io/documentation#static-files" target="_blank" &gt;Javalin static files feature&lt;/a&gt; and files were loaded from resources folder.&lt;/p&gt;
&lt;p&gt;It turns out that the resource filtering plugin processe &lt;code&gt;favicon.ico&lt;/code&gt; (in fact all &lt;code&gt;.ico&lt;/code&gt; files) and broke them by saving them incorrectly. So I need to exclude those files from filtering!&lt;/p&gt;
&lt;h2 id="final-solution"&gt;Final solution&lt;/h2&gt;
&lt;p&gt;Final configuration in the &lt;code&gt;pom.xml&lt;/code&gt; looks like in snippet below.&lt;/p&gt;
&lt;p&gt;It processing all files in &lt;code&gt;src/main/resources&lt;/code&gt; except all &lt;code&gt;.ico&lt;/code&gt; files. But I want &lt;code&gt;.ico&lt;/code&gt; in resource folder - so I need add them but without filtering (processing them).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;&amp;lt;!-- clipped --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;resources&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;resource&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;directory&amp;gt;&lt;/span&gt;src/main/resources&lt;span class="nt"&gt;&amp;lt;/directory&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;filtering&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/filtering&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;excludes&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/*.ico&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/excludes&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/resource&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;resource&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;directory&amp;gt;&lt;/span&gt;src/main/resources&lt;span class="nt"&gt;&amp;lt;/directory&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;filtering&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/filtering&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;includes&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;include&amp;gt;&lt;/span&gt;**/*.ico&lt;span class="nt"&gt;&amp;lt;/include&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/includes&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/resource&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/resources&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s all - everything is now OK ;-)&lt;/p&gt;</description></item><item><title>Why my session is not persisted?! Another story of (de)serialising...</title><link>https://banach.net.pl/posts/2021/why-my-session-is-not-persisted-another-story-of-deserializing/</link><pubDate>Thu, 14 Jan 2021 09:00:00 +0100</pubDate><guid>https://banach.net.pl/posts/2021/why-my-session-is-not-persisted-another-story-of-deserializing/</guid><description>
&lt;p&gt;Yep, that&amp;rsquo;s another story of &amp;ldquo;always check if serialising and de-serialising
works as expected&amp;rdquo;. And some kind of reminder for future-me if I forgot about
this! ;-)&lt;/p&gt;
&lt;p&gt;Session of logged users seems to be a pretty easy topic, yep? For sure it is!
What if you want to share that session between different instances of your app?
That seems quite easy too - let&amp;rsquo;s store it in a database and you&amp;rsquo;re good to go.&lt;/p&gt;
&lt;h2 id="need-and-sort-of-background"&gt;Need and sort of &amp;ldquo;background&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;For my last project, I&amp;rsquo;m using a simple web framework called
&lt;a href="https://javalin.io/" target="_blank" &gt;Javalin&lt;/a&gt;, which runs default on &lt;em&gt;Jetty&lt;/em&gt;. And after making
PoC I&amp;rsquo;ve come across of topic &amp;ldquo;single user using many app instances&amp;rdquo;.
Fortunately, there is even tutorial about it on Javalin page
(&lt;a href="https://javalin.io/tutorials/jetty-session-handling" target="_blank" &gt;here&lt;/a&gt;). I&amp;rsquo;ve checked it,
wrote a bit code aaaand&amp;hellip; it is not working as expected!&lt;/p&gt;
&lt;h2 id="problem"&gt;Problem&lt;/h2&gt;
&lt;p&gt;When enabled saving session into database and user was&amp;hellip; logged out instantly!
Error 401 was sent from the secured resource and frontend was redirecting to the
login which invalidates session (which, in fact, was desired behaviour). But the
question is - why this 401 was sent if the user logged in correctly?&lt;/p&gt;
&lt;p&gt;After debugging and looking into saved session object in database I&amp;rsquo;ve found out
that there is NO USER DATA THERE!&lt;/p&gt;
&lt;p&gt;Wait, what? I&amp;rsquo;m sure that I&amp;rsquo;ve put that data into the session. It works if the
session was in memory!&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see how writing and reading values into session looks in Javalin:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// saving value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mystery&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;I like trains&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// and reading value to the &amp;#34;value&amp;#34; variable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mystery&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pretty simple, yep? And this is how saving user details was in my app:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserDetails&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ... filling up data and saving to session&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;USER&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Looks the same&amp;hellip; and not working. But &lt;code&gt;mystery&lt;/code&gt; above works, so what the
heck is going on?!&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution!&lt;/h2&gt;
&lt;p&gt;You might already know because I&amp;rsquo;ve already written it in the beginning -
serialisation. Session store (from class &lt;code&gt;JDBCSessionDataStoreFactory&lt;/code&gt;)
didn&amp;rsquo;t know how to save object with user details to the database. So it
simply ignores it. And the rest of the app didn&amp;rsquo;t work because there was
no user data, so no info about user roles, so no cookies and big &amp;ldquo;NONONO&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;So, solution was simply to serialise and de-serialise it, for example as
a JSON. For that I&amp;rsquo;ve used Jackson mapper:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// yep, simple com.fasterxml.jackson.databind.ObjectMapper&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ObjectMapper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// serialize before saving to session&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserDetails&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ... filling up data and saving to session&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;USER&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// and reading it in some other place&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userDetailsStr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;USER_SESSION_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userDetailsStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And everything started to work smoothly and as expected!&lt;/p&gt;
&lt;h2 id="takeaways"&gt;Takeaways&lt;/h2&gt;
&lt;p&gt;I will repeat myself from the beginning - always check if serialising
and de-serialising works as it should. And additionally - check if data is saved. That can save a lot of work and time. :-)&lt;/p&gt;</description></item><item><title>Git config based on current (sub)folder</title><link>https://banach.net.pl/posts/2020/git-config-based-on-current-subfolder/</link><pubDate>Sun, 29 Nov 2020 16:00:00 +0100</pubDate><guid>https://banach.net.pl/posts/2020/git-config-based-on-current-subfolder/</guid><description>
&lt;p&gt;Some time ago I&amp;rsquo;ve looked over my current company repositories.
Surprisingly there was a lot of commits with emails that are not company ones.
Instead of those - a lot of &lt;em&gt;gmail.com&lt;/em&gt;, &lt;em&gt;outlook.com&lt;/em&gt;
and other free email providers!&lt;/p&gt;
&lt;p&gt;That looks a bit unprofessional, isn&amp;rsquo;t it? Even if we do some
freelance gigs - we probably want to put nicer email than
private one used for social websites. And if, for some reason,
we&amp;rsquo;re forced to use company email - we need to be sure that
it will be used!&lt;/p&gt;
&lt;p&gt;So, what we can do to make it better? Just configure it
with couple lines of code ;-) Let&amp;rsquo;s assume that all our work-related
stuff will live in &lt;code&gt;~/work&lt;/code&gt; folder and all other is personal. So,
let&amp;rsquo;s start with a simple &lt;code&gt;~/.gitconfig&lt;/code&gt; configuration:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# ~/.gitconfig
[user]
name = Johny Mnemonic
email = johny@mnemonic.tld
[includeIf &amp;#34;gitdir:~/work/&amp;#34;]
path = ~/work/.gitconfig
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Configuration in &lt;em&gt;user&lt;/em&gt; section will be default one. We will
overwrite it in other configuration files, like in folders with
work.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s then create an additional configuration for work, just in our
work folder:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# ~/work/.gitconfig
[user]
email = johny@memory.courier.tld
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We want to put our name in commits, yep? So only one thing that we
overwrite will be the email address. Instead of &lt;em&gt;private&lt;/em&gt; one, there will
be company email.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. Now all repositories, that didn&amp;rsquo;t have set up individual
user settings, will use ones that are based on the folder, e.g.:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/work/super-duper&lt;/code&gt; will use work configuration,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/project/my-website&lt;/code&gt; will use private (default) one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I hope that &amp;ldquo;short writeup&amp;rdquo; will help somebody in some way. :-) Also - did you
have any comments?
Drop me a line on &lt;a href="https://twitter.com/elwin013/" title="Kamil Banach on Twitter" target="_blank" &gt;Twitter&lt;/a&gt;!&lt;/p&gt;</description></item></channel></rss>