<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://jan.alphadev.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jan.alphadev.net/" rel="alternate" type="text/html" /><updated>2026-06-06T03:02:43+00:00</updated><id>https://jan.alphadev.net/feed.xml</id><title type="html">Jan’s Stuff</title><subtitle>Der alltägliche Wahnsinn</subtitle><author><name>Jan Seeger</name></author><entry><title type="html">Running modern Git on ancient Synology kernels - revisited</title><link href="https://jan.alphadev.net/blog/2026/git-on-synology-2/" rel="alternate" type="text/html" title="Running modern Git on ancient Synology kernels - revisited" /><published>2026-06-05T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/git-on-synology-2</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/git-on-synology-2/"><![CDATA[<p>In the <a href="/blog/2026/git-on-synology/">original post</a> I had made small shim that redirects calls from libc getrandom to the Kernel device.</p>

<p>Since I had used alpine images, linking against libmusl was a reasonable choice, but that meant this hack would be limited to libmusl-based images.</p>

<p>I have since found a way to generalise this to work with any flavor of libc:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// getrandom_shim.c  — no libc dependency</span>
<span class="cp">#include</span> <span class="cpf">&lt;sys/syscall.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stddef.h&gt;</span><span class="cp">
</span>
<span class="k">static</span> <span class="kt">long</span> <span class="nf">sys</span><span class="p">(</span><span class="kt">long</span> <span class="n">n</span><span class="p">,</span> <span class="kt">long</span> <span class="n">a</span><span class="p">,</span> <span class="kt">long</span> <span class="n">b</span><span class="p">,</span> <span class="kt">long</span> <span class="n">c</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">long</span> <span class="n">r</span><span class="p">;</span>
<span class="cp">#if defined(__x86_64__)
</span>    <span class="k">register</span> <span class="kt">long</span> <span class="n">r10</span> <span class="n">asm</span><span class="p">(</span><span class="s">"r10"</span><span class="p">);</span> <span class="c1">// unused here</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"syscall"</span> <span class="o">:</span> <span class="s">"=a"</span><span class="p">(</span><span class="n">r</span><span class="p">)</span> <span class="o">:</span> <span class="s">"a"</span><span class="p">(</span><span class="n">n</span><span class="p">),</span><span class="s">"D"</span><span class="p">(</span><span class="n">a</span><span class="p">),</span><span class="s">"S"</span><span class="p">(</span><span class="n">b</span><span class="p">),</span><span class="s">"d"</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
                 <span class="o">:</span> <span class="s">"rcx"</span><span class="p">,</span><span class="s">"r11"</span><span class="p">,</span><span class="s">"memory"</span><span class="p">);</span>
<span class="cp">#elif defined(__aarch64__)
</span>    <span class="k">register</span> <span class="kt">long</span> <span class="n">x8</span> <span class="n">asm</span><span class="p">(</span><span class="s">"x8"</span><span class="p">)</span><span class="o">=</span><span class="n">n</span><span class="p">,</span> <span class="n">x0</span> <span class="n">asm</span><span class="p">(</span><span class="s">"x0"</span><span class="p">)</span><span class="o">=</span><span class="n">a</span><span class="p">,</span> <span class="n">x1</span> <span class="n">asm</span><span class="p">(</span><span class="s">"x1"</span><span class="p">)</span><span class="o">=</span><span class="n">b</span><span class="p">,</span> <span class="n">x2</span> <span class="n">asm</span><span class="p">(</span><span class="s">"x2"</span><span class="p">)</span><span class="o">=</span><span class="n">c</span><span class="p">;</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"svc 0"</span> <span class="o">:</span> <span class="s">"+r"</span><span class="p">(</span><span class="n">x0</span><span class="p">)</span> <span class="o">:</span> <span class="s">"r"</span><span class="p">(</span><span class="n">x8</span><span class="p">),</span><span class="s">"r"</span><span class="p">(</span><span class="n">x1</span><span class="p">),</span><span class="s">"r"</span><span class="p">(</span><span class="n">x2</span><span class="p">)</span> <span class="o">:</span> <span class="s">"memory"</span><span class="p">);</span>
    <span class="n">r</span> <span class="o">=</span> <span class="n">x0</span><span class="p">;</span>
<span class="cp">#endif
</span>    <span class="k">return</span> <span class="n">r</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">long</span> <span class="nf">getrandom</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">flags</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">flags</span><span class="p">;</span>
    <span class="kt">long</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">sys</span><span class="p">(</span><span class="n">SYS_open</span><span class="p">,</span> <span class="p">(</span><span class="kt">long</span><span class="p">)</span><span class="s">"/dev/urandom"</span><span class="p">,</span> <span class="mi">0</span> <span class="cm">/*O_RDONLY*/</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
    <span class="kt">long</span> <span class="n">n</span> <span class="o">=</span> <span class="n">sys</span><span class="p">(</span><span class="n">SYS_read</span><span class="p">,</span> <span class="n">fd</span><span class="p">,</span> <span class="p">(</span><span class="kt">long</span><span class="p">)</span><span class="n">buf</span><span class="p">,</span> <span class="p">(</span><span class="kt">long</span><span class="p">)</span><span class="n">len</span><span class="p">);</span>
    <span class="n">sys</span><span class="p">(</span><span class="n">SYS_close</span><span class="p">,</span> <span class="n">fd</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">n</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We now have to add <code class="language-plaintext highlighter-rouge">-nostdlib</code> to the compilation command, but <code class="language-plaintext highlighter-rouge">musl-dev</code> is still needed for the syscall headers:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">"</span>:/src <span class="nt">-w</span> /src alpine:3.19 sh <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"apk add --no-cache gcc musl-dev &amp;&amp; gcc -shared -fPIC -nostdlib -O2 -o getrandom_shim.so getrandom_shim.c"</span>
<span class="nb">chmod </span>644 getrandom_shim.so
</code></pre></div></div>

<p>And now we can use the <code class="language-plaintext highlighter-rouge">getrandom_shim.so</code> for any images, regardless of what style of libc they use.</p>

<p>On that note, this follow-up post answers whether this hack is still in place.</p>]]></content><author><name>Jan Seeger</name></author><category term="playground" /><category term="synology" /><category term="git" /><summary type="html"><![CDATA[In the original post I had made small shim that redirects calls from libc getrandom to the Kernel device.]]></summary></entry><entry><title type="html">Gear Replacements</title><link href="https://jan.alphadev.net/blog/2026/gear-replacements/" rel="alternate" type="text/html" title="Gear Replacements" /><published>2026-05-08T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/gear-replacements</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/gear-replacements/"><![CDATA[<h1 id="peak-design-everyday-backpack-35l-v1">Peak Design Everyday Backpack 35L v1</h1>

