IT Support Specialist
- Support im 1st und 2nd Level für Windows-Clients und Microsoft 365
- Fragen zu Konten, Zugriffen und Lizenzen in Microsoft 365
- Lösungen und wiederkehrende Störungen in der Wissensdatenbank dokumentiert

Ich bin 1996 in Maroua, Kamerun, geboren und in Douala aufgewachsen. 2013 bin ich zum Studium nach Deutschland gekommen. Zuerst kamen drei Jahre Deutsch, eins in Kamerun und zwei hier, dann das Studium.
Neben dem Studium habe ich drei Jahre bei AOE in der IT gearbeitet: Konten und Rechte in Entra ID, PowerShell-Skripte, Jenkins. Dort ist auch meine Bachelorarbeit entstanden. Seba baue und betreibe ich allein, vom Server bis zur App.
Wer mich kennt, beschreibt mich als ruhig, strukturiert und hilfsbereit. Ich höre lieber zu, als viel zu reden, und ich halte Ordnung, in Systemen wie im Code.
Bachelorarbeit bei AOE: ein IT-Support-Assistent, der lokal aus Helpdesk-Tickets antwortet.
Grundlagen der Informatik, danach der Wechsel an die Hochschule RheinMain.
Ein Jahr in Kamerun, zwei in Deutschland, dann der Zugang zum Studium.
Heller geschrieben ist, was ich täglich nutze. Gedämpft steht, womit ich gearbeitet habe, aber nicht jeden Tag.
Jede Schule hat ihre eigene Adresse und ihre eigenen Daten. Seba deckt Einschreibung, Anwesenheit, Noten, Zeugnisse, Gebühren und Nachrichten ab, auf Französisch und Englisch, weil in Kamerun beide Schulsysteme nebeneinander laufen.
Ich habe das Backend, die Web-App und die Eltern-App geschrieben und betreibe den Server bei Hetzner selbst. Der Code ist nicht öffentlich. Deshalb zeige ich hier, was im System passiert, wenn eine Schule ein Kind einschreibt, und danach, wie der Server abgesichert ist.
Eine Familie steht im Sekretariat. Die Sekretärin trägt Name, Geburtsdatum, Stufe, Klasse, mindestens einen Elternteil und einen Notfallkontakt ein und klickt auf Speichern. Ihr Browser schickt POST /api/students.
Schüler anlegen darf nur die Schulleitung oder eine Verwaltungskraft, der die Schulleitung dieses Recht gegeben hat. Lehrkräfte und Eltern können es nicht.
nginx nimmt die Anfrage an und begrenzt die Rate, helmet und CORS lassen nur die eigenen Origins durch. Danach liest Seba aus dem Host-Header, um welche Schule es geht, und legt sie für den Rest der Anfrage fest.
Das Token der Sekretärin muss zu genau dieser Schule gehören. Mit dem Token einer anderen Schule antwortet der Server mit 401.
Die ValidationPipe entfernt jedes Feld, das nicht im DTO steht. Eine Matrikelnummer kann der Browser deshalb gar nicht mitschicken, die vergibt der Server.
Die Berechtigung prüft der Use Case selbst. GrantLoader lädt die Rechte der Sekretärin und verlangt CREATE:STUDENT. Danach prüft er, ob die Klasse zur Stufe gehört, die Stufe zum Zyklus passt und die Schule ein Schuljahr geöffnet hat.
Jedes Kind bekommt eine eigene Matrikelnummer, zum Beispiel MAT-2026-0042. Eine Sperre auf der Zählerzeile sorgt dafür, dass zwei gleichzeitige Einschreibungen nicht dieselbe Nummer bekommen.
In derselben Transaktion schreibt Seba den Schüler und seine Einschreibung für das Jahr. Scheitert ein Schritt, wird alles zurückgerollt, und es bleibt keine halbe Akte liegen.
Nach dem Commit legt Seba das Gebührenblatt an und schreibt einen Eintrag ins Audit-Log. Hat die Schule die Gebühren dieser Stufe noch nicht eingetragen, scheitert nur das Gebührenblatt. Die Einschreibung bleibt gültig, denn eine fehlende Einstellung soll keine Familie wieder nach Hause schicken.
Die Sekretärin druckt den Eltern einen Zettel mit der Matrikelnummer. Bei der Registrierung in der App geben die Eltern die Nummern ihrer Kinder ein. Konto und Verknüpfung entstehen in einer Transaktion, und ist eine Nummer unbekannt oder schon vergeben, legt Seba gar kein Konto an.
Jede Stufe darf ablehnen, und die erste, die es tut, beantwortet die Anfrage. Fahr über eine Stufe oder tippe sie an.
Seba läuft auf einem VPS bei Hetzner: Produktion, eine Demo für Interessenten und die Marketing-Seite sebamun.com. Jede Maßnahme ist als Skript im Repository versioniert.
Von oben nach unten: was zwischen dem Internet und den Daten der Schulen steht.
Auf der Maschine laufen Produktion, Demo und Marketing-Seite als eigene Node-Prozesse, dazu PostgreSQL und Redis. Der Schutz ist in Schichten aufgebaut: Netz, Eingang, Rechte, Daten, Überwachung. Jede Schicht wirkt unabhängig von den anderen.
ufw lehnt standardmäßig jede eingehende Verbindung ab und erlaubt nur SSH, HTTP und HTTPS. SSH akzeptiert nur Schlüssel, keinen Login als root und höchstens drei Versuche. fail2ban sperrt Adressen nach wiederholten Fehlversuchen.
Die SSH-Härtung liegt als eigene Datei in sshd_config.d und wird mit sshd -T gegen die geladene Konfiguration geprüft.
nginx übernimmt TLS, setzt HSTS, CSP und die übrigen Sicherheits-Header und begrenzt die Rate: 30 Anfragen pro Sekunde auf /api/, 2 auf /auth/. Dahinter zählt die App selbst noch einmal, in Redis.
Die drei Node-Prozesse hören nur auf 127.0.0.1. Die Demo ist zusätzlich durch Basic-Auth geschützt.
Drei Linux-Konten mit getrennten Aufgaben. Das erste hat volle Rechte und administriert den Server, führt aber keine Anwendung aus. Das zweite führt Produktion und Demo aus, ohne sudo und ohne den eigenen Code ändern zu können. Das dritte führt nur die Marketing-Seite aus und kann die Secrets der anderen nicht lesen.
Dasselbe Prinzip in der Datenbank: Die Rolle der Marketing-Seite darf das Schulverzeichnis lesen und eine Anfrage eintragen, sonst nichts.
Die .env-Dateien gehören root, die App darf sie nur lesen, und sie liegen nicht im Git. Secrets werden per Skript gewechselt, Redis verlangt ein Passwort.
Jede Nacht: pg_dump, Verschlüsselung mit age, Upload per rclone nach Backblaze B2. Der Server kennt nur den öffentlichen Schlüssel und kann seine Backups weder lesen noch löschen. Die Wiederherstellung ist dokumentiert und getestet.
healthping prüft alle fünf Minuten die drei Anwendungen, den Plattenplatz und die Zertifikate und meldet sich nur, wenn alles passt. Bleibt die Meldung aus, kommt der Alarm von außen. Logs und Metriken laufen über Grafana, Loki und Prometheus, alles auf 127.0.0.1.
Sicherheitsupdates spielt unattended-upgrades ein, in der CI blockiert yarn npm audit Abhängigkeiten ab Schweregrad high. Die Ops-Skripte sind wiederholbar, laufen ohne --apply nur zur Probe, sichern vor dem Schreiben und haben einen Rollback.
Jede Schicht arbeitet für sich, fällt eine aus, hält die nächste. Fahr über eine Schicht oder tippe sie an.
Der First-Level-Support bei AOE beantwortet dieselben Fragen immer wieder. Für meine Bachelorarbeit habe ich einen Assistenten gebaut, der aus rund 13.000 Deskpro-Tickets und 54 internen Anleitungen antwortet. Er läuft vollständig lokal, kein Ticket verlässt das Unternehmen.
RAG mit lokalen Open-Source-Modellen war eine bewusste Wahl: Die Tickets ändern sich jeden Tag, und jede Antwort soll sich auf ihre Quellen zurückführen lassen. Die ganze Pipeline habe ich selbst gebaut, von der Datenabfrage bis zum Chat-Widget.
Ich habe die Tickets aus fünf Jahren mit allen Nachrichten über die Deskpro-API in MongoDB geladen, ein Dokument pro Ticket. Vor dem Einbetten entferne ich HTML, Telefonnummern, URLs und E-Mail-Adressen. Die 54 Anleitungen teile ich in Stücke von 256 Tokens.
Die Tickets mischen Deutsch und Englisch, deshalb bettet das mehrsprachige Modell BAAI/bge-m3 alles ein. Tickets und Anleitungen liegen in zwei eigenen ChromaDB-Collections, daneben steht ein BM25-Index für exakte Begriffe.
Llama 3.2 mit 3B Parametern läuft lokal über Ollama und formuliert die Frage dreimal um. Für die Beispielfrage:
Jede Variante läuft durch BM25 und die Vektorsuche, auf Tickets und Anleitungen, mit je 5 Treffern. Ich normalisiere die Scores per Min-Max, verrechne sie nach Rang mit 1/(Rang+k) und behalte pro Quelle die besten 3.
Der Prompt erlaubt nur, was in den Treffern steht, Wissen von außen ist verboten. Das Modell schreibt eine Antwort aus den Tickets und eine aus den Anleitungen. Der Router nimmt die Antwort, deren Treffer im Schnitt höher bewertet wurden.
Bei dieser Frage gewannen die Anleitungen, 1,13 gegen 0,73 für die Tickets. Die Antwort folgt der Anleitung zur Zoom-Lizenz: im Admin-Bereich unter Benutzer prüfen, ob eine Lizenz frei ist, und dann die E-Mail-Adresse des Mitarbeiters eintragen. Ist keine mehr frei, erst prüfen, ob alte Konten entfernt werden können, bevor neue Lizenzen bestellt werden.
Im React-Chat kann man jede Antwort mit Daumen hoch oder runter bewerten, das Feedback landet in MongoDB.
Gemessen habe ich Latenz und Antwortverhalten von vier lokalen Modellen: Llama 3.2 3B, Mistral 7B, Gemma2 9B und DeepSeek-R1 7B. Arize Phoenix hat über OpenTelemetry jeden Schritt aufgezeichnet, so sah ich die Zeit pro Schritt. Llama 3.2 war in jedem Schritt am schnellsten.
Beim erneuten Lesen des Codes habe ich einen Fehler gefunden, der einen Teil der Suchergebnisse verlor, bevor sie fusioniert wurden. Ohne festen Testsatz ist er mir damals nicht aufgefallen. Heute würde ich deshalb zuerst einen festen Testsatz aus Tickets von 2024 bauen und daran Recall@k der Suche und die Quellentreue der Antworten messen. Dazu kämen ein Reranker, Quellenangaben in jeder Antwort und eine gründlichere Entfernung personenbezogener Daten.
Oben die Daten, die ich vorab aufbereitet habe, darunter der Weg einer Frage. Fahr über eine Stufe oder tippe sie an.
Eine Frage oder eine Stelle? Schreib mir eine E-Mail oder ruf direkt an. Ich melde mich zeitnah.