Feuerfest

Just the private blog of a Linux sysadmin

Termix: SelfHosted connection manager

I finally got around setting myself up with a Termix instance (their GitHub). Its a connection manager for various protocols (SSH, RDP, Telnet, etc.) accessible via a web-frontend. Termix it self runs inside a Docker container.

Here is a view of the web-frontend (I resized the window to make it smaller). I generated a new SSH-Key solely for the use connecting from Termix to the configured hosts. Then added the public key to 2 hosts, put them inside a folder for better overview, hit connect and it works.

It supports the creation of tunnels too and various other options. So far I have only used it with SSH so I can't say much regarding RDP (or Telnet 😂). Having this reachable via HTTPS could be a nice solution in environments where direct SSH (and VPN) is blocked.

The docker compose file

I configured SSL with certificates from my own CA. These are mounted read-only into the container under /certs. This all works without Traefik, Caddy or Nginx for SSL.

services:
  termix:
    image: ghcr.io/lukegus/termix:latest
    container_name: termix
    restart: unless-stopped
    environment:
      - ENABLE_SSL=true
      - SSL_PORT=8443
      - SSL_DOMAIN=host.tld
      - PORT=8080
      - SSL_CERT_PATH=/certs/host.tld.crt
      - SSL_KEY_PATH=/certs/host.tld.key
    ports:
      - "6666:8443"
    volumes:
      - /opt/docker/termix/data:/app/data
      # Mount cert-dir for certificates read-only
      - /opt/docker/certs/:/certs:ro

A welcomed surprise

I was pleasantly surprised to notice that the Termix docker container automatically reported "Healthy" inside my dashboard. Without me ever having defined a proper healthcheck.

Turns out Termix is one of these rare projects who define a healthcheck in the container image itself:

root@host:~# docker inspect termix | grep -A 20 Healthcheck
            "Healthcheck": {
                "Test": [
                    "CMD-SHELL",
                    "wget -q -O /dev/null http://localhost:30001/health || exit 1"
                ],
                "Interval": 30000000000,
                "Timeout": 10000000000,
                "StartPeriod": 60000000000,
                "Retries": 3
            },

Nice!

Comments

WHATWG, Firefox and bad ports

When I setup my Termix instance I used port 6666/tcp. However on my first visit I wasn't greeted with a Termix login page, rather a Firefox message appeared. One I had never encountered before.

Huh? What? I use all kinds of strange ports in my home network and never got that error message.

I was kind of annoyed that there was no button labeled "I know the risk, take me there anyway".

However a quick search showed the solution.

  1. Open about:config
  2. Enter: network.security.ports.banned.override
    • The key doesn't exist per-default
  3. Create it as type "String"
  4. Add the port number
    • If multiple ports are needed specify them as a comma separated list: 6666,7777

This is how it looks in my case:

What ports are blocked? And why?

If we look at the source code, we see the list of ports that is blocked: https://searchfox.org/firefox-main/source/netwerk/base/nsIOService.cpp#122

In total just shy over 80 ports are blocked. And there seems to be no separation between UDP or TCP ports.

A bit more Firefox context is in their Knowledge Base: https://kb.mozillazine.org/Network.security.ports.banned

They get this port list from the "The Web Hypertext Application Technology Working Group (WHATWG)" who define a list of "bad ports" in this document: https://fetch.spec.whatwg.org/#port-blocking

Apparently "A port is a bad port if it is listed in the first column of the following table.", well you never stop learning. 😉

Comments

OpenSSL error "error 47 at 0 depth lookup: permitted subtree violation" explained, or: Why I have to generate a new CA root certificate

I wanted to get rid of the HTTPS-Warning when opening the web-frontend of my DSL-Router. As I still use the vendor-supplied selfsigned certificate there. Hence I used my ca-scripts (GitHub) to generate a certificate for the IP and standard hostname (fritz.box).

Only to get the error:

error 47 at 0 depth lookup: permitted subtree violation
error 192.168.1.1.crt: verification failed

Huh? This is how I used my script. hostcert.sh calls sign.sh to sign the CSR and verifies the signed certificate against the CA-Root certificate.

root@host:~/ca# ./hostcert.sh 192.168.1.1 fritz.box
CN: 192.168.1.1
DNS ANs: fritz.box
IP ANs: 192.168.1.1
Enter to confirm.

writing RSA key
Reading pass from $CAPASS
CA signing: 192.168.1.1.csr -> 192.168.1.1.crt:
Using configuration from ca.config
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
countryName           :PRINTABLE:'DE'
localityName          :ASN.1 12:'Karlsruhe'
organizationName      :ASN.1 12:'LAN CA host cert'
commonName            :ASN.1 12:'192.168.1.1'
Certificate is to be certified until Mar 14 20:57:03 2027 GMT (365 days)

Write out database with 1 new entries
Database updated
CA verifying: 192.168.1.1.crt <-> CA cert
C=DE, L=Karlsruhe, O=LAN CA host cert, CN=192.168.1.1
error 47 at 0 depth lookup: permitted subtree violation
error 192.168.1.1.crt: verification failed