<p>After half a decade the zipper on my trusty backpack has failed. No surprise there, I had been travelling for 10s of thousands of KMs with the thing by Airplane, Train, Buses, Car, Motorcycles and on Foot and have used (and abused) it to hell and back.</p>

<p>The options were having it repaired, but that would have either involved sending the thing halfway across the globe (and getting a replacement v2 variant in the end) or repairing it here locally, I had even started to look into possible replacement parts and their specs. But that would have set me back more than half of the cost of a brand-new backpack.</p>

<p>Instead I then took this as an opportunity to upgrade to the <a href="https://www.peakdesign.com/eu/products/travel-backpack?Size=45L&amp;Color=Black">Peak Design 45L Travel Backpack</a>, which solves several issues I had with the old one, like the straps, the luggage pass-through and the small volume. On the other hand this also means I now have to optimise my gear and packing procedure again from scratch (which isn’t necessarily a bad thing, but additional effort)</p>

<h1 id="motorcycle-carplay-display">Motorcycle CarPlay Display</h1>

<p>After two seasons of riding with the <a href="/blog/2024/zero-modifications-carplay/#carplay-display">cheap CarPlay display from China</a> (that was supposed to be weather-proof) the pixels in the display got stuck. This was fine but sometimes the backlight cut out, making it impossible to read directions.</p>

<p>By recommendation of a friend of mine, I replaced it with an <a href="elebest">Elebest C650</a>, which is solves several of the issues I had with the first device:</p>

<ul>
  <li>
    <p>The screen is high-res enough that you can’t make out any pixels by eye and way brighter</p>
  </li>
  <li>
    <p>Media Playback does only resume on connect if there is an app actively in memory and has an open media session</p>
  </li>
  <li>
    <p>The display is mounted in a cradle and can be removed</p>

    <p>This one is also supposed to be weather-proof, but instead of verifying this I can remove the display over the winter.</p>
  </li>
</ul>

<p><img src="/assets/2026/0e732692-f283-4381-963b-c4cdc63f46b1.jpg" alt="Elebest C650 mounted to the handlebar" /></p>

<h1 id="gloves">Gloves</h1>

<p>During the <a href="/blog/2026/trip-report-wiesbaden/">Wiesbaden Trip</a> I got cold and bought warm <a href="https://www.polo-motorrad.com/de-de/reusch-driftice-gore-tex-leder-textilhandschuh-lang-schwarz-8/3115131006001934/pdp">Reusch Driftice gloves</a>, I’ll keep my <a href="https://www.motorun.de/RUKKA-Virium-glove-waterproof-black-8">Rukka Virium</a>s but then for warmer weather.</p>]]></content><author><name>Jan Seeger</name></author><summary type="html"><![CDATA[Peak Design Everyday Backpack 35L v1]]></summary></entry><entry><title type="html">Fake Clerk OAuth in Coolify</title><link href="https://jan.alphadev.net/blog/2026/fake-clerk/" rel="alternate" type="text/html" title="Fake Clerk OAuth in Coolify" /><published>2026-05-06T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/fake-clerk</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/fake-clerk/"><![CDATA[<p>After I <a href="/blog/2026/coolify/">fell in love</a> with the way <a href="https://coolify.io/">Coolify</a> makes deployments stupidly simple, it was bound to stay. But I wanted to use OAuth to authenticate users.</p>

<p><a href="https://coolify.io/docs/knowledge-base/oauth">Officially Coolify supports Azure, BitBucket, GitLab and Google OAuth providers</a>. While not explicitly mentioned in the docs, it supports a few more (Authentik, Clerk, Discord, Infomaniak and Citadel).</p>

<p>But mine (<a href="https://pocket-id.org">PocketID</a>) unfortunately isn’t on the list and all the providers differ in their implementations in minute details, like data structures or URL schemas.</p>

<h1 id="fake-clerk-proxy">Fake Clerk Proxy</h1>

<p>An hour of back and forth with Claude later, Clerk was made out to be the closest match to PocketID. All that is needed is a small proxy that rewrites a few urls:</p>

<pre><code class="language-Caddyfile">:80 {
    log {
        output file /var/log/caddy/access.log
        format json
    }

    @authorize path /oauth/authorize
    handle @authorize {
        uri query scope openid+email+profile
        redir {$POCKETID_URL}/authorize?{query} 302
    }

    rewrite /oauth/token    /api/oidc/token
    rewrite /oauth/userinfo /api/oidc/userinfo

    reverse_proxy {$POCKETID_URL}
}
</code></pre>

<p>Combined with a small Dockerfile:</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> caddy:2-alpine</span>
<span class="k">ENV</span><span class="s"> POCKETID_URL=""</span>
<span class="k">COPY</span><span class="s"> Caddyfile /etc/caddy/Caddyfile</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="k">HEALTHCHECK</span><span class="s"> --interval=30s --timeout=3s \</span>
  CMD wget -q --spider http://localhost/ || exit 1
</code></pre></div></div>

<h1 id="custom-claims">Custom Claims</h1>

<p>Coolify expects additional Claims from “Clerk” that PocketID does not provide by default, using a custom User Group that injects these hardcoded values we can fake it.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>email_verified = true
user_id = pocketid-user
</code></pre></div></div>

<p>The actual values don’t matter, Coolify merely expects them to be set and then ignores it. Users are matched by their email address.</p>

<h1 id="configure-coolify-oauth">Configure Coolify OAuth</h1>

