Neuer Chart: canarytokens mit mitgeliefertem oauth2-proxy #17

Open
opened 2026-09-16 16:03:31 +02:00 by jlab-bot · 0 comments
Member

Migriert aus jk-cluster/helm-charts#237 — urspruenglich angelegt von jk-cluster-bot am 2026-09-04.

Verweise der Form #123 im folgenden Text beziehen sich auf die GitHub-Nummerierung, nicht auf die Nummern in diesem Repo. Geschlossene Issues wurden nicht mitgezogen und sind weiterhin nur auf GitHub lesbar.


Neuer Chart für Canarytokens (Thinkst), abgesichert durch einen mitgelieferten oauth2-proxy — analog zu ytdlp-web-player, aber mit einer entscheidenden Einschränkung, die dort nicht galt.

Die Falle, die alles andere bestimmt

Bei ytdlp-web-player sitzt der oauth2-proxy im Datenpfad vor der ganzen Anwendung. Hier darf er das nicht. Ein Canarytoken feuert, weil ein Fremder eine URL abruft. Steht der Proxy davor, bekommt der Angreifer eine Anmeldemaske statt eines ausgelösten Alarms — der Chart wäre lauffähig und trotzdem vollständig nutzlos.

Die Trennlinie hat der Upstream bereits gezogen, in nginx/nginx.conf: eine feste Pfadliste geht ans Frontend, alles andere ans Switchboard. Daran hängt die Auth:

  • Nur die Frontend-Pfade laufen über den Proxy: /generate, /manage, /download, /history, /settings, /resources, /legal, dazu die Startseite und der API-Pfad.
  • Alles andere geht direkt und unauthentifiziert ans Switchboard. Ein Token, das erst nach Anmeldung feuert, ist kein Token.

Umgesetzt wird das im Ingress, nicht im Proxy. Der naheliegende Gegenvorschlag — alles durch den Proxy und die Auslöser über skip_auth_routes freistellen — scheitert daran, dass Token-URLs beliebige Pfade sind. Entscheidend ist aber das Ausfallverhalten: teilt der Ingress auf, sieht der Proxy nie Auslöser-Verkehr. Fällt er aus, ist die Verwaltung gesperrt, die Token feuern weiter. Andersherum wäre ein kaputter Proxy ein stiller Totalausfall der eigentlichen Funktion.

Aufbau

Ein Image (thinkst/canarytokens) in zwei Rollen: Frontend (Deployment, Port 8082) und Switchboard (eigenes Kommando, die Kanal-Ports). Dazu Redis als StatefulSet mit PVC — dort liegen die Token selbst. REDIS_HOST/REDIS_PORT/REDIS_DB sind Werte, eine externe Instanz bleibt also möglich.

Der Upstream-nginx entfällt. Seine einzige Aufgabe ist das Routing oben plus client_max_body_size 50M; das sagt ein Ingress direkt (proxy-body-size). Er wird im Compose ohnehin lokal gebaut — ihn zu übernehmen hieße, ein eigenes Image zu bauen und zu hosten.

Ports — gemessen, nicht vermutet

Kanal Port beweglich?
DNS 53 nein — fremde Resolver fragen nicht nach dem Port
SMTP 25 nein — fremde Mailserver liefern an 25
WireGuard 51820 nein, siehe unten
MySQL 3306 ja — frontend/app.py schreibt CHANNEL_MYSQL_PORT in das Artefakt
Kubeconfig 6443 ja — channel_input_mtls.py gibt denselben Wert an Listener und ausgegebene URL
HTTP 8083 irrelevant, läuft über den Ingress

Dass Kubeconfig beweglich ist, räumt nebenbei eine echte Kollision ab: 6443 ist auf einem k3s-Knoten der Port des kube-apiservers.

WireGuard ist ein Upstream-Fehler, kein Designentscheid. canarytokens/wireguard.py schreibt in die ausgegebene Konfiguration Endpoint = {public_ip}:{DEFAULT_PORT} — eine Modulkonstante, nicht die Einstellung. CANARY_CHANNEL_WIREGUARD_PORT verschiebt also nur den Listener; das Artefakt zeigt weiter auf 51820. Wer die Einstellung ändert, verteilt still Konfigurationen, die ins Leere zeigen. Das gehört in values.yaml und NOTES.

Konsequenz für die Freigabe nach außen: Service-Typ und alle Ports werden Werte. Der Chart legt sich weder auf NodePort noch LoadBalancer noch hostPort fest. Für NodePort ist zu wissen: 25 und 53 liegen unter 30000, 51820 über 32767 — ein Bereich, der alle drei umfasst, wäre die Abschaffung der Grenze, nicht ihre Erweiterung. Verzichtet man auf WireGuard-Token, bleiben zwei Ports und eine eng begründbare Erweiterung.

Image-Pinning per Digest

thinkst/canarytokens hat 1885 Tags und keinen einzigen Versionstag — nur latest, master, dev und sha-<commit>. Ein sha--Tag ist unveränderlich, aber für Renovate nicht ordenbar; genau daran ist dieses Repo in der searxng-Runde schon einmal gescheitert.