The offending command is:

root@host:~/ca# openssl verify -CAfile ca.crt fritz.box.crt 
C=DE, L=Karlsruhe, O=LAN CA host cert, CN=fritz.box
error 47 at 0 depth lookup: permitted subtree violation
error fritz.box.crt: verification failed

The root cause is that I forgot that I added an X509v3 Name Constraints. This dictates that all Common Name or SubjectAltNames, have to end in .lan and clearly fritz.box is in violation of that.

root@host:~/ca# openssl x509 -in ca.crt -text | grep "X509v3 Name" -A2
            X509v3 Name Constraints: 
                Permitted:
                  DNS:lan

The solution is to generate it solely for the IP, right?

root@host:~/ca# ./hostcert.sh 192.168.1.1
CA verifying: 192.168.1.1.crt <-> CA cert
C=DE, L=Karlsruhe, O=LAN CA host cert, CN=192.168.1.1
error 47 at 0 depth lookup: permitted subtree violation
error 192.168.1.1.crt: verification failed

Yeah no.. It's wrong too. In the first certificate the IP was also defined. I just thought fritz.box is the offending SAN as it is listed first (my script adds IP SANs after DNS SANs).

Through this I learned that as soon as one name constraint is specified, all SubjectAltNames have to follow the constraints. Constraints of type DNS and IPAddress are checked independently. And 192.168.1.1 doesn't match the Permitted DNS zone of .lan.

The corresponding RFC 5280 sections are:

Looks like I have to generate a new CA. Narf! This time however, I will make sure to extract all allowed and denied name constraints from the CA root certificate and check it against the supplied SubjectAltName BEFORE I create or sign the CSR.

Comments

My n8n docker compose file (without caddy, traefik, nginx, etc. for SSL)

This is my docker compose file for n8n. I use certs signed by my own private CA via a mounted folder.