<p>Once deployed and running, the “Clerk” OAuth can be configured as usual and the Base URL/Authentication Endpoint pointing to the “Clerk” Proxy.</p>

<p>PocketID being Passkeys-only is considered safe, but the old Username/Password login is vulnerable to brute-force attacks.
To prevent this we add a redirect in the Server Configuration -&gt; Proxy -&gt; Dynamic Configurations we add a file called login-redirect.yaml (Don’t forget to replace <code class="language-plaintext highlighter-rouge">{coolify-host}</code> with your domain):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">http</span><span class="pi">:</span>
  <span class="na">middlewares</span><span class="pi">:</span>
    <span class="na">redirect-to-clerk</span><span class="pi">:</span>
      <span class="na">redirectRegex</span><span class="pi">:</span>
        <span class="na">regex</span><span class="pi">:</span> <span class="s2">"</span><span class="s">^https://{coolify-host}/login$"</span>
        <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://{coolify-host}/auth/clerk/redirect"</span>
        <span class="na">permanent</span><span class="pi">:</span> <span class="kc">false</span>

  <span class="na">routers</span><span class="pi">:</span>
    <span class="na">coolify-login-redirect</span><span class="pi">:</span>
      <span class="na">rule</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Host(`{coolify-host}`)</span><span class="nv"> </span><span class="s">&amp;&amp;</span><span class="nv"> </span><span class="s">Path(`/login`)"</span>
      <span class="na">middlewares</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">redirect-to-clerk@file</span>
      <span class="na">service</span><span class="pi">:</span> <span class="s">coolify</span>
      <span class="na">entryPoints</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">https</span>
      <span class="na">tls</span><span class="pi">:</span>
        <span class="na">certresolver</span><span class="pi">:</span> <span class="s">letsencrypt</span>
</code></pre></div></div>

<p>Coolify’s Traefik should automatically pick up dynamic configs and the next login should immediately redirect you to your OAuth Endpoint.</p>

<h1 id="further-considerations">Further considerations</h1>

<p>This will break authentication if the “Clerk” Proxy container or PocketID is ever not up and reachable.</p>

<p>Should either one of those go down we can place this shell script on the host as an easy way to gain emergency access:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="nv">FILE</span><span class="o">=</span><span class="s2">"/data/coolify/proxy/dynamic/login-redirect.yaml"</span>

<span class="k">if</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$FILE</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">rm</span> <span class="s2">"</span><span class="nv">$FILE</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"Login redirect disabled — password login restored"</span>
<span class="k">else
    </span><span class="nb">cat</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$FILE</span><span class="s2">"</span> <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
http:
  middlewares:
    redirect-to-clerk:
      redirectRegex:
        regex: "^https://{coolify-host}/login</span><span class="nv">$"</span><span class="sh">
        replacement: "https://{coolify-host}/auth/clerk/redirect"
        permanent: false

  routers:
    coolify-login-redirect:
      rule: "Host(`{coolify-host}`) &amp;&amp; Path(`/login`)"
      middlewares:
        - redirect-to-clerk@file
      service: coolify
      entryPoints:
        - https
      tls:
        certresolver: letsencrypt
</span><span class="no">EOF
</span>    <span class="nb">echo</span> <span class="s2">"Login redirect enabled — passkey login enforced"</span>
<span class="k">fi</span>
</code></pre></div></div>]]></content><author><name>Jan Seeger</name></author><category term="playground" /><category term="oauth" /><category term="coolify" /><category term="pocketid" /><summary type="html"><![CDATA[After I fell in love with the way Coolify makes deployments stupidly simple, it was bound to stay. But I wanted to use OAuth to authenticate users.]]></summary></entry><entry><title type="html">Running modern Git on ancient Synology kernels</title><link href="https://jan.alphadev.net/blog/2026/git-on-synology/" rel="alternate" type="text/html" title="Running modern Git on ancient Synology kernels" /><published>2026-04-21T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/git-on-synology</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/git-on-synology/"><![CDATA[<p><strong>UPDATE</strong>: There is a <a href="/blog/2026/git-on-synology-2/">follow-up with better variant of the hack described by this post</a>.</p>

<p>Git push to my Gitea failed with the following message:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>remote: error: unable to get random bytes for temporary file: Function not implemented
remote: error: unable to create temporary file: Function not implemented
</code></pre></div></div>

<p>Instead of generating random bits from <code class="language-plaintext highlighter-rouge">/dev/urandom</code>, modern Git now calls the <code class="language-plaintext highlighter-rouge">getrandom</code> syscall via libc, which has been available from Kernel 3.17 and up. Unfortunately for me Synology does not update to newer Kernel versions, instead they backport patches to the old version that came with the device, which is good for stability, but bad if you need to run a more modern Git version.</p>

<h1 id="ld_preload-shim">LD_PRELOAD shim</h1>

<p>Claude suggested to replace the NAS, second best option was to build this small shim as SO file against libmusl and mount it into Gitea’s container:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// file: getrandom_shim.c</span>
<span class="cp">#define _GNU_SOURCE
#include</span> <span class="cpf">&lt;sys/types.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;errno.h&gt;</span><span class="cp">
</span>
<span class="kt">ssize_t</span> <span class="nf">getrandom</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">buflen</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">flags</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">flags</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/dev/urandom"</span><span class="p">,</span> <span class="n">O_RDONLY</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">errno</span> <span class="o">=</span> <span class="n">EIO</span><span class="p">;</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span>
    <span class="kt">ssize_t</span> <span class="n">n</span> <span class="o">=</span> <span class="n">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="n">buflen</span><span class="p">);</span>
    <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">n</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We take this small shim that redirects the syscall to the old <code class="language-plaintext highlighter-rouge">/dev/urandom</code> device and compile it against alpine, which uses musl:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/volume1/docker/shim# docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">"</span>:/src <span class="nt">-w</span> /src alpine:3.19 sh <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"apk add --no-cache gcc musl-dev &amp;&amp; gcc -shared -fPIC -O2 -o getrandom_shim.so getrandom_shim.c"</span>
