Compare commits

..
44 Commits
Author SHA1 Message Date
TURN-Rotation (automatische Kanonisierung) c53b0fe830 chore(coturn): TURN-Rotation aus turn-secret-rotation-20260901-020001 uebernommen
Automatisch kanonisiert: der Rotations-CronJob im Cluster erreicht git.lab nicht und pusht nach Gitea; dieser Commit bringt die Rotation auf den kanonischen Weg zurueck, bevor der Mirror sie ueberschreiben kann.
2026-09-01 03:23:28 +00:00
turn-secret-rotation fc577ba89b chore(coturn): automated TURN shared-secret rotation
Scheduled rotation (Issue #38). New secret generated, re-encrypted with
the scoped rotation age key, checksum/rotated-at annotations bumped so
Flux restarts coturn + synapse-main on merge. Please review and merge.
2026-09-01 02:00:01 +00:00
Thore Cimbal 6cd6ee618e monitoring: the two charts on a floating range were the two left behind
Every other release in this repo pins an exact version. Exactly two used a
range, and exactly those two stopped receiving updates: 0.12.6 is the last 0.x
alloy chart and 5.37.0 the last 5.x kube-state-metrics, so both ranges had been
sitting on a ceiling that reads as 'stays current'. A pinned version at least
looks stale.

Rendered both versions of each chart against our real values before changing
anything, because a chart jump moved an internal endpoint out from under a
network rule earlier today. Same objects, same selectors, same service names;
kube-state-metrics names its targetPort instead of numbering it and swaps
endpoints for endpointslices, neither of which our policies or scrapes touch.

nginx for the docs server goes along: 1.26-alpine carried two criticals,
1.31.4-alpine measures clean.
2026-08-21 12:00:00 +00:00
Thore Cimbal 533fcdeac1 netpol: MAS reaches Synapse through haproxy now, and the rule did not know it
Until ESS 26.4.0 the auth service pointed straight at matrix-stack-synapse-main,
which allow-ingress-synapse permits, so nothing was needed here. ESS 26.8.0 moves
matrix.endpoint to the haproxy service — and MAS walked into the default-deny.
Login broke with 500 'failed to provision device' on /oauth2/token.

The rule was mine, from #0088 this morning. It named who may reach haproxy, and
the list was complete for the topology of that hour. A chart decided otherwise
six hours later.
2026-08-21 12:00:00 +00:00
Thore Cimbal 07ffd0e86e ESS: 26.4.0 -> 26.8.0 (#0051)
The CVE gain is the smallest of today's steps: 17 critical findings become 6 and
217 high become 120 across Synapse, MAS, element-admin and lk-jwt-service.

The real reason is in the Synapse changelog rather than the CVE column. v1.157.0
fixes a bug introduced in v1.150.0 where reactivating a deactivated and erased
user did not restore their profile, breaking login, name changes and invitations.
We run v1.151.0, inside that range, and that is precisely what happened to an
account this morning.

Everything breaking in the span was checked against our state and none of it
applies: no Synapse workers, the stable auth integration already in use rather
than MSC3861, and no livekitAuth values set. Our elementWeb override to the fork
survives — both chart versions were rendered with our real values and compared.

Not 26.8.1: it appeared today. A fresh backup of the Synapse and MAS databases
plus media was taken by hand beforehand, and sorb holds a host snapshot.
2026-08-21 12:00:00 +00:00
Thore Cimbal fcf3169aa3 flux: upgrade the four controllers to the v2.9.4 set (#0051)
Fourteen critical findings and 231 high ones sit in the controllers that deploy
everything else. Measured on the target images before touching anything: all four
go to zero critical, and high drops from 231 to 48.

The manifest was generated with the flux CLI for exactly the components we run —
source, kustomize, helm and notification. The release install.yaml carries seven
deployments; dropping it in unchanged would have added image-automation,
image-reflector and source-watcher, which we do not operate. Structurally the
generated file matches what it replaces: four deployments, four service accounts,
eleven CRDs.

Flux applies this itself through the flux-system Kustomization, so it upgrades
its own controllers. The way back is a kubectl apply of the previous file
straight from git, which is why the commit before this one is named in the
message: 8114b9ec.
2026-08-21 12:00:00 +00:00
Thore Cimbal 8114b9ec1e cert-manager: step 7 of 7 — v1.20.3 to v1.21.1, the target (#0051)
Seven minors in sequence, each with the latest patch, exactly as the
documentation requires. Every step was verified before the next one started:
three deployments on the new version and fifteen certificates Ready.

The measured point of the exercise: v1.14.0 carried 8 critical and 128 high
findings across controller, webhook and cainjector; v1.21.1 carries 0 and 24.
The next scan round will show it in the dashboard rather than in this message.
2026-08-21 12:00:00 +00:00
Thore Cimbal 44a884062d cert-manager: step 6 of 7 — v1.19.6 to v1.20.3 (#0051)
This is the step with the RBAC narrowing: the cert-manager-edit aggregate
ClusterRole no longer grants create on challenges or create, patch and update on
orders. Those resources belong to cert-manager's own ACME workflow. Nothing here
is bound to that ClusterRole — zero bindings across all namespaces — so no
tooling loses a permission it was using.

Previous step verified: three deployments on v1.19.6, fifteen certificates
Ready.
2026-08-21 12:00:00 +00:00
Thore Cimbal e394cf1a58 cert-manager: step 5 of 7 — v1.18.6 to v1.19.6 (#0051)
This is the step with the ACME metric label change: the high-cardinality path
label on certmanager_acme_client_request_count and _duration_seconds is replaced
by a bounded action label. Nothing here uses those metrics — neither the
operating stack's rules nor any dashboard references them — so no dashboard or
alert has to follow.

Previous step verified: three deployments on v1.18.6, fifteen certificates
Ready.
2026-08-21 12:00:00 +00:00
Thore Cimbal fff5f54213 cert-manager: step 4 of 7 — v1.17.4 to v1.18.6 (#0051)
One minor at a time with the latest patch, as the documentation requires. The
previous step is verified: all three deployments on v1.17.4 and fifteen
certificates Ready.
2026-08-21 12:00:00 +00:00
Thore Cimbal 45ce4417e9 cert-manager: step 3 of 7 — v1.16.5 to v1.17.4 (#0051)
One minor at a time with the latest patch, as the documentation requires. The
previous step is verified: all three deployments on v1.16.5 and fifteen
certificates Ready.
2026-08-21 12:00:00 +00:00
Thore Cimbal 5507f13cae chore: nudge the push mirror after the firewall work on the operating host
The mirror to rohana failed from 15:51 onward because the host's public 443 was
unreachable while ufw and iptables were being changed. The network is back, but
GitLab had already backed off and neither re-enabling nor toggling the mirror
restarted it — a push mirror needs a push event.

This commit is empty. Its tree is identical to the previous one, so Flux still
applies cert-manager v1.16.5 and no minor version is skipped.
2026-08-21 12:00:00 +00:00
Thore Cimbal d90861e112 cert-manager: step 2 of 7 — v1.15.5 to v1.16.5 (#0051)
One minor at a time with the latest patch, as the documentation requires. The
previous step is verified: all three deployments on v1.15.5 and fifteen
certificates Ready.
2026-08-21 12:00:00 +00:00
Thore Cimbal 93155c2f37 cert-manager: step 1 of 7 — v1.14.0 to v1.15.5 (#0051)
Seven minors behind, and the documentation allows only one minor at a time with
the latest patch of each; skipping is offered solely as uninstall and reinstall.

Measured on the images rather than assumed: v1.14.0 carries 8 critical and 128
high findings across controller, webhook and cainjector; v1.21.1 carries 0 and
24. Both potentially breaking changes on the way were checked against our state
and do not apply — no dashboard or alert uses the ACME metrics whose label
changes, and nothing is bound to the cert-manager-edit ClusterRole whose
permissions narrow.

Fifteen certificates are Ready before this starts; that is the check after every
step.
2026-08-21 12:00:00 +00:00
Thore Cimbal c9a3ae60f5 authentik: arm the automatic rollback again, the jump is done (#0051)
Both steps are verified: migrations applied without inconsistency, blueprints
unchanged, and a real login recorded server-side on each version. Outside a
migration window an automatic rollback is the right behaviour, so remediation
goes back to three retries.
2026-08-21 12:00:00 +00:00
Thore Cimbal 92add9b9a2 authentik: 2026.5.6 -> 2026.8.0, the second and last step (#0051)
The intermediate step is verified: migrations applied without inconsistency,
both pods ready, blueprints unchanged at seventeen flows and two providers, and
a real login through Element, MAS and Authentik recorded server-side at 14:51.

This step adds the remaining high findings. Measured on the images: 2026.5.6
carries 5 critical and 92 high, 2026.8.0 carries 5 and 21. What stays is
perl-base and libxml2, neither of which has a fix available.

A fresh backup was taken first. The one from before the intermediate step sits
on the old schema and would be the wrong way back now.
2026-08-21 12:00:00 +00:00
Thore Cimbal a252ac0f09 authentik: 2026.2.3 -> 2026.5.6, the mandatory intermediate step (#0051)
Authentik's documentation forbids skipping major releases and requires the
latest minor of each before moving on, so 2026.5.6 comes before 2026.8.0 rather
than being an optional stop.

It is also where the benefit is. Measured on the images rather than assumed:
2026.2.3 carries 27 critical and 477 high findings, 2026.5.6 carries 5 and 92.
This one step removes every critical finding that can be removed here; what
remains is perl-base and libxml2, neither of which has a fix. The second step
adds high findings only.

Both breaking changes of this release were checked against our state and do not
apply: the deprecated Postgres connection options are not set anywhere, and
there are no outposts whose version would have to match.

A manual backup was taken immediately before this, because the nightly one is
hours old and the way back is a database restore rather than a version rollback.
2026-08-21 12:00:00 +00:00
Thore Cimbal df6d291d81 authentik: disarm the automatic rollback before the version jump (#0051)
The release carried upgrade remediation with three retries and no strategy. The
Flux CRD is explicit: the strategy defaults to rollback, remediation runs between
each attempt, and the last failure is remediated as well whenever retries exceed
zero. A failing upgrade would therefore have rolled Helm back to the old version
up to four times, against a database Django had already migrated forward — the
migration inconsistency Authentik's own documentation warns about, triggered by
this line.

Here the way back is a database restore, not a version rollback, so an automatic
rollback cannot help and can only deepen the damage. It goes back to three once
the jump is done; outside a migration window the remediation is right.

Nothing else changes: the chart version, the values and the install remediation
are untouched.
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 90a89f338a netpol: wave 4 — the authentication service loses its outbound too (#0088)
Its configuration was read from the running process yesterday: the
homeserver by internal service name, Authentik by its public name. Public
names resolve to the node address, and traffic there is not subject to the
policy at all - so the upstream path survives the restriction while
everything else goes.

Held back until now on purpose, because a mistake here hits sign-in and the
analysis alone is not an acceptance. The owner is testing a login against
this change; if it fails, the revert is one commit and about a minute,
rehearsed in wave 1.

Congruence rechecked: twenty-one excluded, the same twenty-one narrowly
ruled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 c7fe0488e2 netpol: say in the manifest why the last five keep their outbound (#0088)
An open egress that nobody explains reads as neglect. These five are
decisions, so the reasons sit at the rule, where the next reader looks -
not in an issue and not in an ADR alone.

Synapse federates to arbitrary servers and previews addresses users pick;
there is no list to write, and the right control sits a level higher in
url_preview_ip_range_blacklist. Coturn and the SFU relay media to arbitrary
clients - that is the service. ClamAV pulls signatures from a CDN, and
pinning would break the update silently, which is the failure class this
project keeps finding.

MAS is now only there out of caution, and the comment says so. Read from
the running process today: its upstream is Authentik at its public name and
the homeserver at the internal service name. The public name resolves to
the node address, which the policy does not cover anyway - so MAS is
restrictable and only the sign-off is missing, because a mistake there hits
sign-in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 d5b59eb5e9 netpol: wave 3 — the two namespaces the issue never mentioned (#0088)
authentik and monitoring carried only the metadata block, so every pod
there could reach anything. Both now follow the pattern from matrix: the
broad policy keeps an exclusion list and stays as the catch-all, and each
workload gets a rule of its own.

The destinations were read, not guessed. Alloy ships to 10.0.0.3 on 3100
and 9090 - taken from its running configuration. Authentik sends mail
through smtp.ionos.de:587, and that is load-bearing rather than optional:
the blueprints use password recovery and invitations by mail. The database
and kube-state-metrics speak to nobody outside.

Mail gets a /27 rather than two /32. The name resolves to .97 and .113
today, both in the provider's own block; a third address would break mail
with nobody watching a rule, and thirty-two addresses of one provider's
mail infrastructure is the smaller price. That is the opposite call to
ClamAV on purpose - this is not a CDN in front of half the internet.

Deliberately not allowed: authentik's version check. It sits behind
Cloudflare with rotating addresses and nothing depends on it, so the call
fails and gets logged. The manifest says so, because whoever finds that
error later should know it is intended.

Congruence checked in both namespaces before pushing: excluded and
narrowly ruled are the same sets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 0a7b8f4bf1 netpol: wave 2 — four workloads keep exactly one destination (#0088)
Both backups reach the Hetzner storage box over SSH; the TURN rotation and
Wiki.js reach rohana. Everything else is now refused for them.

Two separate policies rather than entries in egress-nur-intern, because a
policy's egress rules apply to every pod it selects: hanging the storage
box off the shared policy would hand it to fourteen workloads that have no
business there.

rohana is allowed as 10.0.0.3/32, not as its public address. The DNS
pointer from the previous commit sends the public name down the private
path, and the certificate is valid there - both measured after the change,
not assumed. So the group's one deliberate Gitea exception no longer leaves
the cluster at all.

Congruence checked before pushing, and this is the check worth keeping:
twenty names are excluded from the broad rule and exactly the same twenty
are covered by a narrow one. An entry on one side only would either cut a
workload off completely or leave the restriction inert, and neither is a
syntax error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 69c9ef13a1 dns: point rohana at the private path from inside the cluster (#0088)
rohana.axion1337.de resolves publicly to a Hetzner address, while the host
is reachable over the private network at 10.0.0.3 - and serves a valid
Let's Encrypt certificate for that very name there, measured with full
verification. Without an internal pointer every access from the cluster
leaves it for no reason and needs an outbound exception.

A dedicated zone rather than a second hosts block: the Corefile already
runs hosts /etc/coredns/NodeHosts in the main block, so a second one there
would collide. The .server import at the end of the Corefile takes a zone
of its own, and the reload plugin picks the change up without a restart.

The price is in the file, not in a commit message nobody rereads: two paths
become one. If 10.0.0.3 is down, rohana is unreachable from the cluster
although the public route would work, and the failure looks like "Gitea is
gone" rather than "the private path is gone". The comment says where to
look first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore Cimbal 3fbc268e84 Reapply "netpol: wave 1 — six deploy and helper workloads lose their outbound (#0088)"
This reverts commit 80cf715e3c.
2026-08-21 12:00:00 +00:00
Thore Cimbal 80cf715e3c Revert "netpol: wave 1 — six deploy and helper workloads lose their outbound (#0088)"
This reverts commit d9ef54dc99.
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 d9ef54dc99 netpol: wave 1 — six deploy and helper workloads lose their outbound (#0088)
matrix-rtc-authorisation-service, wikijs-config, init-secrets,
synapse-check-config and both deployment markers have no demonstrable need
to leave the cluster. They now get DNS plus the pod and service networks
and nothing else.

The name goes into BOTH lists, and that is the whole point: NetworkPolicies
are additive, so as long as the broad policy still selects a pod and allows
0.0.0.0/0, a second and stricter rule for the same pod changes nothing. The
file already carried that warning as a comment; this change obeys it rather
than rediscovering it.

Checked before pushing: both selector lists are congruent - no workload is
excluded from the broad rule without receiving the narrow one, and none the
other way round. A one-sided entry would either open a pod completely or
cut it off entirely, and neither shows up as a syntax error.

Class A is deliberately the first wave: these workloads run at deploy time,
so a mistake surfaces at the next rollout rather than in a user's face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore CimbalandClaude Opus 5 59b7d288dc drill: monthly media restore probe, proven against production (#0030)
Counterpart to restore-drill.yaml: that one covers the databases, this the
Synapse media store. Separate job on purpose - the database probe needs a
throwaway Postgres, this one the production PVC read-only, and folding two
different permission and failure pictures into one job makes an emergency
harder to diagnose, not easier.

The name is deliberate. BackupJobFailed already matches restore-drill.*,
so a failure is covered without a new rule and reaches the maintenance
room through the single alertmanager route.

It re-proves its own comparison every run. After the check passes, one
shared file is altered by a byte and the comparison must report it -
otherwise the job fails with "this probe proves nothing". A comparison
that has only ever said "equal" is a guess, and that stays true when it
runs monthly rather than once.

The intersection carries the proof, not the totals: the backup is a
snapshot while production keeps running, and Synapse prunes its own
preview caches. One-sided files are therefore tolerated in url_cache and
url_cache_thumbnails and are an error anywhere else.

The production PVC is mounted read-only and only emptyDir is written. On
today's single node a second read-only mount of the ReadWriteOnce volume
is unproblematic; the comment says what would have to change if a second
node ever appeared, because that failure would alert without anything
being wrong with the data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
2026-08-21 12:00:00 +00:00
Thore Cimbal 73f56c3414 synapse: Metadaten-Netz in die Sperrliste der URL-Vorschau (#0088)
169.254.0.0/16 fehlte - das Netz des Cloud-Metadaten-Dienstes. Die Vorschau holt
Adressen, die Nutzer bestimmen; die Sperre gehoert deshalb auch an diese Stelle
und nicht nur in die NetworkPolicy eine Ebene tiefer. Dazu 100.64.0.0/10.

Aufgefallen bei sorbs Frage, warum ich vorgeschlagen hatte, die URL-Vorschau
abzuschalten. Der Vorschlag war falsch: url_preview_ip_range_blacklist ist die
richtige Kontrolle an der richtigen Stelle und war gesetzt - ich haette pruefen
muessen statt eine Funktion in Frage zu stellen. Beim Pruefen kam diese Luecke
heraus, die mehr wert ist als der Vorschlag.
2026-08-20 12:00:00 +00:00
Thore Cimbal a6eaea6074 netpol: zehn Arbeitslasten ohne Ausgang nach draussen (#0088)
Sie sprechen ausschliesslich cluster-intern: die drei nginx-Dienste, beide
Postgres, der ClamAV-Vorschalter, haproxy, die Wiki-Gruppenpruefung und die
beiden Matrix-Bots. Fuer sie faellt der pauschale 0.0.0.0/0-Satz weg; DNS und
Cluster-Netz bleiben.

⚠️ Der Ausschluss steht in egress-block-metadata selbst, nicht in einer neuen
strengen Policy. NetworkPolicies sind additiv - solange irgendeine Policy
0.0.0.0/0 fuer einen Pod erlaubt, aendert eine zweite, strengere nichts. Genau
dieser Fehler waere die naheliegende Umsetzung gewesen.

Zwei Dinge vorher gemessen statt angenommen:

-  waehlt auch Pods, die das Label gar nicht tragen (29 Pods, 1 Treffer,
  notin liefert 28). Ohne diese Gewissheit haetten drei Pods ohne
  app.kubernetes.io/name ihren Metadaten-Schutz verloren.
- Die Selektoren gegen den laufenden Cluster geprueft: 20 behalten breiten
  Ausgang, 9 laufende Pods werden eng - Summe 29, also genau die Gesamtzahl.

Zwei Policies, weil ein podSelector nicht ueber zwei Label-Schluessel odern kann;
drei Pods im Namespace fuehren nur .

Rollback: beide neuen Policies loeschen, Ausschluss zuruecknehmen.
2026-08-20 12:00:00 +00:00
Thore Cimbal a3d7e03d90 wiki: Gruppenpruefung wartet auf die NetworkPolicy (#0103)
Der erste echte Lauf scheiterte mit "Connection refused", obwohl Service,
Endpunkt, Portname, Pod-Marken und Policy-Regel alle korrekt waren.

Ursache gemessen, nicht geraten: Die NetworkPolicy-Regeln fuer einen NEU
erzeugten Pod sind beim Start des Containers noch nicht programmiert. Im selben
Pod nacheinander: psql sofort abgewiesen, roher TCP-Test zu, nach 20 Sekunden
psql erfolgreich.

Ein frueherer nc-Test aus einem separaten Pod lief zufaellig spaet genug und sah
sauber aus - er hat mich zunaechst in die falsche Richtung geschickt.

Der Job wartet jetzt bis zu 60 s auf Erreichbarkeit und meldet erst danach einen
Fehler. Ohne das scheiterte er jede Nacht an einem Wettlauf, und man gewoehnt
sich an den roten Job - genau die Abstumpfung, die #0104 abgestellt hat.

⚠️ Dieselbe Klasse kann andere kurzlebige Jobs im Namespace treffen
(wikijs-backup, synapse-backup, restore-drill). Nicht geprueft, eigener Befund.
2026-08-20 12:00:00 +00:00
Thore Cimbal 0f24fe8a54 wiki: taegliche Pruefung auf Nutzer ohne Gruppe (#0103)
Letztes offenes Abnahmekriterium des Issues: Der Fall 'angemeldet, aber ohne
Gruppe' muss erkennbar sein. Bei @apo fiel er erst auf, weil er sich beschwerte -
Wiki.js meldet diesen Zustand nirgends.

Taegliche Stichprobe auf der Datenbank statt Instrumentierung des Logins: Ein
Patch am Anmeldeweg waere dafuer unverhaeltnismaessig, und ein Tag Verzug ist
hinnehmbar - vorher lag der Fall tagelang unbemerkt. Ausgabe geht ueber Alloy
nach Loki, Suchbegriff 'Wiki-Nutzer ohne Gruppe'.

Der Pod bekommt eine eigene NetworkPolicy-Regel. Ohne sie kaeme er nicht an die
Datenbank, und zwar lautlos - die Datenbank laesst nur wikijs und wikijs-backup
zu (AGENTS.md: jeder neue Pod braucht seine eigene Regel).

Das Skript prueft zuerst, ob die Datenbank ueberhaupt antwortet. Ohne diese
Gegenprobe saehe 'keine Nutzer ohne Gruppe' bei unerreichbarer DB genauso aus wie
ein sauberes Ergebnis - stiller Erfolg ist hier das groessere Risiko als ein Fund.

Beide Zweige gegen die laufende Datenbank getestet, lesend: der gute Fall meldet
'OK (4 geprueft)', der Warn-Zweig nennt die Betroffenen und den Behebungsweg.
2026-08-20 12:00:00 +00:00
Thore Cimbal 803c444885 MatrixRTC: Client-IP am SFU erhalten (externalTrafficPolicy Local)
Mit dem Chart-Default Cluster verteilt kube-proxy NodePort-Pakete selbst weiter
und ersetzt dabei die Absenderadresse. Der SFU sieht den Client deshalb nie unter
seiner echten Adresse - am 2026-08-19 war das gewaehlte ICE-Paar prflx 10.42.0.1,
eine peer-reflexive Adresse aus dem Cluster-Netz. Das macht die Kandidatenwahl
unnoetig instabil; Local ist die dokumentierte LiveKit-Empfehlung fuer Kubernetes.

Ohne Nachteil, weil der Cluster aus einem Knoten besteht - der uebliche Preis
(nur Knoten mit laufendem Pod nehmen Verkehr an) kann nicht greifen. Bei einem
zweiten Knoten neu zu bewerten.

Lokal gegen den Chart gerendert, nicht angenommen: beide NodePort-Dienste
bekommen Local, der ClusterIP-Dienst bleibt unberuehrt.

Behebt NICHT das schubweise ICE-Flappen (16.08. = 119 Wechsel, 18.08. = 0) - das
ist aelter und liegt an der Zahl der Netzwerkpfade auf Client-Seite.
2026-08-20 12:00:00 +00:00
Thore Cimbal d87c432f89 elementWeb auf v0.6.0
Release des Upstream-Anschlusses (ADR-0022). Gleicher Quellstand wie rc.3
(8ca03fe), das die Abnahme bestanden hat - nur unter Release-Nummer. Die Images
sind nicht bitgleich, weil der Build die Versionszeichenkette aus git describe
ins Artefakt backt.

Produktion laeuft damit erstmals auf einem Fork, der wieder an der
Upstream-Historie haengt.

Rueckhebel: Tag zurueck auf v0.5.4.
2026-08-19 12:00:00 +00:00
Thore Cimbal de347343b3 elementWeb auf v0.6.0-rc.3
Zweiter Anlauf des Upstream-Anschlusses (ADR-0022). Entfernt den Merge-Rest,
der rc.2 die Raumliste brach, und bringt einen typecheck-Job mit, den
docker_web als needs fuehrt - dieses Image ist das erste, das ohne bestandene
Typpruefung gar nicht haette entstehen koennen.

Abnahme steht aus, in dieser Reihenfolge: Raumliste laedt, ClamAV per Zip,
ClamAV per .png (der umgezogene Bild-Pfad), Call-Teilnehmerliste.

Rueckhebel: Tag zurueck auf v0.5.4.
2026-08-19 12:00:00 +00:00
Thore Cimbal b319f21133 Historie: rc.2-Vorfall am Tag-Kommentar festhalten
Der Block beschrieb rc.2 noch als laufenden Stand.
2026-08-19 12:00:00 +00:00
Thore Cimbal 1c3df5badb elementWeb zurueck auf v0.5.4 - rc.2 bricht die Raumliste
react-soft-crash bei sorb (Rageshake 2026-08-19 15:17, Safari):
"Setting 'feature_room_list_sections' does not appear to be a setting."
aus SettingsStore.getValue in RoomListItemViewModel.generateItemSync - also
bei jedem Raumlisteneintrag.

Fehler in der Merge-Aufloesung von ADR-0022: Upstream hat den Labs-Schalter
feature_room_list_sections entfernt (Sektionen laufen jetzt ueber
RoomList.showSections). Settings.tsx hat Upstreams Fassung uebernommen, in
RoomListItemViewModel.ts blieb die alte getValue-Zeile daneben stehen.

Der Build konnte das nicht fangen: getValue nimmt einen String, der Fehler
entsteht erst zur Laufzeit.

Kandidat kommt nach dem Fix als rc.3 zurueck.
2026-08-19 12:00:00 +00:00
Thore Cimbal 0f45266426 elementWeb auf v0.6.0-rc.2 (Upstream-Anschluss v1.12.26)
Kandidat, kein Release. Bringt den Merge aus ADR-0022 in Produktion, damit die
Abnahme an einem echten Client stattfinden kann.

Zu pruefen sind die zwei Patches, die der Merge verschieben musste:
ClamAV-Fehlermeldung im Bild-Pfad und die Call-Teilnehmerliste in der Raumliste.
Der Datei-Pfad (Zip) ist unberuehrt und diente heute als Ausgangswert - der
Scanner meldete die EICAR-Datei erwartungsgemaess zweimal, beim Senden und beim
Empfangen.

Rueckhebel: Tag zurueck auf v0.5.4.
2026-08-19 12:00:00 +00:00
Thore Cimbal d809b3f695 config(element): remove the interface path to a foreign homeserver
Decision sorb. Hides the edit button beside the server name, so the homeserver can no
longer be switched through the UI, and the 401/403 login error now names the server
rather than staying generic.

Honest about its reach, in the comment as well as here: it is a surface restriction.
MatrixChat still takes hs_url from the query string in two registration flows without
consulting this setting, so a crafted link is unaffected. Against
GHSA-wrcp-5v3v-3j6v - open since 2026-07-20, affecting everything below 1.12.22 while
we run 1.12.17 - it narrows the way in without closing it. The update in management
#0099 remains the actual fix.

The matching line went into the desktop client separately, since that one carries its
own config.json.
2026-08-19 12:00:00 +00:00
Thore Cimbal 1e65f5b3dd ci: stop creating pipelines that have no jobs to run
Pipeline 518 today was red with zero jobs - a push where no rule matched, which
GitLab still turns into a pipeline and then marks failed. The standstill check
reported it correctly, and it was the only finding standing between the scheduled
checks and green.

Acknowledging it would have been the wrong move twice over: the entry would name one
pipeline id, so the next empty pipeline files a fresh finding, and the underlying
behaviour - red without a fault - is exactly what management #0104 is about. Rules
that prevent the empty pipeline remove the class instead.

schedule comes first deliberately. The daily canonization changes no paths, so a
changes-based rule alone would drop it.
2026-08-19 12:00:00 +00:00
Thore Cimbal 3935f359f9 feat(synapse): close federation with an empty domain whitelist (#0060)
Decision sorb. Measured basis rather than preference: four months of operation with
zero destinations, zero remote users and zero rooms with outside participation, while
the federation API answered publicly - the delegation routes it over 443, so 8448
being shut never mattered.

An empty list federates with nobody and one entry opens it for exactly that domain,
so the capability stays one line away rather than gone.

The comment records what must not be done instead, because it is not obvious and it
would look correct: blocking /_matrix/federation at the edge. lk-jwt-service verifies
OpenID tokens through /_matrix/federation/v1/openid/userinfo and reaches it over the
public name - no hostAliases, ClusterFirst DNS - so a path-level block kills group
calls. Synapse serves that endpoint without an X-Matrix signature, so the whitelist
does not touch it.

Caught while validating: the first version of this edit split the auto_join block,
moving auto_join_rooms_for_guests under federation. Functionally identical after the
fragments merge, wrong to read, and fixed before pushing - the diff is now 20 added
lines and nothing moved.
2026-08-19 12:00:00 +00:00
Thore CimbalandClaude Opus 5 e7469a36b8 feat(netpol): deny pods the metadata service, leave everything else open (#0088)
First egress rule in matrix, authentik and monitoring. It allows DNS, the cluster
ranges and the whole internet, and denies only 169.254.0.0/16 - link-local, where
Hetzner serves instance metadata unauthenticated to any pod.

Deliberately narrow. The textbook cut, 0.0.0.0/0 except RFC1918, would have severed
two things here, both over 10.0.0.3 on the private Hetzner network: Alloy writes
metrics and logs there, and the TURN rotation reaches Gitea through a hostAlias to
that address. Private ranges therefore stay open.

The payoff is modest and should be stated as such: measured from a pod, the service
answers with instance-id, hostname, region, MAC and network config, while userdata
and public-keys are empty. No credentials are exposed here, unlike the AWS case this
hardening usually targets. It costs nothing though, and it closes the class.

Two preconditions checked rather than assumed, because both are the usual way this
breaks: kube-system carries kubernetes.io/metadata.name so the DNS rule actually
matches, and the cluster is IPv4-only so 0.0.0.0/0 really does cover everything.

Rollback is deleting the one policy per namespace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:00:00 +00:00
Thore CimbalandClaude Opus 5 8473a1f0e5 ci: remove the CANONIZE_TOKEN probe, it did its job
Pipeline 521: the variable is visible inside the job, the push to a throwaway ref
succeeded and the ref was removed again. Combined with the token's own properties -
Maintainer, write_repository only - the monthly canonization has what it needs on
2026-09-01.

Two things the probe itself taught, kept here rather than in the removed block: an
API-triggered pipeline has source "api", not "web", so the first attempt silently
skipped the job entirely; and this repo has no "pruefen" stage, which rejected the
pipeline outright. Both would have cost the same time again next year.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:00:00 +00:00
Thore CimbalandClaude Opus 5 c3e42be0d1 ci: the probe never ran - an API trigger has source api, not web
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:00:00 +00:00
Thore CimbalandClaude Opus 5 e816d76f60 ci: drop the pruefen stage from the probe - this repo has no such stage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:00:00 +00:00
22 changed files with 1491 additions and 791 deletions
+22 -34
View File
@@ -3,6 +3,28 @@
# weiterhin selbststaendig aus dem Gitea-Mirror. Repo-Topologie: git.lab ist
# kanonisch, rohana/Gitea ist Push-Mirror und Flux-Quelle.
# Ohne workflow-Block legt GitLab auch dann eine Pipeline an, wenn KEIN Job auf sie
# passt - und fuehrt sie als "failed". Das ist rot ohne Fehler, und rot ohne Fehler
# gewoehnt einem das Hinsehen ab (management #0104). Real passiert am 2026-08-19,
# Pipeline 518. Diese Regeln verhindern die leere Pipeline, statt sie hinterher zu
# quittieren.
#
# Reihenfolge zaehlt: schedule zuerst, sonst faellt die taegliche Kanonisierung mit
# heraus - sie aendert keine Pfade und wuerde von der changes-Regel nie erfasst.
.pfade_mit_pipeline: &pfade_mit_pipeline
- apps/**/*
- clusters/**/*
- .gitlab-ci.yml
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "api"
- if: $CI_COMMIT_BRANCH == "main"
changes: *pfade_mit_pipeline
- when: never
verify:
image: alpine:3.20
rules:
@@ -200,37 +222,3 @@ canonize_wiki:
- git clone --bare --quiet https://rohana.axion1337.de/sorb/ThreadNetWiki.git /tmp/wiki.git
- GIT_SSL_CAINFO="$LABCA" git -C /tmp/wiki.git push "https://oauth2:${WIKI_CANONIZE_TOKEN}@${CI_SERVER_HOST}/axion1337.chat/threadnet-wiki.git" 'refs/heads/main:refs/heads/main'
- echo "git.lab threadnet-wiki spiegelt jetzt Gitea sorb/ThreadNetWiki (main)."
# ---------------------------------------------------------------------------
# TEMPORAER (2026-08-19): beweist, dass CANONIZE_TOKEN in der Pipeline ankommt
# und schreiben darf - OHNE main anzufassen. Laeuft nur, wenn eine Web-Pipeline
# ausdruecklich mit TEST_CANONIZE=1 gestartet wird. Wird nach dem Test entfernt.
# ---------------------------------------------------------------------------
canonize_token_test:
stage: pruefen
image: alpine:3.20
rules:
- if: $CI_PIPELINE_SOURCE == "web" && $TEST_CANONIZE == "1"
script:
- apk add --no-cache git ca-certificates >/dev/null
- export GIT_SSL_CAINFO="$CI_PROJECT_DIR/ci/lab-ca-chain.crt"
- |
if [ -z "$CANONIZE_TOKEN" ]; then
echo "FEHLT: CANONIZE_TOKEN ist im Job nicht sichtbar."
echo "Ursache meist: Variable 'protected', Pipeline laeuft aber auf einem"
echo "ungeschuetzten Branch. Genau dann bliebe die Rotation liegen."
exit 1
fi
echo "Sichtbar: CANONIZE_TOKEN ist im Job gesetzt."
URL="https://oauth2:${CANONIZE_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
REF="canonize-token-probe-${CI_PIPELINE_ID}"
# Ausgabe wird gefiltert: git schreibt die URL bei Fehlern mit ins Log.
if git push "$URL" "HEAD:refs/heads/$REF" 2>&1 | sed 's#oauth2:[^@]*@#oauth2:***@#g'; then
echo "SCHREIBEN OK: Zweig $REF angelegt."
else
echo "SCHREIBEN FEHLGESCHLAGEN - Rolle oder Scope reichen nicht."
exit 1
fi
git push "$URL" --delete "refs/heads/$REF" 2>&1 | sed 's#oauth2:[^@]*@#oauth2:***@#g' \
&& echo "Aufgeraeumt: $REF wieder entfernt." \
|| echo "WARNUNG: $REF blieb liegen, bitte von Hand loeschen."
+15 -1
View File
@@ -8,7 +8,16 @@ spec:
chart:
spec:
chart: authentik
version: "2026.2.3"
# Zweite und letzte Stufe des Sprungs (management #0051, Gate 2).
# Der Weg war 2026.2.3 -> 2026.5.6 -> 2026.8.0: Authentik verlangt die
# letzte Minor-Fassung jeder Reihe und verbietet das Ueberspringen von
# Fassungsreihen.
#
# An den Images gemessen, nicht geschaetzt (CRITICAL / HIGH):
# 2026.2.3 27 / 477 2026.5.6 5 / 92 2026.8.0 5 / 21
# Die Zwischenstufe trug den gesamten CRITICAL-Gewinn, diese Stufe die
# restlichen 71 HIGH. Was bleibt, sind perl-base und libxml2 ohne Fix.
version: "2026.8.0"
sourceRef:
kind: HelmRepository
name: goauthentik
@@ -18,6 +27,11 @@ spec:
retries: 3
upgrade:
remediation:
# Waehrend des Sprungs 2026.2.3 -> 2026.5.6 -> 2026.8.0 stand hier 0, damit
# Flux ein misslingendes Upgrade nicht selbsttaetig auf die alte Fassung
# zurueckrollt - der Rueckweg war dort eine Datenbank-Wiederherstellung,
# keine Fassungsruecknahme (management #0051). Sprung ist durch, also
# wieder scharf: im Normalbetrieb ist die Behebung richtig.
retries: 3
valuesFrom:
- kind: Secret
+174
View File
@@ -94,3 +94,177 @@ spec:
ports:
- protocol: TCP
port: 8089
---
# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088).
#
# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei
# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy
# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea
# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen.
#
# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der
# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19:
# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und
# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist
# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse.
#
# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer
# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher
# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der
# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall.
#
# Rollback: diese eine Policy im Namespace loeschen.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-block-metadata
namespace: authentik
spec:
# Waehlt alle Pods AUSSER denen mit eigener, engerer Regel (#0088).
# Bleibt als Auffangnetz: Eine kuenftige Arbeitslast ohne eigene Policy
# landet hier - Metadaten gesperrt, Rest offen - statt voellig ungeregelt.
# ⚠️ Der Ausschluss MUSS hier stehen: NetworkPolicies sind additiv.
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: NotIn
values:
- authentik
- postgresql
- authentik-backup
policyTypes:
- Egress
egress:
# 1. DNS - zuerst, weil ohne DNS nichts geht.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1).
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz -
# ausser Link-Local.
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.0.0/16
---
# Welle 3 (#0088): Authentik-Namespace, enge Regeln je Arbeitslast.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-nur-intern
namespace: authentik
spec:
# Die Datenbank spricht mit niemandem ausserhalb des Clusters.
podSelector:
matchLabels:
app.kubernetes.io/name: postgresql
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-storage-box
namespace: authentik
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: authentik-backup
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# Hetzner Storage Box, borg ueber SSH - wie die Backups in `matrix`.
- to:
- ipBlock:
cidr: 91.98.246.178/32
ports:
- protocol: TCP
port: 23
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-authentik-smtp
namespace: authentik
spec:
# Server und Worker tragen beide app.kubernetes.io/name=authentik.
podSelector:
matchLabels:
app.kubernetes.io/name: authentik
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# E-Mail-Versand ueber smtp.ionos.de:587. TRAGEND, nicht optional: die
# Blueprints nutzen Passwort-Wiederherstellung und Einladungen per Mail.
#
# /27 statt zwei /32: Der Name loest heute auf 213.165.67.97 und .113 auf,
# beide im selben Block des Anbieters. Eine dritte Adresse wuerde bei /32
# den Mailversand brechen, ohne dass jemand eine Regel im Blick haette -
# 32 Adressen der Mail-Infrastruktur eines Anbieters sind der kleinere
# Preis. (Anders als bei ClamAV: das ist kein CDN vor dem halben Internet.)
- to:
- ipBlock:
cidr: 213.165.67.96/27
ports:
- protocol: TCP
port: 587
#
# BEWUSST NICHT erlaubt: version.goauthentik.io (Versionspruefung). Liegt
# hinter Cloudflare mit wechselnden Adressen und ist nicht tragend - der
# Aufruf scheitert und Authentik protokolliert es. Wer den Fehler spaeter
# findet: das ist Absicht, kein Versehen.
+7 -1
View File
@@ -9,7 +9,13 @@ spec:
chart:
spec:
chart: cert-manager
version: "v1.14.0" # Oder aktuellste stabile Version
# Schrittweise auf v1.21.1 (management #0051). cert-manager verlangt
# "one minor version at a time, always the latest patch" - Ueberspringen
# ist nur ueber Deinstallation+Neuinstallation vorgesehen.
# Weg: 1.15.5 -> 1.16.5 -> 1.17.4 -> 1.18.6 -> 1.19.6 -> 1.20.3 -> 1.21.1
# Am Zielimage gemessen: v1.14.0 traegt 8 CRITICAL und 128 HIGH ueber die
# drei Images, v1.21.1 traegt 0 und 24.
version: "v1.21.1"
sourceRef:
kind: HelmRepository
name: jetstack
+36
View File
@@ -0,0 +1,36 @@
# Interner Zeiger auf rohana (#0088).
#
# rohana.axion1337.de loest oeffentlich auf 188.245.193.243 auf. Der Host ist
# aber ueber das private Netz unter 10.0.0.3 erreichbar UND traegt dort ein
# gueltiges Let's-Encrypt-Zertifikat fuer genau diesen Namen - am 2026-08-21
# mit voller Zertifikatspruefung nachgemessen. Ohne diesen Zeiger verlaesst
# jeder Zugriff aus dem Cluster (Gitea-Push der TURN-Rotation, git-storage von
# Wiki.js) unnoetig den Cluster und braucht eine Egress-Ausnahme nach draussen.
#
# Warum eine eigene Zone und kein zweiter `hosts`-Block: Die Corefile fuehrt
# bereits `hosts /etc/coredns/NodeHosts` im Hauptblock. Ein zweiter dort waere
# ein Konflikt, deshalb ein `.server`-Import mit eigener Zone (Corefile,
# letzte Zeile). `reload` im Hauptblock zieht Aenderungen selbsttaetig nach -
# ein Neustart von CoreDNS ist nicht noetig.
#
# ⚠️ PREIS: Aus zwei Wegen wird einer. Faellt 10.0.0.3 aus, ist rohana aus dem
# Cluster nicht mehr erreichbar, obwohl der oeffentliche Weg funktionieren
# wuerde - und der Fehler sieht aus wie "Gitea ist weg", nicht wie "der private
# Pfad ist weg". Wer hier sucht: erst `nslookup rohana.axion1337.de` aus einem
# Pod, dann diese ConfigMap.
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom
namespace: kube-system
data:
# Dateiname MUSS auf .server enden - nur die werden am Ende der Corefile
# importiert. `.override` liefe im Hauptblock und damit in den Konflikt oben.
rohana.server: |
rohana.axion1337.de:53 {
hosts {
10.0.0.3 rohana.axion1337.de
fallthrough
}
forward . /etc/resolv.conf
}
+1
View File
@@ -1,6 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- coredns-custom.yaml
- ns-cert-manager.yaml
- ns-matrix.yaml
- cert-manager-repo.yaml
+7 -1
View File
@@ -8,7 +8,13 @@ spec:
chart:
spec:
chart: alloy
version: "0.x"
# ⚠️ Bis 2026-08-21 stand hier "0.x". Das sah nach "bleibt aktuell" aus
# und war eine DECKE: 0.12.6 ist die letzte 0er-Fassung des Charts, seither
# erschienen 1.x-Fassungen, die der Bereich nie erreichen konnte. Alloy blieb
# dadurch auf v1.7.5 stehen (2 CRITICAL) waehrend v1.16.3 keine traegt.
# Jetzt fest wie jeder andere Release in diesem Repo - eine feste Nummer ist
# sichtbar veraltet, ein gedeckelter Bereich nicht.
version: "1.9.0"
sourceRef:
kind: HelmRepository
name: grafana
+6 -1
View File
@@ -8,7 +8,12 @@ spec:
chart:
spec:
chart: kube-state-metrics
version: "5.x"
# ⚠️ Bis 2026-08-21 "5.x" - dieselbe Decke wie bei alloy: 5.37.0 ist die
# letzte 5er-Fassung, aktuell ist 8.4.0. Stand deshalb auf App v2.15.0
# (1 CRITICAL); v2.20.0 traegt keine. Beide Renderings verglichen:
# gleiche Objekte, gleiche Selektoren, targetPort 8080 -> benannt "http"
# (derselbe Port), RBAC endpoints -> endpointslices.
version: "8.4.0"
sourceRef:
kind: HelmRepository
name: prometheus-community
+1
View File
@@ -6,3 +6,4 @@ resources:
- kube-state-metrics.yaml
- alloy-config.yaml
- alloy.yaml
- networkpolicy.yaml
+130
View File
@@ -0,0 +1,130 @@
---
# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088).
#
# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei
# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy
# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea
# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen.
#
# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der
# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19:
# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und
# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist
# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse.
#
# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer
# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher
# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der
# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall.
#
# Rollback: diese eine Policy im Namespace loeschen.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-block-metadata
namespace: monitoring
spec:
# Waehlt alle Pods AUSSER denen mit eigener, engerer Regel (#0088).
# Bleibt als Auffangnetz: Eine kuenftige Arbeitslast ohne eigene Policy
# landet hier - Metadaten gesperrt, Rest offen - statt voellig ungeregelt.
# ⚠️ Der Ausschluss MUSS hier stehen: NetworkPolicies sind additiv.
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: NotIn
values:
- alloy
- kube-state-metrics
policyTypes:
- Egress
egress:
# 1. DNS - zuerst, weil ohne DNS nichts geht.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1).
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz -
# ausser Link-Local.
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.0.0/16
---
# Welle 3 (#0088): Monitoring-Namespace, enge Regeln je Arbeitslast.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-nur-intern
namespace: monitoring
spec:
# Liefert Metriken, holt selbst nichts von aussen.
podSelector:
matchLabels:
app.kubernetes.io/name: kube-state-metrics
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-ops-host
namespace: monitoring
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: alloy
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# Der operating-Host im PRIVATEN Netz: Logs nach Loki (3100), Metriken per
# remote_write nach Prometheus (9090). Beide Ziele aus der laufenden
# Alloy-Konfiguration gelesen, nicht angenommen.
- to:
- ipBlock:
cidr: 10.0.0.3/32
ports:
- protocol: TCP
port: 3100
- protocol: TCP
port: 9090
+3 -3
View File
@@ -4,7 +4,7 @@ metadata:
name: coturn-secret
namespace: matrix
stringData:
TURN_SECRET: ENC[AES256_GCM,data:Cbu5SoxQp0L9WFZFbEiyK8j0IJuSgoJE9OpRKZVXyF+PcTM+ewD+3TI8xq+g0C34XDWeaJdxaj3B4yyKsqQLIA==,iv:lW8lD3D75Z0b0EfBj5myUrv4GWMvRTBELjhCVm+QJjc=,tag:nC5L/PcYPoLJP1whSSfSTA==,type:str]
TURN_SECRET: ENC[AES256_GCM,data:ATip8K3DKFyyInoJ9/e6SGJobGNruDgNn8fGopkct2cbTHKXduIZWjMEaZ4y47XS5d52qjbHSGSNm/uATsnN3A==,iv:vcGxyduSlqQPZv7fVmWgUypaSSMCDxF2m8WnDiiRaq0=,tag:P+lZAzQZYK5NY7KIr8igug==,type:str]
sops:
age:
- enc: |
@@ -26,6 +26,6 @@ sops:
-----END AGE ENCRYPTED FILE-----
recipient: age1x4jjwc8nuttwr8us924pvdc6dll5npkc6c8f4zf2hx5d2qu75dtqx0fm0d
encrypted_regex: ^(data|stringData)$
lastmodified: "2026-08-01T02:00:01Z"
mac: ENC[AES256_GCM,data:1Tx2/4O/fcv9BPYLXainmsILC2HbBJtJkgVnbkCe9oVSKca/hVf6zAa8Pd6n/tvHHAuU1Ghm2mglk/IxbNKgCDHH+xaEeK5sfhAK0Ot2ffIMLK5chBNMH0DpvTjqP8ttMidgPJ4XYzizB850gq7pSRrZBJCC2oG66F+WPnfKkxQ=,iv:NIDfNbIyWSgvjpXtQFVjnZFjtKx8sV3a45ssiebkHf0=,tag:hNDmHPTu4RyNMvjyfJRCmA==,type:str]
lastmodified: "2026-09-01T02:00:01Z"
mac: ENC[AES256_GCM,data:SNMa4VdFzNY5cC3Rv53XhH7J9ptePn5LQR6038m09zOaGa6M9iXrK6f3SgSh8+53u40IewyFf/5ZjwH7d7O08r3uZp+VPcEovubElqbQ6in/YPkWGZ/M1rDJ4y1JM5FlfcYHRqzbTsDwJco7SoYtUthSrmQDIX+Bdb04ZgwzTuQ=,iv:D9ZVhMk71lGaFFrdzeosRB4IJMxzic96QqUAkaCLPrk=,tag:feWHR1RQaroIvJlT80vluQ==,type:str]
version: 3.12.2
+1 -1
View File
@@ -88,7 +88,7 @@ spec:
# Bumped on every TURN_SECRET rotation (Issue #38) to force a new pod, since
# Kubernetes doesn't restart running pods when a referenced Secret's content
# changes and the initContainer that reads it only runs once at pod start.
rotated-at: "2026-08-01T02:00:01Z"
rotated-at: "2026-09-01T02:00:01Z"
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
@@ -15,6 +15,8 @@ data:
config.json: |
{
"brand": "aXion1337.Chat",
"_kommentar_custom_urls": "Entscheidung sorb 2026-08-19 (management #0099). Blendet den 'Bearbeiten'-Knopf am Servernamen aus (ServerPicker.tsx) - der Homeserver ist damit ueber die Oberflaeche nicht mehr wechselbar, und die Login-Fehlermeldung benennt bei 401/403 den Server statt generisch zu bleiben (ErrorUtils.tsx). Es ist eine OBERFLAECHEN-Sperre: der hs_url-Query-Parameter wird in MatrixChat.tsx (mobile Registrierung, Registrierungs-Bestaetigung) weiterhin ohne diese Pruefung uebernommen. Gegen das offene Advisory GHSA-wrcp-5v3v-3j6v (betrifft < 1.12.22, wir laufen 1.12.17) verengt das die Flaeche, ersetzt aber NICHT das Upstream-Update aus #0099. Dieselbe Zeile muss im Desktop-Client stehen - der hat seine eigene config.json.",
"disable_custom_urls": true,
"_kommentar_bug_report": "local = der Knopf erscheint als 'Protokolle herunterladen'; die Logs bleiben auf dem Geraet und gehen an KEINEN Server. Ohne diesen Wert ist die ganze Sektion unsichtbar und Nutzer kommen gar nicht an ihre Logs (ThreadNet-Web#9). Der element.io-Endpunkt wurde am 2026-08-01 bewusst entfernt - dieser Wert holt die Faehigkeit zurueck, ohne den Abfluss.",
"bug_report_endpoint_url": "local",
"_kommentar_report_event": "Weg B (Entscheidung sorb 2026-08-15): Meldungen bleiben im event_reports-Speicher des Servers und werden ueber Element Admin gesichtet - Draupnir bekommt dafuer BEWUSST keine Server-Admin-Rechte. Ohne diesen Text sieht der Melder nach dem Absenden gar nichts und weiss nicht, ob und an wen die Meldung geht (#0045).",
@@ -35,6 +35,13 @@ data:
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
# Link-local, enthaelt den Cloud-Metadaten-Dienst 169.254.169.254.
# Bis 2026-08-20 nur durch die NetworkPolicy gedeckt (#0088) - die
# Vorschau holt Adressen, die Nutzer bestimmen, also gehoert die Sperre
# auch an diese Stelle und nicht nur eine Ebene tiefer.
- '169.254.0.0/16'
# CGNAT - dahinter koennen fremde Netze liegen.
- '100.64.0.0/10'
- '::1/128'
- 'fe80::/10'
- 'fc00::/7'
@@ -69,6 +76,26 @@ data:
auto_join_rooms:
- "#onboarding:axion1337.chat"
auto_join_rooms_for_guests: false
# Foederation geschlossen (Entscheidung sorb 2026-08-19, management #0060,
# ADR-0021). Leere Liste = mit KEINEM Server foederieren; ein Eintrag hier
# oeffnet gezielt fuer diese eine Domain.
#
# Grundlage war eine Messung, keine Meinung: in vier Monaten Betrieb null
# Eintraege in `destinations`, null fremde Nutzer, null Raeume mit fremder
# Beteiligung - bei gleichzeitig oeffentlich erreichbarer Foederations-API
# (die Delegation fuehrt sie ueber 443, 8448 ist ohnehin zu).
#
# ⚠️ NICHT betroffen und darf es auch nicht sein:
# /_matrix/federation/v1/openid/userinfo. Ueber diesen Endpunkt prueft der
# MatrixRTC-Authorisation-Service (lk-jwt-service) die OpenID-Tokens, und er
# ruft ihn ueber den OEFFENTLICHEN Namen auf - das Deployment hat keine
# hostAliases und ClusterFirst-DNS. Synapse bedient ihn ohne
# X-Matrix-Signatur (REQUIRE_AUTH=False), die Whitelist greift dort also
# nicht. Wer hier auf Pfad-Ebene sperrt statt ueber diese Liste, legt die
# Gruppen-Calls lahm - genau der mrtc-Ausfall, nur mit anderer Ursache.
federation:
config: |
federation_domain_whitelist: []
room_publish:
config: |
room_list_publication_rules:
+68 -2
View File
@@ -12,7 +12,23 @@ spec:
chart:
spec:
chart: matrix-stack
version: "26.4.0"
# 26.8.0 (management #0051). Bringt Synapse v1.158.0, MAS 1.22.0,
# element-admin 0.1.12, lk-jwt-service 0.4.4 und LiveKit-SFU v1.12.0.
#
# NICHT 26.8.1: die erschien am 2026-08-21, also am Tag dieses Sprungs.
#
# Der Grund steht nicht in der CVE-Spalte (17 -> 6 CRITICAL, 217 -> 120 HIGH),
# sondern in Synapse v1.157.0: 'Fix a bug introduced in Synapse v1.150.0 where
# reactivating a deactivated and erased user did not restore their profile,
# breaking login, name changes, and invitations.' Wir fuhren v1.151.0 - das war
# die Ursache des Vorfalls vom 2026-08-21 (AAR clark-calls-reset).
#
# Alle Bruchstellen der Spanne gegen unseren Stand geprueft und gegenstandslos:
# keine Synapse-Worker (v1.152, ESS 26.5.0), stabile MAS-Integration statt
# MSC3861 bereits in Gebrauch (v1.157), kein livekitAuth.keysYaml (26.8.1).
# Unsere elementWeb-Ueberschreibung auf threadnet-web ueberlebt - beide
# Chart-Fassungen wurden mit unseren echten Werten gerendert und verglichen.
version: "26.8.0"
sourceRef:
kind: HelmRepository
name: element-ess-oci
@@ -74,6 +90,27 @@ spec:
cpu: 50m
limits:
memory: 128Mi
# Client-IP erhalten statt sie wegzu-SNATten.
#
# Mit dem Chart-Default `Cluster` verteilt kube-proxy eingehende NodePort-Pakete
# selbst weiter und ersetzt dabei die Absenderadresse durch die des Knotens. Der
# SFU sieht den Client deshalb nie unter seiner echten Adresse: Am 2026-08-19 war
# das ausgewaehlte ICE-Paar `prflx 10.42.0.1` - eine peer-reflexive Adresse aus
# dem Cluster-Netz. Das macht die Kandidatenwahl unnoetig instabil und ist die
# dokumentierte LiveKit-Empfehlung fuer Kubernetes.
#
# `Local` ist hier ohne Nachteil, weil der Cluster aus EINEM Knoten besteht: Der
# uebliche Preis - nur Knoten mit laufendem Pod nehmen Verkehr an - kann nicht
# greifen. Bei einem zweiten Knoten waere das neu zu bewerten.
#
# Was es NICHT behebt: das schubweise ICE-Flappen (16.08. = 119 Wechsel, 18.08.
# = 0). Das ist aelter und stammt aus der Zahl der Netzwerkpfade auf Client-Seite.
sfu:
exposedServices:
rtcMuxedUdp:
externalTrafficPolicy: Local
rtcTcp:
externalTrafficPolicy: Local
# Element Web
elementWeb:
@@ -81,6 +118,35 @@ spec:
image:
registry: rohana.axion1337.de
repository: sorb/threadnet-web
# v0.6.0-rc.2 = Anschluss an Element Web v1.12.26 (ADR-0022). Ein echter
# Merge-Commit statt Cherry-Picks; die Fork-Patches mussten umziehen,
# weil Upstream MImageBody.tsx geloescht und den Raumlisten-Inhalt nach
# RoomListItemContent ausgelagert hat. Betroffen sind genau die zwei
# Stellen, die die Abnahme pruefen muss: die ClamAV-Fehlermeldung im
# Bild-Pfad (ImageBodyViewModel.computeErrorLabel) und die
# Call-Teilnehmerliste in der Raumliste.
# Am 19.08. ausgerollt und nach wenigen Minuten zurueckgenommen: die
# Raumliste stuerzte bei jedem Eintrag ab (react-soft-crash), weil in
# RoomListItemViewModel.ts eine getValue-Zeile auf den von Upstream
# entfernten Labs-Schalter feature_room_list_sections stehenblieb -
# Sektionen laufen dort inzwischen ueber RoomList.showSections. Eine
# Leiche aus der Merge-Aufloesung, die kein Build fangen konnte:
# getValue nimmt einen String und wirft erst zur Laufzeit.
# rc.3 = derselbe Merge ohne den Rest, plus ein typecheck-Job, den
# docker_web als needs fuehrt: kein Image mehr ohne Typpruefung. Der
# web-Job baut nur, webpack wirft Typen weg - tsc hatte den Fehler die
# ganze Zeit gemeldet, gefragt hatte ihn niemand. Massstab ist "kein
# Fehler ausserhalb von node_modules", weil Upstream v1.12.26 selbst
# nicht typrein ist (matrix-js-sdk 42.2.0, in einem sauberen Checkout
# gegengeprueft).
# v0.6.0 = Abnahme auf rc.3 bestanden (19.08.), derselbe Quellstand
# 8ca03fe unter Release-Nummer. Geprueft am laufenden System, nicht nur
# am Build: Raumliste laedt, ClamAV blockt beim Senden (ein Scan-Aufruf,
# kein Upload), abgewiesene Datei zeigt die Meldung, und das .png wurde
# zugestellt, beim Herunterladen abgewiesen und die Meldung gerendert -
# damit ist der portierte Bild-Pfad belegt, nicht nur vermutet. Auch die
# Call-Teilnehmerliste, der zweite umgezogene Patch, steht richtig drin.
# Rueckhebel bleibt der Tag-Revert auf v0.5.4.
# v0.5.4 = Sender-Verifikation (threadnet-call fee9866): auf Safari
# uebersprang LiveKit den Track-Tausch stumm (sender?.replaceTrack),
# das rohe Mikro blieb auf der Leitung. Der Fork prueft und erzwingt
@@ -116,7 +182,7 @@ spec:
# Die Linie beginnt bei v0.3.0, dem ersten kanonischen CI-Build aus
# apps/web/Dockerfile - er loeste die Derivat-Images ab, deren
# Entrypoint ohne Exec-Bit /config.json still brach (ThreadNet-Web#8).
tag: v0.5.4
tag: v0.6.0
ingress:
host: axion1337.chat
+2 -1
View File
@@ -34,7 +34,8 @@ spec:
mountPath: /html
containers:
- name: nginx
image: nginx:1.26-alpine
# 1.26-alpine trug 2 CRITICAL; 1.31.4-alpine gemessen: 0 CRITICAL, 0 HIGH.
image: nginx:1.31.4-alpine
ports:
- containerPort: 8080
volumeMounts:
+3 -1
View File
@@ -16,7 +16,7 @@ patches:
value: "401f8a87d0ef5d91d2e5032d4aede42c"
- op: add
path: /metadata/annotations/turn-secret-checksum
value: "05aad8b742fb02c42f4c1a5629ae31e1"
value: "fd34214736091bf3ced17afe7bb4d61f"
resources:
- matrix-postgres-auth.yaml
@@ -42,6 +42,7 @@ resources:
- synapse-backup.yaml
# Monatliche Restore-Probe: spielt die Sicherungen isoliert zurueck (#0030)
- restore-drill.yaml
- restore-drill-media.yaml
# Automatisierte TURN-Secret-Rotation (Issue #38)
- turn-secret-rotation-secret.yaml
- turn-secret-rotation.yaml
@@ -65,6 +66,7 @@ resources:
- wiki-ingress.yaml
- wikijs-config.yaml # Konfig-Job (headless Setup + OIDC + Rollen)
- wikijs-postgres-backup.yaml # Nächtliches Borg-Backup der Wiki-DB (#0048)
- wikijs-gruppenpruefung.yaml # Tägliche Prüfung auf Nutzer ohne Gruppe (#0103)
# Synapse-Modul als eigene Datei gepflegt (lintbar/testbar), aber als ConfigMap gemounted -
# disableNameSuffixHash, da der Name in synapse-values.yaml's eingebettetem values.yaml
+322
View File
@@ -109,6 +109,23 @@ spec:
port: haproxy-403
- protocol: TCP
port: haproxy-wkd
# Matrix Authentication Service ruft Synapse ueber haproxy.
#
# ⚠️ Bis ESS 26.4.0 zeigte MAS DIREKT auf matrix-stack-synapse-main; dorthin
# erlaubt allow-ingress-synapse den Zugriff, hier war nichts noetig. ESS 26.8.0
# stellt matrix.endpoint auf den haproxy-Dienst matrix-stack-synapse um - und
# damit lief MAS in die default-deny-Regel. Folge: /oauth2/token antwortete mit
# 500 'failed to provision device', die Anmeldung brach ab (2026-08-21, #0051).
#
# Die Lehre steht im AAR: Eine Regel, die auf einen Dienstnamen zeigt, haengt an
# einer Entscheidung des Charts, nicht an unserer.
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: matrix-authentication-service
ports:
- protocol: TCP
port: haproxy-synapse
# Draupnir (Issue #18) calls Synapse's client-server API directly, in-namespace -
# without this it would be silently blocked by the default-deny policy.
- from:
@@ -405,6 +422,311 @@ spec:
- podSelector:
matchLabels:
app.kubernetes.io/name: wikijs-backup
# Tägliche Gruppenprüfung (wikijs-gruppenpruefung.yaml, #0103) liest nur.
- podSelector:
matchLabels:
app.kubernetes.io/name: wikijs-gruppenpruefung
ports:
- protocol: TCP
port: postgres
---
# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088).
#
# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei
# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy
# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea
# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen.
#
# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der
# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19:
# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und
# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist
# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse.
#
# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer
# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher
# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der
# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall.
#
# Rollback: diese eine Policy im Namespace loeschen.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-block-metadata
namespace: matrix
spec:
# Waehlt alle Pods AUSSER denen mit eigener, engerer Regel (#0088).
#
# WER HIER NOCH DRIN STEHT, UND WARUM (Stand 2026-08-21, Welle 4):
#
# synapse-main Foederation zu beliebigen Matrix-Servern und URL-Vorschau
# zu Adressen, die Nutzer bestimmen. Ein Ziel laesst sich
# nicht angeben. Die richtige Kontrolle sitzt eine Ebene
# hoeher: url_preview_ip_range_blacklist in synapse-values,
# seit 2026-08-20 inkl. 169.254.0.0/16 und 100.64.0.0/10.
#
# coturn Relais fuer Medien zu beliebigen Clients - das IST der
# matrix-rtc-sfu Dienst. Eine Zielliste gaebe es nur, wenn man wuesste,
# wer anruft.
#
# clamav Signaturen von database.clamav.net. Loest auf Cloudflare
# auf, Adressen wechseln. Pinnen braeche den Abgleich STILL
# - Signaturen veralten, der Dienst laeuft weiter, niemand
# merkt es. Cloudflare-Bereiche zu erlauben waere keine
# Einschraenkung. Besserer Weg waere ein interner Spiegel;
# das ist eigene Infrastruktur und bewusst nicht Teil von
# #0088. Bis dahin: Ausgang offen, Grund hier.
#
# ⚠️ Der Ausschluss muss HIER stehen, nicht in einer zusaetzlichen engen Policy:
# NetworkPolicies sind additiv. Solange diese Policy 0.0.0.0/0 fuer einen Pod
# erlaubt, aendert eine zweite, strengere Regel fuer denselben Pod nichts.
#
# `NotIn` waehlt auch Pods, die das Label gar nicht tragen - am 2026-08-20 im
# Cluster nachgemessen (29 Pods, 1 Treffer, notin liefert 28). Drei Pods im
# Namespace fuehren kein app.kubernetes.io/name, deshalb der zweite Ausdruck
# auf `app`. Beide Ausdruecke sind UND-verknuepft.
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: NotIn
values:
- element-web
- element-admin
- postgres
- wikijs-postgres
- clamav-http-scanner
- wikijs-gruppenpruefung
- haproxy
- draupnir
# Welle 4 (#0088): Upstream ist Authentik ueber den oeffentlichen
# Namen, der auf die Knoten-Adresse zeigt - die unterliegt der
# Policy nicht. Homeserver ueber den internen Service-Namen.
- matrix-authentication-service
# Welle 2 (#0088): je genau ein Ziel, eigene Policy unten.
- synapse-backup
- wikijs-backup
- turn-secret-rotation
- wikijs
# Welle 1 (#0088): kein externer Bedarf nachweisbar.
- matrix-rtc-authorisation-service
- wikijs-config
- init-secrets
- synapse-check-config
- deployment-markers-pre
- deployment-markers-post
- key: app
operator: NotIn
values:
- element-web-docs
- concierge-bot
policyTypes:
- Egress
egress:
# 1. DNS - zuerst, weil ohne DNS nichts geht.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1).
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz -
# ausser Link-Local.
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.0.0/16
---
# Pods ohne jeden Bedarf an Ausgang nach draussen (#0088).
#
# Zehn Arbeitslasten sprechen ausschliesslich cluster-intern: die drei nginx-Dienste,
# beide Postgres, der ClamAV-Vorschalter, haproxy, die Wiki-Gruppenpruefung und die
# beiden Matrix-Bots (Foederation ist geschlossen, Draupnir holt keine fremden
# Policy-Listen). Fuer sie faellt der pauschale 0.0.0.0/0-Satz weg; erlaubt bleibt
# DNS und das Cluster-Netz.
#
# Zwei Policies, weil ein podSelector nicht ueber zwei Label-Schluessel odern kann -
# die Vereinigung mehrerer Policies leistet genau das.
#
# Rollback: beide Policies loeschen und den Ausschluss in egress-block-metadata
# zuruecknehmen.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-nur-intern
namespace: matrix
spec:
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- element-web
- element-admin
- postgres
- wikijs-postgres
- clamav-http-scanner
- wikijs-gruppenpruefung
- haproxy
- draupnir
# Welle 4 (#0088): Upstream ist Authentik ueber den oeffentlichen
# Namen, der auf die Knoten-Adresse zeigt - die unterliegt der
# Policy nicht. Homeserver ueber den internen Service-Namen.
- matrix-authentication-service
# Welle 1 (#0088): Deploy- und Hilfs-Arbeitslasten.
- matrix-rtc-authorisation-service
- wikijs-config
- init-secrets
- synapse-check-config
- deployment-markers-pre
- deployment-markers-post
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
---
# Dieselben Regeln fuer die zwei Pods, die kein app.kubernetes.io/name fuehren.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-nur-intern-app-label
namespace: matrix
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- element-web-docs
- concierge-bot
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
---
# Welle 2 (#0088): Arbeitslasten mit genau EINEM externen Ziel.
#
# Eigene Policies statt Aufnahme in egress-nur-intern: Die Egress-Regeln einer
# Policy gelten fuer ALLE von ihr gewaehlten Pods. Haenge man die Storage Box an
# egress-nur-intern, bekaemen sie auch die vierzehn, die sie nicht brauchen.
#
# ⚠️ Die Namen stehen zusaetzlich in der NotIn-Liste von egress-block-metadata.
# Ohne das bliebe die breite Erlaubnis bestehen und diese Regeln waeren
# wirkungslos - NetworkPolicies sind additiv.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-storage-box
namespace: matrix
spec:
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- synapse-backup
- wikijs-backup
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# Hetzner Storage Box, borg ueber SSH. Einziges Ziel ausserhalb des
# Clusters - kein privater Weg dorthin bekannt (Stand 2026-08-21).
- to:
- ipBlock:
cidr: 91.98.246.178/32
ports:
- protocol: TCP
port: 23
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-rohana
namespace: matrix
spec:
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
# Schreibt den rotierten TURN-Schluessel nach Gitea - die einzige
# bewusste Gitea-Ausnahme der Gruppe (AGENTS.md).
- turn-secret-rotation
# git-storage von Wiki.js: https://rohana.axion1337.de/sorb/ThreadNetWiki.git
# (aus der laufenden Konfiguration gelesen, nicht angenommen).
- wikijs
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.42.0.0/16
- ipBlock:
cidr: 10.43.0.0/16
# rohana ueber das PRIVATE Netz. Der oeffentliche Name zeigt seit
# coredns-custom.yaml hierher; das Zertifikat gilt auch auf diesem Weg.
- to:
- ipBlock:
cidr: 10.0.0.3/32
ports:
- protocol: TCP
port: 443
+144
View File
@@ -0,0 +1,144 @@
# Monatliche Medien-Restore-Probe (#0030). Gegenstueck zu restore-drill.yaml: dort
# Datenbanken, hier der Medienspeicher von Synapse.
#
# Warum eigener Job und nicht angehaengt: Die Datenbank-Probe braucht eine Wegwerf-Postgres,
# diese hier das Produktions-PVC (lesend). Zwei verschiedene Rechte- und Fehlerbilder in
# einem Job zu buendeln macht die Fehlersuche im Ernstfall schwerer, nicht leichter.
#
# Der NAME ist bewusst `restore-drill-media`: Die bestehende Regel BackupJobFailed matcht
# `restore-drill.*`, also ist der Fehlschlag ohne neue Alarmregel abgedeckt und landet
# ueber die einzige Alertmanager-Route im wartung-Raum.
#
# ⚠️ Das Produktions-PVC wird `readOnly: true` gemountet. Geschrieben wird ausschliesslich
# nach emptyDir. Das PVC ist ReadWriteOnce; im heutigen Ein-Knoten-Cluster ist ein zweiter
# lesender Mount unproblematisch. Kaeme je ein zweiter Knoten dazu, koennte der Pod dort
# landen und der Mount scheitern - der Job schluege fehl und alarmierte, ohne dass an den
# Daten etwas waere. Dann gehoert hier eine Node-Affinitaet zu Synapse hin.
apiVersion: batch/v1
kind: CronJob
metadata:
name: restore-drill-media
namespace: matrix
spec:
# 4. des Monats, 05:20 - eine Stunde nach der Datenbank-Probe, damit beide denselben
# frischen Sicherungsstand pruefen, sich aber nicht um Storage Box und CPU streiten.
schedule: "20 5 4 * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
metadata:
labels:
app.kubernetes.io/name: synapse-backup # NetworkPolicy/Egress wie die Backups
app.kubernetes.io/component: restore-drill-media
spec:
restartPolicy: OnFailure
containers:
- name: drill
image: rohana.axion1337.de/sorb/axion-backup:v2
env:
- name: BORG_PASSPHRASE
valueFrom:
secretKeyRef:
name: synapse-backup-credentials
key: borg-passphrase
- name: SSH_PRIVATE_KEY_FILE
value: /secrets/ssh/ssh-private-key
- name: SSH_KNOWN_HOSTS_FILE
value: /secrets/known-hosts/known_hosts
command: ["sh", "-c"]
args:
- |
set -eu
export BORG_RSH="ssh -i $SSH_PRIVATE_KEY_FILE -o UserKnownHostsFile=$SSH_KNOWN_HOSTS_FILE -o StrictHostKeyChecking=yes"
export BORG_REPO='ssh://u641795@u641795.your-storagebox.de:23/./synapse-backup'
cd /scratch
latest=$(borg list --last 1 --format '{archive}' "$BORG_REPO")
echo "[drill] Archiv: $latest"
borg extract "$BORG_REPO::$latest" media/media_store
anz=$(find media/media_store -type f | wc -l)
echo "[drill] zurueckgespielt: ${anz} Dateien, $(du -sk media/media_store | cut -f1) KB"
echo "[drill] Produktion jetzt: $(find /live/media_store -type f | wc -l) Dateien, $(du -sk /live/media_store | cut -f1) KB"
if [ "$anz" -le 0 ]; then
echo "[drill] FEHLER: nichts extrahiert - Sicherung unbrauchbar!"
exit 1
fi
( cd /scratch/media/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/restore.sha
( cd /live/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/live.sha
awk '{print $2}' /scratch/restore.sha | sort > /scratch/r.names
awk '{print $2}' /scratch/live.sha | sort > /scratch/l.names
comm -12 /scratch/r.names /scratch/l.names > /scratch/beide.names
# Die Sicherung ist eine Momentaufnahme, die Produktion laeuft weiter.
# Beweiskraeftig ist deshalb die Schnittmenge - die Gesamtzahlen weichen
# regulaer ab, weil Synapse seine Vorschau-Caches selbst aufraeumt.
abw() { awk 'NR==FNR{h[$2]=$1;next} ($2 in h) && (h[$2]!=$1){print $2}' /scratch/live.sha "$1"; }
n_abw=$(abw /scratch/restore.sha | wc -l)
echo "[drill] gemeinsam: $(wc -l < /scratch/beide.names), davon abweichend: ${n_abw}"
if [ "$n_abw" -ne 0 ]; then
echo "[drill] FEHLER: gemeinsame Dateien sind NICHT byte-gleich:"
abw /scratch/restore.sha | head -10
exit 1
fi
# Nur url_cache und url_cache_thumbnails duerfen einseitig sein - alles
# andere waere ein echter Fehlbetrag und kein Cache-Effekt.
fremd=$(comm -23 /scratch/r.names /scratch/l.names | sed 's|^\./||' | cut -d/ -f1 \
| grep -vE '^url_cache(_thumbnails)?$' | sort -u || true)
if [ -n "$fremd" ]; then
echo "[drill] FEHLER: einseitige Dateien ausserhalb der Vorschau-Caches:"
echo "$fremd"
exit 1
fi
# Positivkontrolle bei JEDEM Lauf: eine gemeinsame Datei um ein Byte
# veraendern und pruefen, dass der Vergleich es merkt. Ein Abgleich, der
# nur je "gleich" gesagt hat, ist eine Vermutung - auch als CronJob.
opfer=$(head -1 /scratch/beide.names)
printf 'x' >> "/scratch/media/media_store/${opfer}"
( cd /scratch/media/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/kaputt.sha
if [ "$(abw /scratch/kaputt.sha | wc -l)" -eq 0 ]; then
echo "[drill] FEHLER: der Vergleich erkennt eine absichtliche Aenderung NICHT - diese Probe beweist nichts!"
exit 1
fi
echo "[drill] OK - ${anz} Dateien zurueckgespielt, alle gemeinsamen byte-gleich, Vergleich gegengeprueft"
volumeMounts:
- name: ssh-key
mountPath: /secrets/ssh
readOnly: true
- name: known-hosts
mountPath: /secrets/known-hosts
readOnly: true
- name: scratch
mountPath: /scratch
- name: live
mountPath: /live
readOnly: true
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
volumes:
- name: ssh-key
secret:
secretName: synapse-backup-credentials
defaultMode: 0400
items:
- key: ssh-private-key
path: ssh-private-key
- name: known-hosts
configMap:
name: synapse-backup-known-hosts
- name: scratch
emptyDir:
sizeLimit: 4Gi
- name: live
persistentVolumeClaim:
claimName: matrix-stack-synapse-media
readOnly: true
+3 -3
View File
@@ -4,7 +4,7 @@ metadata:
name: synapse-turn-secret
namespace: matrix
stringData:
values.yaml: ENC[AES256_GCM,data:jGoBDvjGymp4qEVEB6hzgRJk1ZUVB+jNxfhqpl7tG7UQmZGskix43rMJIOdDZihBTOUHP4emhlOzTxqx3SCAFCb8Ie78xA+DfjuLnQ6PQ2jqN48+yc7QE2xbNr5eWlYYKzl1u/z0kCQT723ptA+tQzYCzUq5U624HHxki0firgoSJmaF9S/vbwXHLPqtnviYX48Eim0suwMnavWdWGd1X3yvqUoZnWKz3eDQrH3cp4fQAdnRosveIcWwz+1na67Zshczq7hhlNp/WZ95guAOiqlLxGMPwJQJou24UYynwQTtDle5IzCxOww+sCNuQtPp29NE1gZ5pxIM7Ys6Ul7UxtPAww6OCVbkYYGmQxIq0cY4CrFrgLDJYQ2JJPIexc1ooeIDcYiNYts/2TCAj9ck+Vd+6xN1NEYwm0ZTLL9naLJJ1PEzRkfy3bCOTfKWCRNwLzn/3gsRgr+hrf5dJ6Av6VbM0Ae9xhTEQ4d64C8YRjb4G9BnDRGuaHu+5DYAqiFUEMm6cM4IV3E=,iv:ZRGQREujdrDCNj1OcgV7HAjZREArdnodnD2J5BxkGQo=,tag:2FOO9/rv4IteyulLnsR/YA==,type:str]
values.yaml: ENC[AES256_GCM,data:xc/pUhIbmGizSCS/P5EI+5zgxyoM7SiHOo9loGmUfQ0iiL+EntJlK4WiaHwpvAwCDA/N+4IrUMp5kBjAnCz/Y4qD/geXz932ccG4mIH1ncD5F/2/mFVN+gqo4q+xwy27wzR1RS2p3hJN7z11HrXazqMIgDvz3EXqeYYD6YnM8Z1IfFdGA+daSSoRDiiqh9XFL7g2aVKomWImxkP4kXU4vJCoXXmd3DF8guZv7zQ1uhJtQOzGHF1cST+pxBV4/+zkpdY7Du7L3cNc7qk/oJNbMh3VEyB6Yi3krcLLbXuTSXZ9qBHhXLrsWngxoXRSo++PIfbqAuJ1iSzQ8Zz0NUbqOS+tILLo/Twe9DjwT5hunIPX1vIN/4SOqPgS0ZQm+VISs5eixxYzAA8xvIbc/0twbWA+33HEo5G6mXe6uzoNqsWE1PKYw7gl7vbVqnTYkyMQXdAPIbfKaW4JQ0zkpeFnJpGdKnYse2xwfofEtV6aiV3eNo1IyUjsp2kN7F4RBd1e1SqrI9nyUcM=,iv:MFc+1fDPK5bg607mMN2sDAufmFzE7hHqEmnejMjmiCI=,tag:pgFMtvcm3a9I+coXMO0xXA==,type:str]
sops:
age:
- enc: |
@@ -26,6 +26,6 @@ sops:
-----END AGE ENCRYPTED FILE-----
recipient: age1x4jjwc8nuttwr8us924pvdc6dll5npkc6c8f4zf2hx5d2qu75dtqx0fm0d
encrypted_regex: ^(data|stringData)$
lastmodified: "2026-08-01T02:00:01Z"
mac: ENC[AES256_GCM,data:zk0ivb9asZLHUg819tR8GV5R5ViSiJId0T1o2GqYWjc/AcNK1pBKwIJ+S3TuB0fLz3qaFXUBfzcc+CfztuTLcRzAfu3mF8Hv0boZTe0lGl5XugmozQWjoLjSw+roWZHlXd5CfYbDZsHSO70231NyPlpuUkVr2fqAFZgTEev2A9Y=,iv:IkRYK6PLE74i+Kq5fop4ddS+Q9KW8uNVfABK81uFyIw=,tag:kanZfzBXEYodnQtwWdZ85g==,type:str]
lastmodified: "2026-09-01T02:00:01Z"
mac: ENC[AES256_GCM,data:VdjLTJSa4ZfWLnola8F4u5q50a9Z6+h+Dbb9MKgGz3v+F2II+u29XrmXDS0Xpqx7QMjpzPdDp4gvyBecXxk5p+nzoC2lHSENvrZsloTQi18wEX04C53IXteoB6ijJ8VR6xC0GyKhXgIxo297dqfndSqgsYmQB5w2WKvzmcb+Ctg=,iv:9AWwKsGiJJMe8qpo534/cZT/2bW4DXeiRhzneSPF24s=,tag:n6bSrwV+9WVMzBsh8ou7OA==,type:str]
version: 3.13.3
+115
View File
@@ -0,0 +1,115 @@
# Taegliche Pruefung: Gibt es Wiki-Nutzer ohne Gruppe? (management #0103)
#
# Der Fall, den dieses Issue ausgeloest hat: @apo meldete sich erfolgreich an, landete in
# keiner Gruppe und sah deshalb nichts - ohne Fehlermeldung, ohne Logzeile, ohne Zaehler.
# Aufgefallen ist es erst, weil er sich beschwerte. Das Auto-Enrollment verhindert den Fall
# jetzt; diese Pruefung stellt sicher, dass ein erneutes Versagen NICHT wieder auf eine
# Beschwerde angewiesen ist (Abnahmekriterium des Issues).
#
# Bewusst eine taegliche Stichprobe auf der Datenbank statt einer Instrumentierung des
# Logins: Wiki.js meldet den Zustand "angemeldet, aber ohne Gruppe" nirgends, und ein
# Patch am Anmeldeweg waere fuer diesen Zweck unverhaeltnismaessig. Ein Tag Verzug ist
# hinnehmbar - der Fall lag vorher Tage unbemerkt.
#
# Die Ausgabe geht nach stdout und damit ueber Alloy nach Loki. Suchbegriff fuer eine
# Abfrage oder ein Panel: "Wiki-Nutzer ohne Gruppe".
apiVersion: batch/v1
kind: CronJob
metadata:
name: wikijs-gruppenpruefung
namespace: matrix
spec:
schedule: "15 6 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
metadata:
labels:
# Diese Marke steht auch in networkpolicy.yaml - ohne sie kaeme der Pod nicht
# an die Datenbank, und zwar lautlos.
app.kubernetes.io/name: wikijs-gruppenpruefung
app.kubernetes.io/component: pruefung
spec:
restartPolicy: Never
containers:
- name: pruefung
image: postgres:15-alpine
env:
- name: PGHOST
value: wikijs-postgres
- name: PGDATABASE
value: wiki
- name: PGUSER
valueFrom:
secretKeyRef:
name: wikijs-postgres-secret
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: wikijs-postgres-secret
key: password
command:
- /bin/sh
- -c
- |
set -eu
# Warten, bis die Datenbank erreichbar ist - und zwar wirklich warten:
# Die NetworkPolicy-Regeln fuer einen NEU erzeugten Pod sind beim Start
# des Containers noch nicht programmiert. In diesem Fenster wird die
# Verbindung abgewiesen ("Connection refused"), obwohl Ziel, Marken und
# Regel korrekt sind. Gemessen am 2026-08-20: sofort nach dem Start
# abgewiesen, nach 20 Sekunden erfolgreich.
#
# Ohne diese Schleife scheitert der Job jede Nacht am Wettlauf und meldet
# einen Fehler, den niemand nachvollziehen kann - oder schlimmer: Man
# gewoehnt sich an den roten Job.
bereit=""
for versuch in 1 2 3 4 5 6 7 8 9 10 11 12; do
if psql -Atc 'select 1;' >/dev/null 2>&1; then bereit="ja"; break; fi
sleep 5
done
if [ -z "$bereit" ]; then
echo "FEHLER: Datenbank nach 60 s nicht erreichbar - Pruefung nicht gelaufen."
exit 1
fi
# Gegenprobe: Ohne sie sieht "0 Nutzer ohne Gruppe" bei einer leeren oder
# falschen Datenbank genauso aus wie ein sauberes Ergebnis - stiller
# Erfolg ist hier das groessere Risiko als ein Fund.
gesamt="$(psql -Atc 'select count(*) from users;')"
if [ -z "$gesamt" ] || [ "$gesamt" -eq 0 ]; then
echo "FEHLER: keine Nutzer gelesen - Datenbank leer oder falsche Datenbank."
exit 1
fi
ohne="$(psql -Atc '
select count(*) from users u
left join "userGroups" ug on ug."userId" = u.id
where ug."userId" is null;')"
if [ "$ohne" -gt 0 ]; then
echo "WARNUNG: $ohne von $gesamt Wiki-Nutzer ohne Gruppe - sie sehen nach der Anmeldung nichts (management #0103)"
# -At mit Vorgabe-Trenner "|" statt eines eingebetteten E'\t':
# Die Anfuehrungszeichen-Verschachtelung waere die wahrscheinlichste
# Fehlerquelle in dieser Datei und traegt nichts zur Aussage bei.
psql -Atc '
select u.id, u.email from users u
left join "userGroups" ug on ug."userId" = u.id
where ug."userId" is null order by u.id;' \
| while IFS= read -r z; do echo " betroffen: $z"; done
echo "Behebung: Auto-Enrollment der OIDC-Strategy pruefen (wikijs-config.py, Gruppe wiki-anwender)."
else
echo "OK: kein Wiki-Nutzer ohne Gruppe ($gesamt geprueft)"
fi
resources:
requests:
memory: 32Mi
cpu: 10m
limits:
memory: 96Mi
File diff suppressed because it is too large Load Diff