services:
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=change-me
      - N8N_HOST=HOST
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://HOST:5678/
      - GENERIC_TIMEZONE=Europe/Berlin
      - N8N_SSL_CERT=/certs/HOST.crt
      - N8N_SSL_KEY=/certs/HOST.key
      # Enable nodes "Execute Command" and "Local File Trigger"
      - NODES_EXCLUDE=[]
    volumes:
      - /opt/docker/n8n/n8n_data:/home/node/.n8n
      - /opt/docker/n8n/local-files:/files
      # Mount cert-dir read-only for certificates
      - /opt/docker/certs/:/certs:ro
    healthcheck:
      # No curl in n8n container
      test: ["CMD-SHELL", "wget --no-check-certificate --quiet -O - https://HOST:5678/healthz | grep -q '\"ok\"' || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
Comments

Arschlochhandwerker

Die Stadtwerke Karlsruhe wollen eine Nachzahlung von mir. Immerhin gut 300€. Grund? Mehrverbrauch beim Gas. Hatte ich die 2 Jahre zuvor einen Verbrauch zwischen 5300 bis 5700 kwh, stiegt dieser nun auf 8736 kwh.

Was man dem Graphen auch entnehmen kann, ist, das ich die Jahre über an den Einstellungen gedreht habe, bis ich dann Ende 2023 zufrieden war mit den Temperaturen und dem Verbrauch.

Wo kommt der höhere Verbrauch her?

Nanu? Was ist denn da passiert? An den Heizungseinstellungen hatte ich nichts geändert. Die Werte für die automatische Tag- und Nachttemperatur sind weiterhin bei 19°C bzw. 21°C. Merkwürdig.

Also schaut man sich die Therme an und die Stimmung schlägt sofort in Hass um.

Der Drehregler für die Heizungsvorlauftemperatur steht, auf maximalem Anschlag, bei 75°C. Das war definitiv nicht so und wurde definitiv nicht von mir geändert. Ich hatte hier 60°C eingestellt. Das reicht völlig aufgrund des niedrigen Unterschieds zwischen Tages- und Nachttemperatur. Die Heizung muss hier nur 2°C ausgleichen. Und ob das nun 1 oder 3 Stunden dauert ist morgens bzw. Abends meist völlig egal.

Zudem sind die Heizkörper erst knapp 15 Jahre alt und somit eher neuer. Und als Faustregel gilt auch: Je neuer die Heizkörper, desto eine niedrigere Vorlauftemperatur wird benötigt.

Die letzte Wartung war im März 2025. Die neue Abrechnungsperiode war da erst 2 Monate alt. Klasse. Aber immerhin eine Erklärung.

Nur können die 15°C mehr bei der Heizungsvorlauftemperatur wirklich einen Mehrverbrauch von 3033 kwh bzw. 53,2% erklären?

Also ein wenig im Internet gesucht und ja, es liegt zumindest im Bereich des möglichen. Je nach Quelle werden Werte von 2,5% bis 5% mehr Energieverbrauch pro Grad Vorlauftemperatur erwähnt.

So und um meinen Hass-Gefühlen jetzt nachzugehen überlege ich ernsthaft ob man den Wartungstechniker auf Schadensersatz verklagen kann. Die vernünftigere Seite meines Charakters hat sich stattdessen eben den Labeldrucker geschnappt und.. Naja seht selbst. 😁

Comments

KI in der Medizin ohne Widerspruchsrecht? - Nein, danke!

Mal wieder eine Zahnarztstory...

Ich müsste mal wieder zum Zahnarzt. Mein bisheriger Zahnarzt aber hat seine Praxis vor 2 Jahren verkauft. Der neue Betreiber ist eine Art Zahnarzt-Kette. Mit aktuell 79 "Filialen" in Deutschland. Beim ersten Besuch nach dem Verkauf wurde mir direkt eine Zahnprothese empfohlen. Ich willigte in die Erstellung eines kostenlosen, unverbindlichen Kostenvoranschlages ein. Einerseits aus Neugierde, andererseits um etwas konkretes für eine Zweitmeinung zu haben.

Denn irgendwie fühlte sich das alles merkwürdig an. Wieso sollte sich mein Zahn innerhalb von 6 Monaten, seit dem letzten Kontrolltermin, so verschlechtert haben, das nun eine Prothese notwendig ist? Mein bisheriger Zahnarzt war bei sowas immer proaktiv mit Informationen und meinte sinngemäß während Corona: "Irgendwann wird die Füllung nicht mehr reichen. Da ist eben einfach nicht mehr viel Zahn vorhanden. Dann werden wir über eine Prothese oder Brücke nachdenken müssen. Aber das wird vermutlich erst so in 10-15 Jahren der Fall sein."

Und dann beim ersten Besuch unter neuer Führung, nur 3 Jahre später, muss der Zahn quasi sofort raus? Eine Brücke wird als Möglichkeit ebenfalls nicht erwähnt? Das war nicht gut und rief nur schlechte Erinnerungen in mir hervor. Am Zahn hat sich nichts geändert. Keine neue Bohrung oder Füllung, kein Karies, keine sonstigen Beschwerden, nichts.

Nur dieser Kostenvoranschlag kam leider nie. Das ist in doppelter Hinsicht schlecht. Entweder bestand also kein wirkliches medizinisches Problem. Oder man hat es schlicht und einfach vergessen. Beides absolute NoGos für mich bei einem Arzt. Wenn ich mich bei einem Arzt auf eines verlassen können muss, dann ist es die Arbeitsweise.

Also suchte ich einen neuen Zahnarzt hier in Karlsruhe. Relativ schnell fand ich eine sympathisch wirkende Praxis in der Nähe, die ich anrief.

Es klingelte länger. Dann begrüßte mich der digitale KI-Assistent von Doctolib und teilte mir mit, dass das Gespräch "zu Trainingszwecken" aufgezeichnet wird.
Eine Möglichkeit zum Widerspruch wurde nicht erwähnt.

Ich war verdutzt, etwas erschrocken, legte aber sofort auf. KI an so einer kritischen Stelle? Ohne Möglichkeit des Widerspruchs das meine Daten zu KI-Trainingszwecken verwendet werden? Bei einem Arzt? Nein, danke! (Gefühlsmäßig eher ein: Fickt euch!)

Erst danach realisierte ich, was mich noch so aufregte. Das die Formulierung "zu Trainingszwecken" hier unter Garantie so verwendet wird, um über das, was eigentlich passiert, hinweg zu täuschen. Wir alle kennen diese Formulierung aus den Hotlines diverser Firmen. "Dieses Gespräch kann zu Trainingszwecken oder Qualitätsgründen aufgezeichnet werden. Wünschen Sie dies nicht, drücken Sie die 1." Kennen wir alle. Lieben wir alle. Nur das es sich in diesem Fall um die Ausbildung bzw. Schulung von neuen Mitarbeiter handelt. Oder um stichprobenartig die Qualität zu überprüfen oder eben bei Beschwerden.

Und ja, klar. Natürlich kann ich nicht wissen ob das Drücken der Taste 1 tatsächlich etwas bewirkt, oder nur ein digitales, datenschutzrechtliches Placebo ist. Noch ob die Daten auch wirklich nur dafür verwendet werden.
Wir sind aber zumindest meistens fein damit, weil irgendwie müssen Menschen eben angelernt werden.

In diesem Fall trifft aber absolut rein gar nichts davon zu. Irgendein Anbieter verwendet meine intimen Gesundheitsdaten um "irgendwas" damit zu machen. Mindestens aber um sein Produkt zu verbessern. Ein Produkt für dessen Nutzung er bereits vom einsetzenden Arzt Geld bekommen hat! Wieso muss ich also noch mit meinen Daten zahlen? Und dann auch ausgerechnet noch in der Medizin? Dem kritischsten Bereichen von allen? 

Kurz: Ich rief bei einem anderen Zahnarzt an.

Hier begrüßte mich ein sehr freundlicher Mensch.

Comments