/volume1/docker/shim# <span class="nb">chmod </span>644 getrandom_shim.so
</code></pre></div></div>

<p>Once the shim is built, we can now add these two lines the Gitea docker-compose.yaml to inject it into the dynamic linker before all the other symbols are resolved and redeploy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>environment:
      - LD_PRELOAD=/usr/local/lib/getrandom_shim.so
…
volumes:
      - /volume1/docker/shim/getrandom_shim.so:/usr/local/lib/getrandom_shim.so:ro
</code></pre></div></div>

<p>Git is now working properly again. This post is put under the <a href="/category/playground">playground category</a> deliberately, because I don’t know yet whether I want to keep it like this or not.</p>]]></content><author><name>Jan Seeger</name></author><category term="playground" /><category term="synology" /><category term="git" /><summary type="html"><![CDATA[UPDATE: There is a follow-up with better variant of the hack described by this post.]]></summary></entry><entry><title type="html">Trip Report: Wiesbaden</title><link href="https://jan.alphadev.net/blog/2026/trip-report-wiesbaden/" rel="alternate" type="text/html" title="Trip Report: Wiesbaden" /><published>2026-04-14T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/trip-report-wiesbaden</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/trip-report-wiesbaden/"><![CDATA[<p>First big motorcycle trip of the year. Fresh set of tires (approximately 150 KMs of use). Friday through Sunday, 700 KMs</p>

<p><img src="/assets/2026/2C0E5E90-A6DF-4EE0-A631-DFE05C588960.png" alt="Map showing the Tour" /></p>

<h1 id="day-1-baden">Day 1: Baden</h1>

<p>Started trip in light drizzle and 12 degrees C.</p>

<p>As the tour went on the rain stopped but I started to feel cold and replaced my light gloves with warmer ones I picked up along the way in Pforzheim.</p>

<p><img src="/assets/2026/35B10592-0413-47CC-9648-0C74F914DAAE.jpg" alt="Photo taken at our first stop in Pforzheim" /></p>

<p>With a small delay we made it to Heidelberg where (at least in the old town area) publicly accessible chargers were sparse. A friendly parking garage manager did help me by blocking a free spot that had just opened up.</p>

<p>After a quick bite (including a quick dessert to go from <a href="https://zeitfuerbrot.com/baeckereien">Zeit für Brot</a>) we went on to Mainz way delayed.</p>

<p>To not have additional delays we decided to use the faster route via the Autobahn, but were held up majorly when a construction site merged three lanes into one and traffic stopped to a crawl.</p>

<p>We made it to Mainz right after the sun went down. But the charger near our hotel I had previously planned on using was in a parking garage with license plate recognition and they refused to let me in to charge.</p>

<p><img src="/assets/2026/27C4F344-5344-477A-A43D-AE3321F7F4F2.jpg" alt="Electric Bike parked on the sidewalk next to the Charger" /></p>

<p>I then found one in the old town area that showed as free, but when I arrived one was blocked by an electric car that had the cable in but wasn’t even charging and the other one by a rundown combustion Opel, so I had to drive up onto the sidewalk next to the charger.</p>

<p>I first tried the EnBW app and it appeared to unlock it but wouldn’t start charging. Two attempts via their website didn’t work. I then proceeded to download their app, where you had to unlock the charger first and then plug the vehicle in after it prompts you, it worked. Left the 80% charge target in because we wanted to spend time in Wiesbaden the next day.</p>

<h1 id="day-2-franken">Day 2: Franken</h1>

<p>We stayed at the Ibis city, rooms and hotel bar were nice but when I woke up I was greeted by bed bugs.</p>

<p><img src="/assets/2026/72AE1443-2346-4E8D-B0FA-059AABF54CFC.jpg" alt="Bed Bug from the Hotel" /></p>

<p>During checkout the front desk wasn’t occupied, but since we stayed only for the one night I didn’t bother waiting around to complain.</p>

<p>We went on to meet up for breakfast with friends. Naturally, the public charger on the map was on fenced off company grounds and the gate guard didn’t even know about a charger for electric vehicles existing.</p>

<p>I then found one nearby that wasn’t on any map by ESWE unfortunately neither the Roaming, nor their app worked.</p>

<p>Weather was nice and sunny, lots of other motorcycle riders out.</p>

<p>We continued on to Aschaffenburg. Found a charger next to some industrial zone. Plugged it in, and due to the learnings of the days prior, went straight to the AppStore and downloaded the app. While charging we went to a nearby Biergarten for some coffee and cake.</p>

<p>The journey onwards to Würzburg was uneventful and nice. We switched to lighter clothes for the day.</p>

<p>After checking into the hotel at the border of the city we went looking for a charger nearby. All the ones we found, either didn’t work or had the wrong plugs. Finally when we wanted to go grab some lunch, we came by a company parking lot with lots of free chargers and a poster saying for public use, naturally not showing up on the map.</p>

<h1 id="day-3-hohenlohe">Day 3: Hohenlohe</h1>

<p>The next day I grabbed a quick coffee double espresso from the hotel before getting my bike back from the charger, then got back and ate real breakfast.</p>

<p>The weather already didn’t look promising with light rain.</p>

<p>We drove on to Schwäbisch Hall, where after a long, time reach anxiety kicked in again. Perhaps it was a combo of using the rain mode (which reduces the power recovered from braking), being soaking wet and cold and wanting to escape to a bakery with a warm coffee and watching the battery gauge slowly go down: Plugged in for 15 mins, couldn’t find an open bakery and drove on for the final (about 35) KMs. Didn’t need the 15 mins of charging in the end.</p>

<p>Wind, overcast sky and pouring rain for hours make for a terrible riding experience.</p>

<p>In Schwäbisch Hall we found the parking lot of the DMV that had chargers. There was a sign for no motorcycle parking, which I conveniently ignored and pulled a ticket. The chargers are activated using your entry ticket and you pay for both charging and parking before leaving.</p>

<p>We went to a restaurant at the top of the Einkorn and met up with friends and family there for lunch. Pealed off several layers of soaked clothing in the hopes of it drying at least somewhat.</p>