Deshalb: per Digest pinnen (thinkst/canarytokens@sha256:…). Unveränderlich, und Renovate kann Digests eigenständig anheben. Die appVersion trägt dann keine sprechende Nummer — der Grund gehört als Kommentar in Chart.yaml.

Zu belegen bei der Umsetzung

  1. Der Auth-Schnitt. Gerendert zeigen, dass ein Frontend-Pfad über den Proxy geht und ein beliebiger anderer Pfad nicht. Das ist der Kern des Charts.
  2. Die Probes. Es gibt keinen Health-Endpunkt — kein /healthz, kein /ping. Und das Switchboard auf seinem HTTP-Kanal anzusprechen kann Alarme erzeugen, das ist der Auslöser-Pfad. Ein tcpSocket beweist den Listener, ohne eine Anfrage zu senden, die als Token-Treffer durchgeht — dasselbe Muster wie bei teamspeak-server, dort aus demselben Grund.
  3. Das Digest-Pinning mit Renovate, im Trockenlauf. Eine gültige Regel ist keine treffende Regel.

Abhängigkeit: die bekannten Kosten

Nach CLAUDE.md ist die Abhängigkeit optional und standardmäßig aus, über condition:. Ich halte mich daran, halte es hier aber für unbequem: eine ungeschützte Instanz lässt jeden, der den Host erreicht, Token in den eigenen Domains erzeugen. Das gehört entsprechend deutlich in values.yaml und NOTES.

Dazu die Kosten aus der ytdlp-Runde: ein Unterchart ist für Fleet unsichtbar — eine neue Proxy-Version erreicht den Cluster erst, wenn der äußere Chart neu veröffentlicht wird. Der gepinnten Version gehört derselbe „KEEP THIS VERSION MOVING"-Kommentar. Und: config.upstreams wird mit toJson gerendert, ohne tpl — ein {{ .Release.Name }} landet dort wörtlich; die vom Release abgeleitete Frontend-Adresse muss über config.configFile.

Voraussetzungen außerhalb des Charts

Eine öffentliche IP, auf die CANARY_PUBLIC_IP zeigt, und Domains, deren NS-Einträge dorthin delegieren. Ohne beides ist der Chart wertlos — das gehört in die NOTES, nicht in die Annahme.

