aXion1337 Light ist das exakte helle Gegenstueck zum Lieblingstheme: 'aXion1337
Dark' ist Gruvbox Dark (Farben und die acht Username-Farben sind die
Gruvbox-Palette), also ist Gruvbox Light das stilistische Analogon - gleiche
Rollenverteilung, gleiche Akzentfamilie, nur die Helligkeitsachse gespiegelt.
Dazu zehn benannte Paletten mit vollem Schluesselsatz (24 Farben inkl.
username-colors): Ocean Depths, Sunset Boulevard, Forest Canopy, Modern
Minimalist, Golden Hour, Arctic Frost, Desert Rose, Tech Innovation, Botanical
Garden, Midnight Galaxy - vier davon hell.
Chirurgisch eingefuegt (Lehre vom 2026-07-30): die YAML wurde nicht geparst und
nicht neu serialisiert, nur Text vor der schliessenden Array-Klammer ergaenzt.
Diff-Bilanz 0 entfernte / 427 neue Zeilen, YAML und eingebettetes JSON validiert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der naechtliche YAML-Redump (9aa42f9/e963203) hatte Kommentare und
Formatierung der Datei zerstoert - semantisch war er korrekt (verifiziert),
aber wartungsfeindlich. Jetzt: Original von c631b0a + exakt die 5 Zeilen
aus ThreadNet-Web#1 (4 Feature-Flags + setting_default). Effektive Config
byte-identisch mit dem bereits deployten Stand.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Web-Deploy war Teilmenge der Desktop-Defaults: video_rooms, group_calls,
element_call_video_rooms, new_room_decoration_ui + setting_default
feature_group_calls ergaenzt. Discord-Raumliste war schon beidseitig an.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Automatischer Rotations-PR (Gitea #46) darf seit der Topologie-Umstellung
nicht mehr auf dem Mirror gemergt werden - Branch vom Mirror geholt und
hier kanonisch gemergt; der Push-Mirror traegt den Stand zurueck.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
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.
Das prometheus-node-exporter-HelmRelease (hostNetwork) kollidierte auf
dem Host mit dem etablierten systemd-Exporter auf Port 9100 und konnte
seit Anlage nie binden (4883 Restarts, CrashLoopBackOff). Host-Metriken
kommen weiterhin vom systemd-Dienst, den CFGMON direkt via 10.0.0.2:9100
scrapt - das DaemonSet haette sie nur dupliziert. Alloy-Scrape auf den
verwaisten Service ebenfalls entfernt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der Service matrix-stack-synapse-main exponiert 8008/9093/8080/9001
(synapse-metrics = 9001) - Port 9000 existiert nicht, der Scrape war
seit Anlage tot. Aufgedeckt durch den ersten TargetDown-Alarm des neuen
Alertings (gitops#32) direkt nach dem Deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
First full rebuild deployed to production (previous releases used the
widgets/element-call/-only patch workaround, fixed but not cut over in
Issue #12). Includes the new client-side scan hooks (DecryptFile.ts,
ContentMessages.ts) that cover encrypted rooms, which the server-side
Synapse module (Issue #19) structurally cannot see.
Synapse's own media-scanning module (Issue #19) can never see E2EE
attachment content - a structural limit, not a bug. This adds a small
standalone HTTP wrapper around the same ClamAV instance, reachable
from browser JS at /_scan, so the ThreadNet-Web client fork can scan
plaintext both before encrypting/uploading and after downloading/
decrypting - covering both directions regardless of room encryption.
Auth via Synapse's own /whoami endpoint, no separate auth system.
Synapse runs on Twisted's reactor, not asyncio's event loop - the
original asyncio.open_connection/wait_for calls failed immediately
with "RuntimeError: no running event loop", silently fail-opening
every scan (confirmed live: EICAR test file passed through unscanned).
Rewritten using twisted.internet.endpoints.HostnameEndpoint/
connectProtocol and a custom Protocol for the INSTREAM conversation.
Deploys ClamAV and a small stdlib-only Synapse spam-checker module
implementing check_media_file_for_spam over clamd's INSTREAM protocol.
Unlike the originally-considered matrix-content-scanner proxy (which
needs client-side cooperation neither Element Web nor Element X
provide), this hooks Synapse's own module API directly - transparent
to every client for unencrypted media. No custom Synapse image needed:
the module is mounted via a ConfigMap onto PYTHONPATH using the ESS
chart's extraVolumes/extraVolumeMounts/extraEnv support. Fails open on
scanner errors so a ClamAV outage can't block all uploads.
Testing E2EE support for the management room per user request - marked
"not considered production safe" upstream, verifying no regressions
before deciding to keep it.
Confirmed by extracting dist/config.js from the actual image - v3.x's
getConfigPath() only checks --draupnir-config/--mjolnir-config CLI
flags, no longer NODE_CONFIG_DIR/node-config auto-discovery.
initialManager's automatic management-room creation (no manual room
setup needed) only works on v3.1.0+ - v2.9.0 crashed on first startup
trying to join a room that was never created.
Bot account registered via mas-cli, compatibility token issued and
stored via sops. Adds an explicit NetworkPolicy allow-rule since
Draupnir needs to reach Synapse's client-server API in-namespace,
which the existing default-deny policy would otherwise silently block.
Real testing (Safari + Firefox, fresh rejoins) confirmed VP9 selection
always falls back to VP8 - our fork's buildPublishOptions() forces
simulcast:true unconditionally, which breaks LiveKit's SVC negotiation
path that vp9/av1 require. This is the same failure mode originally
suspected in the 2026-07-28 incident; the "LiveKit handles this
automatically" assumption from doc research did not hold up in
practice. Reverting to the known-safe VP8/H.264/H.265 SFU allow-list
and widget image. Root cause documented in Issue #11 for a future,
scoped fix (conditional simulcast vs. scalabilityMode per codec).
Patches the re-enabled VP9/AV1 codec dropdown (threadnet-call) onto the
existing v0.1.0 base image. Test-only tag - to be reverted or made
permanent depending on real call test results.
Appends video/VP9 and video/AV1 to the SFU's room.enabled_codecs
allow-list via matrixRTC.sfu.additional, keeping all currently
negotiated codecs unchanged. Opt-in test per Issue #11 - codecs
become selectable, not the new default.
Live-verified via matrix-rtc-sfu logs: VP9 gets silently rejected and
falls back to VP8 (SFU's enabledPublishCodecs is [VP8, H264, H265], no
VP9/AV1). Dropdown now only offers those three; also fixed
screenShareCodec's default which was "vp9" (fork commit ac99cc65 in
threadnet-call).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The MediaQualitySettings labels moved into the Video tab had no German
translations at all (en/app.json had all 10 keys, de/app.json had none) -
fork commit f13c4b43 in threadnet-call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moved from behind the Developer-mode toggle into the regular Video
settings tab (fork commit f61650c0 in threadnet-call) - these are just
resolution/framerate/bitrate/codec pickers, not actually developer-only
functionality, just accidentally buried where most users would never
find them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live-tested Issue #38's rotation: after merging the automated rotation
PR, coturn restarted quickly (Kustomization-level, 1m interval), but
synapse-main lagged behind since it depends on this separate HelmRelease
with its own 5m interval - a real (self-healing, but avoidable) window
where coturn had the new TURN secret and Synapse still had the old one,
which would reject each other's credentials/relayed media. Matching the
interval to production-apps's 1m tightens that window without needing
any new automation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Job hung indefinitely on git clone - public-internet reachability to
rohana.axion1337.de from inside pods is currently down (related to
Issue #41's firewall/private-network findings), and the node-level
/etc/hosts workaround only helps containerd's image pulls, not processes
running inside pods (they resolve via CoreDNS + their own /etc/hosts, not
the node's). hostAliases routes this job's git/API traffic over the
private Hetzner network directly - verified working with a real pod.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes issue #38's automation half (architecture fix + first rotation
already landed in earlier commits this session). Monthly CronJob
(rohana.axion1337.de/sorb/axion-secret-rotation:v1 - alpine + git/sops/
jq/age) that:
- generates a new secret
- re-encrypts coturn-secret.yaml and synapse-turn-secret.yaml using the
scoped rotation-only age key (added as an additional recipient in an
earlier commit) - never touches the repo's master sops-age key
- bumps the turn-secret-checksum (HelmRelease annotation) and rotated-at
(coturn Deployment annotation) so merging actually restarts both
consumers, reusing the existing checksum-annotation pattern already in
this repo rather than inventing a new mechanism
- opens a Pull Request rather than pushing straight to main - a human
reviews and merges, keeping a checkpoint before production picks up new
credentials while still automating the tedious coordination work
Needs a Gitea PAT (repo write scope) filled into
turn-secret-rotation-secret.yaml's gitea-token key before first use.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a second age recipient (dedicated keypair, private key never touches
the repo) to just these two files, so the upcoming rotation CronJob can
decrypt/re-encrypt them without needing the repo's master sops-age key,
which can decrypt every secret in the repo. Verified: master key still
decrypts both files; the new scoped key can decrypt both files but
correctly cannot decrypt an unrelated secret (authentik-secret.yaml).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Server-side apply left the old RollingUpdate defaults (maxSurge/maxUnavailable)
on the live object from before strategy.type was ever set explicitly, which
the API rejects when combined with type: Recreate. Explicit null clears it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Discovered live: the default RollingUpdate strategy tries to schedule the
new pod before removing the old one, which can never succeed for a
hostNetwork Deployment on a single-node cluster (both pods would need the
same node ports 3478/5349 simultaneously). Surfaced by the rotated-at
annotation bump from the previous commit getting stuck in
FailedScheduling. Recreate kills the old pod first, freeing the ports.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Issue #38 discussion surfaced a real bug: the TURN shared secret was
duplicated - correctly SOPS-encrypted in coturn-secret.yaml, but also
hardcoded in plaintext in synapse-values.yaml (a plain, non-SOPS
ConfigMap), visible in git history. Also found turn_user_lifetime is
86400000ms (24h), not "short-lived" as previously assumed - raising the
stakes of the leak somewhat.
Extracted the turn config block into its own dedicated SOPS-encrypted
Secret (synapse-turn-secret.yaml), wired via a second HelmRelease
valuesFrom entry (same pattern already used for ess-mas-values-secret).
Rotated the value while doing this, so the leaked plaintext secret is no
longer live anywhere.
Added checksum/rotated-at annotations (matrix-stack HelmRelease's
existing element-config-checksum patch gets a sibling turn-secret-checksum;
coturn's Deployment pod template gets a rotated-at annotation) so future
rotations actually restart both consumers - Kubernetes doesn't restart
running pods when a referenced Secret's content changes on its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live testing showed pg_dump got "Connection refused" immediately at job
start, but a manual pg_isready/psql from an identically-labeled pod a few
seconds later succeeded fine every time. The cluster's NetworkPolicy
controller needs a brief moment to program a brand-new pod's IP into the
target's allowed ingress rules; a job that starts dumping instantly can
race that window. backup.sh now waits for pg_isready before each pg_dump
(up to 15 tries / ~30s) instead of assuming connectivity is ready at
container start.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes issues #6 and #15 (treated jointly, per combined analysis in their
comments). Two CronJobs, one per namespace, each pushing to its own Borg
repo on the newly booked Storage Box (u641795.your-storagebox.de:23, BX11
1TB) with its own repo passphrase - a leaked passphrase for one doesn't
expose the other:
- matrix: synapse-backup dumps the synapse + matrixauthenticationservice
DBs (shared postgres, existing chart-generated POSTGRES_ADMIN_PASSWORD)
and the Synapse media_store PVC (read-only mount)
- authentik: authentik-backup dumps the authentik DB (existing
authentik-credentials pg-password)
Custom image (rohana.axion1337.de/sorb/axion-backup:v1, postgres:17-alpine
+ borgbackup + openssh-client - matches the live Postgres major version
exactly, unlike Alpine's stock postgresql16-client) pushed as a public
package, same pattern as the existing threadnet-web image (no
imagePullSecret needed).
SSH host key pinned via known_hosts ConfigMap (captured via ssh-keyscan
ahead of time) rather than trusting on first connect in an unattended job.
Retention: 7 daily / 4 weekly / 6 monthly via borg prune. Schedule: 03:00
and 03:15, offset to avoid resource contention.
NetworkPolicy: added each backup job's pod as an allowed source to its
namespace's existing postgres ingress rule (matrix's allow-ingress-postgres,
authentik's allow-ingress-authentik-postgresql from #37). Egress already
unrestricted in both namespaces, so no change needed for the outbound SSH
connection to the Storage Box.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes issue #37. The Bitnami postgresql subchart's auto-generated
NetworkPolicy restricted the port (5432) but not the source - any pod in
any namespace could reach it, since additive NetworkPolicy rules can't
restrict an existing permissive one, only the chart itself controls it
(hence not touched during the original #10 rollout).
Disabled via postgresql.primary.networkPolicy.enabled: false and replaced
with a scoped policy allowing only authentik-server and authentik-worker
(same namespace) on 5432, matching the pattern already used for the
matrix namespace's postgres access. No metrics exporter is enabled for
this instance, so no monitoring-namespace rule is needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes issue #36. The OAuth2 Provider ("Matrix Authentication Service")
and its Application ("matrix" slug) linking Authentik to MAS were
originally clicked together by hand in the UI and existed nowhere as
code - unlike the flow fixes already captured in authentik-blueprints.yaml.
Losing the Authentik DB would have meant re-creating this from scratch,
including a fresh client_secret that MAS would no longer match.
The client_secret is read via !Env from AUTHENTIK_MAS_OIDC_CLIENT_SECRET,
sourced from a new key in the existing SOPS-encrypted authentik-credentials
Secret, rather than inlined into the blueprint ConfigMap (which isn't
itself encrypted). Value used is the actual live secret already in use,
read directly from the running Authentik DB - not a new/rotated one, so
this changes nothing about the current MAS<->Authentik pairing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Almost every tracked file in the repo had drifted to mode 777 on disk
(only files created fresh this session were unaffected), and a chunk of
that drift had already been committed as spurious +x bits on plain
YAML/Markdown files (authentik.yaml, kustomization.yaml, coturn.yaml,
element-server-suite.yaml, TASKS.md, install.md, etc.) - none of these
need to be executable. Restored to 644 for regular files, 755 only for
actual scripts (postCreateCommand.sh, docker-init.sh, install-hooks.sh,
pre-commit hook, element-setup-linux.sh).
Also found element-setup-macos.command was missing +x despite having a
shebang and being meant for double-click execution on macOS - fixed.
Added .gitignore for .DS_Store and .claude/ and stopped tracking the five
.DS_Store files that had been committed by accident.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
H.264 uses classic simulcast (like VP8), not SVC, so it's compatible
with the fork's simulcast-shaped publish options without needing a
code fix - unlike VP9/AV1 (see the 2026-07-28 incident notes). Also
tends to be hardware-accelerated on more devices, notably iOS/Safari.
Server-side: LiveKit has no codec restriction configured, supports
H.264 by default.
User reported visible artifacts/blockiness during calls. Root cause:
without an explicit simulcast_layers config, the fork defaulted to
only [180p, 360p] fallback layers below the 1440p top layer - any
minor network hiccup caused a hard cliff straight to blocky 360p
instead of a graceful step-down. Added a 720p middle rung.
VP9 retry deferred: LiveKit uses SVC for vp9/av1 instead of classic
simulcast, and the fork's buildPublishOptions() always builds
simulcast-shaped layers regardless of codec - likely the real reason
forcing vp9 broke calls entirely. Needs a code fix before retrying.
Chart default of 20Mi request+limit OOM-killed the service today after
~74 days of uptime (3rd OOM over its lifetime), causing intermittent
call setup failures unrelated to the NetworkPolicy work happening in
parallel. 20Mi is razor-thin for a long-running Go service; bumping to
a still-modest 64Mi/128Mi request/limit.
Second port-config incident from this rollout: synapse calls MAS's
oauth2/introspect endpoint on port 8080 (not 8081 as assumed) for
every single authenticated request (sync, pushrules, capabilities,
etc). The rule only allowed kube-system (Traefik) on 8080, so every
authenticated API call failed with 503 "Unable to introspect the
access token" - all clients showed a lost connection. Patched live
immediately, this commit brings Git back in sync with that patch.
Ingress-only default-deny plus per-component allow rules, using named
container ports throughout (not Service ports) after the authentik
port-mismatch incident. Traefik -> element-web/element-admin/haproxy
(fronts both matrix.axion1337.chat and well-known)/MAS; synapse <-
haproxy+MAS; postgres <- synapse+MAS; RTC signalling via Traefik, RTC
media NodePorts left open to the internet by design; monitoring scrape
allows for synapse/postgres/rtc-sfu metrics; ACME solver allow. coturn
(hostNetwork) needs nothing - NetworkPolicy doesn't apply to it. Part
of issue #10.
Caused a live 502 immediately after deploy: the rule allowed 80/443
(the Service's external ports), but NetworkPolicy filters on the pod's
actual container port after kube-proxy's DNAT - authentik-server's
Service maps 80->9000 and 443->9443. Confirmed root cause by suspending
Flux reconciliation (it was silently re-applying my manual test
deletions) and testing with the policies truly absent.
Ingress-only default-deny (egress untouched) plus explicit allows:
Traefik (kube-system) + MAS (matrix ns) -> authentik-server on 80/443,
and Traefik -> cert-manager's ACME HTTP-01 solver pods on 8089.
authentik-postgresql already has its own Bitnami-chart-managed policy,
left alone. Part of issue #10.
v0.2.0 forced video_codec: vp9, which broke calls entirely (no audio/
video transmitted, despite server logs showing the codec regression
fallback to VP8 working). Root cause not fully confirmed. This build
keeps the 1440p/60fps/bitrate defaults but leaves video_codec unset
(defaults to vp8) to isolate whether the codec preference itself was
the trigger.
User reports calls transmit neither video nor audio with
v0.2.0-elementcall-mediaquality. Rolling back to known-good v0.1.0
while investigating; likely the video_codec: vp9 preference, not the
resolution/framerate bump.
Bumps elementWeb image to a build with the embedded Element Call
widget replaced by our own fork (rohana.axion1337.de/sorb/threadnet-call,
based on emmick4/element-call:livekit / upstream PR #3736), raising
default call quality to up to 1440p/60fps camera (VP9) and 1440p/30fps
screen share. These are seeded defaults users can still adjust in
Settings, not hard-enforced caps.
coturn started up cleanly every time (all TCP/UDP listeners on 3478/5349
opened fine per logs) but was killed every ~10s because the liveness
probe ran "netstat -uln", which doesn't exist in coturn/coturn:latest
(sh: netstat: not found) - 36000+ restarts over 88 days for a healthy
process. Switched to a tcpSocket probe against the TCP listener coturn
already opens on 3478, no in-container tooling required.
Neither Element Web's config.json nor MAS's branding config expose a
generic custom-link mechanism, and /if/user/ is blocked for type=external
Matrix accounts, so the working Authentik MFA setup flows had nowhere to
be discoverable. Added a docs/setup/security.html page, following the
exact existing pattern (ConfigMap -> initContainer cp -> nginx), linked
from index.html's Support section.
Several dead ends (TOTP/WebAuthn setup after an anonymous visit bounces
through login and lands on "/") trace back to Brand.default_application
being unset, which falls back to /if/user/ - blocked for type=external
Matrix accounts. Set it to the matrix Application. Only changes the bare
"/" fallback; explicit URLs like /if/admin/ are unaffected.