<p>Picked up the bike on the way back and was surprised that on Sundays parking (and charging) is free!</p>

<p>It was still raining and we were cold all the way to Esslingen where we went into a gas station to top up and warm up for a few minutes.</p>

<p>Slight navigational mishap lead us onto the A8 for a short stint. After that we rode the B27 back home with the option to stop in Tübingen if we get colder or the range gets any lower.</p>

<p>In the end we made it right before the sun went down. Freezing cold and shaking. I plugged the bike back in at home with 17 KMs of battery left.</p>

<p><img src="/assets/2026/4570404C-011C-4A88-85AA-DA17321220FC.jpg" alt="Picture of the bike, completely dirty" /></p>

<h1 id="summary">Summary</h1>

<p>In good conditions the bike has way more reach than what our backs can endure on those seats.</p>

<p>Riding for hundreds of kilometers in wind and rain is miserable. I am now considering what adjustments I’m going to make to my gear for future trips.</p>

<p>I now have three more charging apps on my phone. Price-wise (the free charging not included) it averages about 60 cents/kWh. The best experiences were with chargers that somebody put there, but weren’t on any map. I’ll add them to Chargemap once I’m done writing this.</p>]]></content><author><name>Jan Seeger</name></author><category term="trip report" /><category term="motorcycle" /><category term="zero s 2024" /><summary type="html"><![CDATA[First big motorcycle trip of the year. Fresh set of tires (approximately 150 KMs of use). Friday through Sunday, 700 KMs]]></summary></entry><entry><title type="html">Forwarding SSH to Gitea</title><link href="https://jan.alphadev.net/blog/2026/gitea-ssh-forwarding/" rel="alternate" type="text/html" title="Forwarding SSH to Gitea" /><published>2026-03-22T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/gitea-ssh-forwarding</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/gitea-ssh-forwarding/"><![CDATA[<p>For years my Gitea instance (in Docker) did have proper HTTPS clone URLs using a Reverse Proxy that handles domain and SSL termination, <strong>but I could never get SSH to work properly and had to expose the Gitea built-in SSH server on a different port</strong>.</p>

<p>This post describes my setup step by step in case you (or I) need to recreate it.</p>

<h1 id="prerequisites">Prerequisites</h1>

<ul>
  <li>a working Gitea instance</li>
  <li>with its User Accounts set up</li>
  <li>at least one user with a Public Key set up</li>
  <li>your Synology has its SSH service properly configured, secured, exposed through the firewall</li>
</ul>

<h1 id="ssh-fundamentals">SSH fundamentals</h1>

<p>But first a few things to understand, which I fully grasped way too late into the process:</p>

<ul>
  <li>
    <p>There will be just one SSH server running (on the host)</p>
  </li>
  <li>
    <p>During authentication the SSH protocol only allows authentication. And exactly once.</p>

    <p>That means you cannot during the authentication forward the connection to another server, only after authentication. But  any authentication required by the second server would fail because your (git) client thinks auth has already completed (which in a way it has)</p>
  </li>
  <li>
    <p>Agent-Forwarding might work in theory, but we want regular ssh clones without any host config modifications required, that matters doubly on CI builds.</p>
  </li>
</ul>

<h1 id="the-setup">The setup</h1>

<p>Gitea comes batteries-included with little helpers for that. But the tricky part is getting this to work reliably and securely. We’ll mount the git’s .ssh dir read-write into the Gitea container and Gitea will then keep it updated with all its valid users public keys and an additional <em>tagged</em> command that lets Gitea to detect the user and allows further processing.</p>

<h1 id="setting-up-the-git-user">Setting up the git user</h1>

<p>We need a system user that all git clones then are funnelled through (The git@ part in git@$domain.tld).</p>

<p>Synology’s Web Interface will block the creation of a git user for <em>security reasons</em>. Terminal it then is:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>synouser <span class="nt">--add</span> git <span class="s1">''</span> <span class="s2">""</span> 0 <span class="s2">""</span> 0
</code></pre></div></div>

<p>Then set an invalid password for the git user to prevent password authentication:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>synouser <span class="nt">--setpw</span> git <span class="s1">'*'</span>
</code></pre></div></div>

<p>While you’re at it, go to the Web Interface and tick the “Prevent user from changing password” checkbox.</p>

<h1 id="setting-up-the-git-users-ssh-directory">Setting up the git user’s SSH directory</h1>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/services/homes/git/.ssh
<span class="nb">sudo touch</span> /var/services/homes/git/.ssh/authorized_keys
<span class="nb">sudo chown</span> <span class="nt">-R</span> 1000 /var/services/homes/git/.ssh
<span class="nb">sudo chmod </span>700 /var/services/homes/git/.ssh
<span class="nb">sudo chmod </span>600 /var/services/homes/git/.ssh/authorized_keys
<span class="nb">sudo chmod </span>755 /var/services/homes/git
</code></pre></div></div>

<p>Next we remove Synology’s extended ACLs from the git’s home directory, as sshd’s StrictModes rejects directories with ACL entries:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> /usr/syno/bin/synoacltool <span class="nt">-del</span> /var/services/homes/git
</code></pre></div></div>

<h2 id="preparing-the-git-user-account-settings">Preparing the git user account settings</h2>

<p>The user in my Gitea Docker container has the user id 1000 and because SSH PubKey auth is set to be picky when it comes to file permissions of its key files, I had to match the uid on the host.</p>

<p>First let’s check if the user id 1000 is free:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">id </span>1000
</code></pre></div></div>

<p>In my case it said “no such user”, which is exactly what we need.</p>

<p>Open the passwd file and change the git user id entry to 1000.</p>

<p><strong>Please take utmost care when editing this file, as it potentially could break your Synology install and lock you out of it!</strong></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>vi /etc/passwd
</code></pre></div></div>

<p>Find the <code class="language-plaintext highlighter-rouge">git</code> line and change it to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git:x:1000:100::/var/services/homes/git:/bin/sh
</code></pre></div></div>

<p>This a changes the git user id to 1000 and the login shell to a valid value.</p>