> Migriert aus [`jk-cluster/helm-charts#237`](https://github.com/jk-cluster/helm-charts/issues/237) — urspruenglich angelegt von **jk-cluster-bot** am 2026-09-04. > > Verweise der Form `#123` im folgenden Text beziehen sich auf die **GitHub-Nummerierung**, nicht auf die Nummern in diesem Repo. Geschlossene Issues wurden nicht mitgezogen und sind weiterhin nur auf GitHub lesbar. --- Neuer Chart für [Canarytokens](https://github.com/thinkst/canarytokens-docker) (Thinkst), abgesichert durch einen **mitgelieferten oauth2-proxy** — analog zu `ytdlp-web-player`, aber mit einer entscheidenden Einschränkung, die dort nicht galt. ## Die Falle, die alles andere bestimmt Bei `ytdlp-web-player` sitzt der oauth2-proxy im Datenpfad vor der ganzen Anwendung. **Hier darf er das nicht.** Ein Canarytoken feuert, weil ein *Fremder* eine URL abruft. Steht der Proxy davor, bekommt der Angreifer eine Anmeldemaske statt eines ausgelösten Alarms — der Chart wäre lauffähig und trotzdem vollständig nutzlos. Die Trennlinie hat der Upstream bereits gezogen, in `nginx/nginx.conf`: eine feste Pfadliste geht ans Frontend, **alles andere** ans Switchboard. Daran hängt die Auth: - **Nur die Frontend-Pfade** laufen über den Proxy: `/generate`, `/manage`, `/download`, `/history`, `/settings`, `/resources`, `/legal`, dazu die Startseite und der API-Pfad. - **Alles andere geht direkt und unauthentifiziert ans Switchboard.** Ein Token, das erst nach Anmeldung feuert, ist kein Token. **Umgesetzt wird das im Ingress, nicht im Proxy.** Der naheliegende Gegenvorschlag — alles durch den Proxy und die Auslöser über `skip_auth_routes` freistellen — scheitert daran, dass Token-URLs beliebige Pfade sind. Entscheidend ist aber das Ausfallverhalten: teilt der Ingress auf, sieht der Proxy nie Auslöser-Verkehr. Fällt er aus, ist die Verwaltung gesperrt, **die Token feuern weiter**. Andersherum wäre ein kaputter Proxy ein stiller Totalausfall der eigentlichen Funktion. ## Aufbau Ein Image (`thinkst/canarytokens`) in zwei Rollen: **Frontend** (Deployment, Port 8082) und **Switchboard** (eigenes Kommando, die Kanal-Ports). Dazu **Redis** als StatefulSet mit PVC — dort liegen die Token selbst. `REDIS_HOST`/`REDIS_PORT`/`REDIS_DB` sind Werte, eine externe Instanz bleibt also möglich. **Der Upstream-nginx entfällt.** Seine einzige Aufgabe ist das Routing oben plus `client_max_body_size 50M`; das sagt ein Ingress direkt (`proxy-body-size`). Er wird im Compose ohnehin lokal gebaut — ihn zu übernehmen hieße, ein eigenes Image zu bauen und zu hosten. ## Ports — gemessen, nicht vermutet | Kanal | Port | beweglich? | |---|---|---| | DNS | 53 | **nein** — fremde Resolver fragen nicht nach dem Port | | SMTP | 25 | **nein** — fremde Mailserver liefern an 25 | | WireGuard | 51820 | **nein**, siehe unten | | MySQL | 3306 | **ja** — `frontend/app.py` schreibt `CHANNEL_MYSQL_PORT` in das Artefakt | | Kubeconfig | 6443 | **ja** — `channel_input_mtls.py` gibt denselben Wert an Listener und ausgegebene URL | | HTTP | 8083 | irrelevant, läuft über den Ingress | Dass Kubeconfig beweglich ist, räumt nebenbei eine echte Kollision ab: **6443 ist auf einem k3s-Knoten der Port des kube-apiservers.** **WireGuard ist ein Upstream-Fehler, kein Designentscheid.** `canarytokens/wireguard.py` schreibt in die ausgegebene Konfiguration `Endpoint = {public_ip}:{DEFAULT_PORT}` — eine **Modulkonstante**, nicht die Einstellung. `CANARY_CHANNEL_WIREGUARD_PORT` verschiebt also nur den Listener; das Artefakt zeigt weiter auf 51820. Wer die Einstellung ändert, verteilt still Konfigurationen, die ins Leere zeigen. Das gehört in `values.yaml` und NOTES. **Konsequenz für die Freigabe nach außen:** Service-Typ und alle Ports werden Werte. Der Chart legt sich weder auf NodePort noch LoadBalancer noch hostPort fest. Für NodePort ist zu wissen: 25 und 53 liegen unter 30000, 51820 über 32767 — ein Bereich, der alle drei umfasst, wäre die Abschaffung der Grenze, nicht ihre Erweiterung. Verzichtet man auf WireGuard-Token, bleiben zwei Ports und eine eng begründbare Erweiterung. ## Image-Pinning per Digest `thinkst/canarytokens` hat **1885 Tags und keinen einzigen Versionstag** — nur `latest`, `master`, `dev` und `sha-<commit>`. Ein `sha-`-Tag ist unveränderlich, aber für Renovate **nicht ordenbar**; genau daran ist dieses Repo in der searxng-Runde schon einmal gescheitert. Deshalb: **per Digest pinnen** (`thinkst/canarytokens@sha256:…`). Unveränderlich, und Renovate kann Digests eigenständig anheben. Die `appVersion` trägt dann keine sprechende Nummer — der Grund gehört als Kommentar in `Chart.yaml`. ## Zu belegen bei der Umsetzung 1. **Der Auth-Schnitt.** Gerendert zeigen, dass ein Frontend-Pfad über den Proxy geht und ein beliebiger anderer Pfad **nicht**. Das ist der Kern des Charts. 2. **Die Probes.** Es gibt **keinen Health-Endpunkt** — kein `/healthz`, kein `/ping`. Und das Switchboard auf seinem HTTP-Kanal anzusprechen kann **Alarme erzeugen**, das ist der Auslöser-Pfad. Ein `tcpSocket` beweist den Listener, ohne eine Anfrage zu senden, die als Token-Treffer durchgeht — dasselbe Muster wie bei `teamspeak-server`, dort aus demselben Grund. 3. **Das Digest-Pinning mit Renovate**, im Trockenlauf. Eine gültige Regel ist keine treffende Regel. ## Abhängigkeit: die bekannten Kosten Nach CLAUDE.md ist die Abhängigkeit **optional und standardmäßig aus**, über `condition:`. Ich halte mich daran, halte es hier aber für unbequem: eine ungeschützte Instanz lässt jeden, der den Host erreicht, Token in den eigenen Domains erzeugen. Das gehört entsprechend deutlich in `values.yaml` und NOTES. Dazu die Kosten aus der ytdlp-Runde: ein Unterchart ist für Fleet unsichtbar — eine neue Proxy-Version erreicht den Cluster erst, wenn der äußere Chart neu veröffentlicht wird. Der gepinnten Version gehört derselbe „KEEP THIS VERSION MOVING"-Kommentar. Und: `config.upstreams` wird mit `toJson` gerendert, **ohne `tpl`** — ein `{{ .Release.Name }}` landet dort wörtlich; die vom Release abgeleitete Frontend-Adresse muss über `config.configFile`. ## Voraussetzungen außerhalb des Charts Eine öffentliche IP, auf die `CANARY_PUBLIC_IP` zeigt, und Domains, deren **NS-Einträge** dorthin delegieren. Ohne beides ist der Chart wertlos — das gehört in die NOTES, nicht in die Annahme.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jlab-cluster/helm-charts#17