<p>Now is a good time to restart the ssh service using</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>synoservicectl <span class="nt">--restart</span> sshd
</code></pre></div></div>

<h1 id="forwarding-to-gitea">Forwarding to Gitea</h1>

<p>This is the tricky part I didn’t understand at first: An entry in the authorized_keys file can have additional parameters, like restricting the user to certain commands after auth.</p>

<p>We’ll create our own wrapper to forward to Gitea and restrict the git user to only be able to execute this one command that connects through to Gitea as <code class="language-plaintext highlighter-rouge">/usr/local/bin/gitea-serv</code>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>vi /usr/local/bin/gitea-serv
</code></pre></div></div>

<p>And put the following content there:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
<span class="nb">exec</span> /volume1/@appstore/ContainerManager/usr/bin/docker <span class="nb">exec</span> <span class="nt">-i</span> <span class="nt">-u</span> git <span class="nt">-e</span> <span class="nv">SSH_ORIGINAL_COMMAND</span><span class="o">=</span><span class="s2">"</span><span class="nv">$SSH_ORIGINAL_COMMAND</span><span class="s2">"</span> gitea /app/gitea/gitea serv <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span> <span class="nt">--config</span> /data/gitea/conf/app.ini
</code></pre></div></div>

<p>Make it executable:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chmod </span>755 /usr/local/bin/gitea-serv
</code></pre></div></div>

<h1 id="allow-gitea-serv-in-sudoers">Allow <em>gitea-serv</em> in sudoers</h1>

<p>By default no user can access Gitea (or any other Docker container for that matter). We could add the git user to the docker group, but that would give it full access to any Container. We don’t want that. But we can give it sudo access without password check to the <em>gitea-serv</em> command:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>sh <span class="nt">-c</span> <span class="s1">'cat &gt; /etc/sudoers.d/git-gitea &lt;&lt; EOF
git ALL=(root) NOPASSWD: /usr/local/bin/gitea-serv
Defaults&gt;root env_keep+=SSH_ORIGINAL_COMMAND
EOF'</span>
</code></pre></div></div>

<p>Verify it works by switching to the git user and testing:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>su <span class="nt">-s</span> /bin/sh git <span class="nt">-c</span> <span class="s1">'sudo /usr/local/bin/gitea-serv serv key-1'</span>
</code></pre></div></div>

<p>If it works the expected output should be something like <code class="language-plaintext highlighter-rouge">Hi there, &lt;username&gt;! You've successfully authenticated...</code>. The actual name doesn’t matter, as <em>key-1</em> is just the first key that Gitea has stored.</p>

<p>If it didn’t work yet, it’ll probably sort itself out with the next step.</p>

<h1 id="mount-the-git-users-ssh-directory-into-the-gitea-container">Mount the git users’ .ssh directory into the Gitea container</h1>

<p>I use docker-compose for the Gitea install, so I had to add this in the volumes section:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">volumes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">other volumes</span>
  <span class="pi">-</span> <span class="s">/var/services/homes/git/.ssh:/data/git/.ssh:rw</span>
</code></pre></div></div>

<p>But if you use plain old Docker, just add the directory in there.</p>

<p>Please note: You have to mount the entire .ssh dir, as with file-based mounts Gitea has issues writing the keys.</p>

<h1 id="update-giteas-configuration">Update Gitea’s configuration</h1>

<p>As already mentioned earlier, we have to tell Gitea to write the correct command (our gitea-serv) to the authorized_keys file, and we have to disable the built-in ssh server.</p>

<p>In my case I updated the docker-compose.yaml again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>environment:
  - SSH_DOMAIN=${ssh domain}
  - SSH_PORT=22 # change this to whatever your Synology SSH daemon listens to
  - START_SSH_SERVER=false # Gitea's own SSH server not needed
</code></pre></div></div>

<p>The first two env variables do not have any effect other than to show up in the UI as clone URL. The second disables the built-in SSH server.</p>

<p>The other part of the Gitea config lives in the <em>app.ini</em> file:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[server]</span><span class="w">
</span><span class="py">SSH_CREATE_AUTHORIZED_KEYS_FILE</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">true</span>
<span class="py">SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">sudo /usr/local/bin/gitea-serv key-{{.Key.ID}}</span>
</code></pre></div></div>

<p>Now it is time to restart (or in case of docker-compose redeploy) Gitea:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> <span class="o">{</span>location of your docker-compose.yaml<span class="o">}</span> <span class="o">&amp;&amp;</span> docker compose restart
</code></pre></div></div>

<h1 id="populate-authorized_keys">Populate authorized_keys</h1>

<p>Whenever a key is removed or added in the Gitea webinterface the authorized_keys will be regenerated. But the regeneration can also be invoked from the CLI:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-u</span> git gitea /app/gitea/gitea admin regenerate keys <span class="nt">--config</span> /data/gitea/conf/app.ini
</code></pre></div></div>

<p>Verify the file was written correctly:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec </span>gitea <span class="nb">cat</span> /data/git/.ssh/authorized_keys | <span class="nb">head</span> <span class="nt">-3</span>
</code></pre></div></div>

<p>Expected format should be:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># gitea public key
command="sudo /usr/local/bin/gitea-serv key-1",no-port-forwarding,...,restrict ssh-ed25519 AAAA... user-1
</code></pre></div></div>

<h1 id="test-the-connection">Test the connection</h1>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-T</span> git@<span class="k">${</span><span class="nv">your</span><span class="p">-server</span><span class="k">}</span>
</code></pre></div></div>

<p>Expected output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hi there, jan! You've successfully authenticated with the key named ${your-key-name},
but Gitea does not provide shell access.
</code></pre></div></div>

<p>And your regular users should also still be able to connect:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="o">{</span>non-git-user<span class="o">}</span>@<span class="o">{</span>your-server<span class="o">}</span>
</code></pre></div></div>

<p>I am so happy works! I now can ssh into the machine using any other user and the HTTPS/SSH clone URLs finally work as expected. Didn’t have to do weird redirects, the git user and the sudo part is restricted to this one functionality. And Gitea is now also part of Synology’s fail2ban mechanism.</p>

<h1 id="troubleshooting">Troubleshooting</h1>

<p>These are the steps I took to troubleshoot issues with the setup.</p>

<h2 id="permission-denied-on-connect">Permission denied on connect</h2>

<p>Check what the sshd logs say:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>journalctl <span class="nt">-u</span> sshd.service <span class="nt">--since</span> <span class="s2">"5 minutes ago"</span>
</code></pre></div></div>

<h2 id="bad-acl-permission">bad ACL permission</h2>

<p>Try removing the Extended ACLs again: <code class="language-plaintext highlighter-rouge">synoacltool -del /var/services/homes/git</code></p>

<h2 id="could-not-open-authorized-keys">Could not open authorized keys”</h2>

<p>Check ownership: <code class="language-plaintext highlighter-rouge">sudo chown -R 1000 /var/services/homes/git/.ssh</code></p>

<h2 id="key-not-found">Key not found</h2>

<p>Regenerate keys: <code class="language-plaintext highlighter-rouge">docker exec -u git gitea /app/gitea/gitea admin regenerate keys --config /data/gitea/conf/app.ini</code></p>

<h2 id="git-pushpull-shows-gitea-welcome-message-instead-of-working">Git push/pull shows Gitea welcome message instead of working</h2>

<p><code class="language-plaintext highlighter-rouge">SSH_ORIGINAL_COMMAND</code> is not being passed. Verify sudoers contains:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Defaults&gt;root env_keep+=SSH_ORIGINAL_COMMAND
</code></pre></div></div>

<h2 id="setup-broken-after-dsm-update">Setup broken after DSM update</h2>

<p>Re-do the steps in <a href="#preparing-the-git-user-account-settings">Preparing the git user account settings</a></p>

<h2 id="wrapper-script-docker-not-found">Wrapper script: docker not found</h2>

<p>Verify the Docker binary path:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">readlink</span> <span class="nt">-f</span> /usr/local/bin/docker
</code></pre></div></div>

<p>Update the path in <code class="language-plaintext highlighter-rouge">/usr/local/bin/gitea-serv</code> accordingly. On Synology with Container Manager it is typically:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/volume1/@appstore/ContainerManager/usr/bin/docker
</code></pre></div></div>]]></content><author><name>Jan Seeger</name></author><category term="syslog" /><category term="ssh" /><category term="gitea" /><category term="git" /><summary type="html"><![CDATA[For years my Gitea instance (in Docker) did have proper HTTPS clone URLs using a Reverse Proxy that handles domain and SSL termination, but I could never get SSH to work properly and had to expose the Gitea built-in SSH server on a different port.]]></summary></entry><entry><title type="html">Migrating Synology from Wasabi to HyperBackup Vault</title><link href="https://jan.alphadev.net/blog/2026/migrating-wasabi-synology-hyperbackup/" rel="alternate" type="text/html" title="Migrating Synology from Wasabi to HyperBackup Vault" /><published>2026-03-13T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/migrating-wasabi-synology-hyperbackup</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/migrating-wasabi-synology-hyperbackup/"><![CDATA[<p>New record: 3rd <a href="/category/syslog">syslog Post</a> in 3 days.</p>

<p><a href="https://wasabi.com/">Wasabi</a> used to have a nice offer 4$/TB/month, S3 interface and a few years ago they even started to offer their services in European data centers. And because Synology HyperBackup natively supports S3 with custom endpoints it was a breeze to set up.</p>

<p>But over the years they have increased their prices, while at the same time their service got worse year over year.</p>

<p>When I got my second Synology, the plan was to allocate a small portion of storage on each machine and receive the backups of the other. And since both Synologys are hundreds of kilometers apart, it would even count as proper off-site backup.</p>

<p>Reddit is full of both reports that it works reliably, and that it always refuses connect from one machine to the other. However all agree that for it to work ports 5001 <em>(HTTPS Webinterface)</em> and 6281 <em>(HyperBackup Vault)</em> have to be reachable.</p>

<p>In my case, the port 5001 isn’t reachable from the internet, but a reverse proxy terminates the traffic on a subdomain using  <acronym title="Server Name Indication">SNI</acronym>.</p>

<p>Eventually I gave up, since Wasabi did mostly work, and a working but increasingly expensive backup is better than a free one that isn’t reliable. But then the E-Mails about failing backup jobs slowly started to pile up, until I was fed up and had another go.</p>

<h1 id="tailnet">Tailnet</h1>

<p>I had <a href="https://tailscale.com/">Tailscale</a> installed on both machines. Normally used as Exit Node when using unencrypted WiFis or when I’m abroad and need an IP that is located in Germany. But due to the way Synology has set up the networking on their devices, I could connect from the Tailnet into the NAS but not from the NAS out to the Tailnet.</p>

<p>The <a href="https://tailscale.com/docs/integrations/synology#enable-outbound-connections">Tailscale Docs describe a workaround</a> which involves creating the needed tun device on each boot. Now that they can see each other, we can use the Tailnet link-local IPv6 to set up the backup. Since the SSL cert for the login frame (and in my case even worse: PassKey auth) is broken, you’ll have to use the “Password Login” option.</p>

<p><img src="/assets/2026/a28e49cb-1556-4cde-97d7-440d29939d62.jpg" alt="HyperBackup" /></p>]]></content><author><name>Jan Seeger</name></author><category term="syslog" /><category term="synology" /><summary type="html"><![CDATA[New record: 3rd syslog Post in 3 days.]]></summary></entry><entry><title type="html">The Power Woes continue</title><link href="https://jan.alphadev.net/blog/2026/power-outage/" rel="alternate" type="text/html" title="The Power Woes continue" /><published>2026-03-12T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/power-outage</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/power-outage/"><![CDATA[<p>Around mid-day the power went out <a href="/blog/2025/power-woes/">again</a>. I searched the internet for a cause, or any mention of an outage or power grid issue. Came up empty-handed.</p>

<p><img src="/assets/2026/95433e78-e87c-4640-bf66-4f8d5c03ecc8.jpeg" alt="Syslog: Server ran off battery power between 12:49:41 and 12:49:36 on the 12th of March 2026" /></p>

<p>The power went out during my lunch break. One of the rare occasions where I noticed it before the <acronym title="Uninterruptible Power Supply">UPS</acronym> informed me.</p>]]></content><author><name>Jan Seeger</name></author><category term="syslog" /><category term="ups" /><summary type="html"><![CDATA[Around mid-day the power went out again. I searched the internet for a cause, or any mention of an outage or power grid issue. Came up empty-handed.]]></summary></entry><entry><title type="html">Kotlin + Ktor + Hetzner + Coolify = ❤️</title><link href="https://jan.alphadev.net/blog/2026/coolify/" rel="alternate" type="text/html" title="Kotlin + Ktor + Hetzner + Coolify = ❤️" /><published>2026-03-11T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/coolify</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/coolify/"><![CDATA[<h1 id="web-apps">Web Apps</h1>

<p>For simplicity sake my default way of publishing was with static sites on either GitLab Pages or GitHub Pages. But for deploying web apps I just didn’t have a nice way.</p>

<p>There were unsuccessful attempts with serverless Cloud providers, that charge by the CPU second, but that means I don’t get to have any application state, because the instances are short-lived. And the deployment itself was always fiddly and couldn’t be relied on.</p>

<p>I had stumbled on <a href="https://coolify.io">Coolify</a> earlier before and had wanted to give it a try. But when I found out you can have Hetzner pre-install one when configuring a shared server with them, there was just no reason not to try.</p>

<p>Setup took a few minutes, and now all I have to do is to add a tiny Dockerfile to a Kotlin repo, the usual: pull temurin, build the app, run the jar, expose port 8080 and set up a new App in Coolify. Add a domain name and optionally a Webhook that triggers a rebuild when the code changes, and off we go.</p>

<p>Coolify makes this feel outright casual and effortless. Need a database side-car along with it? Coolify has you covered. Custom config for the reverse proxy? Easily done.</p>

<h1 id="static-site-hosting">Static Site Hosting</h1>

<p>After having migrated from <a href="/blog/2024/good-bye-neocities/">Neocities to GitLab Pages</a>, I had (somewhat jokingly) mused on self-hosting again. And since it worked so well with the custom web apps, I had to try it with a static site.</p>

<p>And with some help from Claude this now runs on my own host. <del>The SSL score is atrocious</del><sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> and I am not fully happy with the setup, but at least it is mine. Instead of using the static site deploy type, I built a bespoke Docker container with nginx.</p>

<p>The convoluted GitLab Pipeline process with multiple containers and interwoven includes is now handled by a small multi-step Dockerfile that contains a total of 19 lines of code for building, deploying and running everything!</p>

<h1 id="batteries-included-apps">Batteries-included Apps</h1>

<p>It even comes with pre-configured apps. You can spin up a complete <a href="https://pocket-id.org">PocketID</a> instance in a few seconds, ready to go.</p>

<p>I always wanted to try <a href="https://appwrite.io">AppWrite</a> or <a href="https://supabase.com">Supabase</a> for small personal projects. Now I can deploy one with a few clicks, play around and tear it down with as few clicks again.</p>

<h1 id="downsides">Downsides</h1>

<p>Coolify is written in PHP.  I’d much prefer something written in a more robust language, but it works.</p>

<p>It does work with arbitrary Git Servers, like Gitea, but takes some coercion and finagling to get it working, while it is clearly designed to be used with the GitHub integration that comes with it.</p>

<p>Note: Gitea doesn’t (yet) have a dedicated Coolify target for WebHooks but will work when selecting “Gitea” as target.</p>

<p>ALL the different http apps are in use. Coolify uses Caddy as frontend, Traefik as backend and the Docker Images usually have their own Webserver or Reverse Proxy in it. It works but that is more moving pieces than I would have liked my setup to have. If any one of those has a security vulnerability the entire stack is open to attack.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><a href="https://www.ssllabs.com/ssltest/analyze.html?d=jan.alphadev.net&amp;latest">Has been fixed since</a>, using a custom Reverse Proxy middleware config. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Jan Seeger</name></author><category term="syslog" /><category term="kotlin" /><category term="ktor" /><category term="hetzner" /><category term="coolify" /><summary type="html"><![CDATA[Web Apps]]></summary></entry><entry><title type="html">First Motorcycle ride of the year</title><link href="https://jan.alphadev.net/blog/2026/first-motorcycle-trip/" rel="alternate" type="text/html" title="First Motorcycle ride of the year" /><published>2026-03-07T00:00:00+00:00</published><updated>2026-06-06T03:00:43+00:00</updated><id>https://jan.alphadev.net/blog/2026/first-motorcycle-trip</id><content type="html" xml:base="https://jan.alphadev.net/blog/2026/first-motorcycle-trip/"><![CDATA[<p>The first somewhat ridable days were weeks ago, but I was either gone or didn’t have the time.</p>

<p>Today everything serendipitously fell into place: yesterday was warm enough, no frost over night, nice weather on a lazy Saturday afternoon and a buddy of mine was also planning on a short ride.</p>

<p>Started the ride at 80% battery, and rode 55 kilometers in total, down to about 50%. We took it slowly on backroads and gradually increased the riding difficulty. We had all kinds of terrain, flat portions and up and down the hills, easy curves, turnpikes. By the end the sun was about to set and the temperatures started to drop from the 17 degrees we had set out at, down to 13 degrees.</p>

<p>Compared to my (also electric) winter car, it is noticeable how much better the motor and the BMC performs. Not only in output power but in estimating, recouping and saving power.</p>

<p>It was fun every minute of the ride and I’m looking forward to the warmer months, when the conditions are even better and the sun is up for longer.</p>]]></content><author><name>Jan Seeger</name></author><category term="trip report" /><category term="motorcycle" /><category term="zero s 2024" /><summary type="html"><![CDATA[The first somewhat ridable days were weeks ago, but I was either gone or didn’t have the time.]]></summary></entry></feed>