Historia pewnego Poloneza
Nasz przyjaciel miał Poloneza. W czasach PRL to był samochód, na którym w zasadzie każdemu zależało – a fabrycznych zabezpieczeń, poza kluczykiem i odrobiną nadziei, nie było żadnych. Kradzieże aut były wtedy na porządku dziennym, a odzyskanie skradzionego wozu graniczyło z cudem. Więc miał swoją rutynę: ukryty w kokpicie przycisk, który trzeba było przytrzymać w trakcie odpalania silnika. Nic więcej. Żadnego blokującego pisk, żadnej kontrolki na desce rozdzielczej, żadnego widocznego sygnału, że coś w ogóle jest zabezpieczone. Ktoś, kto próbowałby odpalić ten samochód bez znajomości tej rutyny, po prostu by go nie odpalił – a nie miałby zielonego pojęcia dlaczego.
I to działało. Samochód nigdy nie zniknął. Za to pewnego ranka stał na cegłach – bez kół. Ukryty przycisk chronił auto jako całość, bo złodziej nie potrafił go uruchomić. Nie chronił niczego, do czego wystarczy klucz nasadowy i piętnaście minut bez świadków.

Co z tego wynika
Ta historia siedziała nam w głowie, kiedy zaczęliśmy myśleć o behavioral-auth. Nie interesowała nas kolejna blokada oparta o coś, co można ukraść, podejrzeć albo wyłudzić — hasło, token, odcisk palca skopiowany ze szklanki. Interesowała nas rutyna. Coś, czego się nie da podejrzeć przez ramię, bo nawet właściciel nie wie, że to robi w ten konkretny sposób: rytm uderzeń w klawisze, odstępy między nimi, to jak dłoń prowadzi kursor, jak często ktoś się poprawia backspace’em.
Nowoczesny odpowiednik ukrytego przycisku pod deską rozdzielczą to nie jeden sekret. To suma tysięcy drobnych, w większości nieuświadomionych nawyków — i to jest coś, czego intruz nie odtworzy, nawet mając hasło. Zna klucz do stacyjki. Nie zna rutyny.
Ale – i to jest ta sama lekcja co z kołami na cegłach — trzeba być uczciwym co do zakresu ochrony. Ten system nie jest zamkiem. Jest czujką. Dalej w tym dokumencie wracamy do tego rozróżnienia, bo ono determinuje każdą decyzję projektową, jaką podjęliśmy.
Scenariusz: ktoś inny siada do klawiatury
Intruz nie musi niczego łamać. Ma hasło – wyłudzone phishingiem, podejrzane przez ramię, złapane keyloggerem na innej maszynie, nieważne jak. Trafia na stanowisko, którego akurat nikt nie pilnuje: przerwa na kawę, koniec zmiany, zapomniane zablokowanie ekranu, może zdalna sesja przejęta razem z poświadczeniami VPN. Loguje się. System operacyjny go wpuszcza – hasło jest prawidłowe, więc z jego punktu widzenia nic złego się nie dzieje.
Zaczyna działać. I tu robi się ciekawie, bo behavioral-auth w tym momencie nie wie nic o tym, co robi – nie widzi treści, nie widzi aplikacji, nie widzi plików. Widzi tylko jak to robi. A robi to inaczej: inny rytm pisania, inne odstępy między klawiszami, więcej poprawek backspace’em, bo nie zna układu ani skrótów tej konkretnej osoby, kursor porusza się mniej płynnie, bo szuka po ekranie zamiast po pamięci mięśniowej. Żadna z tych różnic osobno nic nie znaczy — ludzie mają gorsze i lepsze dni. Ale zsumowane w kolejnych 20-sekundowych oknach i 80-sekundowych sekwencjach błąd rekonstrukcji zaczyna konsekwentnie przekraczać wyuczony próg.
Demon nie reaguje na pierwszą taką sekwencję to byłaby paranoja, nie detekcja. Wymaga serii anomalii i rozpiętości czasowej tej serii (żeby odróżnić realną, ciągłą zmianę od chwilowego zakłócenia sąsiednie sekwencje mocno się nakładają, więc kilka nietypowych ticków z rzędu to wciąż może być kilka sekund realnie nowych danych). Dopiero kiedy niedopasowanie utrzymuje się przez dłuższą chwilę, stan przechodzi z NADZÓR w ALARM.
Co się dzieje dalej, zależy od tego, gdzie to stanowisko żyje.
Lokalnie, zawsze: wpis do logu, komunikat w konsoli (jeśli ktoś na nią patrzy), opcjonalne powiadomienie systemowe. To wszystko. Żadnej blokady, żadnego wylogowania. Co ciekawe, to lokalne powiadomienie jest bronią obosieczną, jeśli intruz akurat siedzi fizycznie przy tym ekranie, widzi, że został zauważony. Może się wystraszyć i uciec. Może też przyspieszyć i zgarnąć, co miał zgarnąć, zanim ktoś zareaguje. Świadomie o tym mówimy, bo to jest realny kompromis tego podejścia, nie przeoczenie.
W firmie, jeśli forwarding do SIEM jest włączony: to samo zdarzenie werdykt, współczynnik odchylenia, jak długo trwa, host, identyfikator sesji trafia równolegle do Wazuh, bez wiedzy osoby siedzącej przy klawiaturze. Operator SOC widzi na desce: stanowisko WS-114, sesja użytkownika mkowalski, alarm behawioralny, odchylenie 3.1x progu, trwa od 4 minut. Nie widzi ani jednego klawisza, który został wciśnięty — tylko werdykt i liczbę. Koreluje to z innym sygnałem (dostęp kartą, geolokalizacja VPN, zgłoszenie w systemie), dzwoni albo pisze do prawdziwego Marka, żeby potwierdzić, czy to na pewno on. Jeśli nie dopiero teraz wchodzi do gry blokowanie: wymuszone wylogowanie przez AD, odcięcie sesji VPN, izolacja stacji przez EDR.
To wszystko robi zespół bezpieczeństwa swoimi narzędziami. behavioral-auth nie bierze w tym udziału poza jednym: zadzwonił dzwonkiem wcześniej, niż zrobiłby to ktokolwiek inny.
I odwrotnie jeśli to jednak był naprawdę Marek, tylko zmęczony po nocnym locie i piszący dziwnie przez pół godziny, alarm sam wygaśnie, kiedy jego zachowanie znowu zacznie pasować do wzorca przez wystarczająco długo. Nikt nie musiał interweniować, nikogo nie zablokowano, koszt fałszywego alarmu to telefon od SOC, a nie zablokowany dostęp w środku ważnego dnia pracy. To jest dokładnie ta właściwość, dla której demon nigdy sam nic nie blokuje bo przy systemie, który patrzy tylko na jak, a nie na co, fałszywe alarmy są nieuniknione, i konsekwencje pomyłki muszą być tanie.
Jak to ma działać na co dzień
Faza NAUKA
Po instalacji demon sam tworzy sobie bazę i zaczyna słuchać klawiatury i myszy przez evdev, po cichu robiąc też zdjęcia twarzy w tle (jeśli moduł twarzy jest włączony). To nie jest rejestracja odcisku palca w dziesięć sekund to obserwacja rutyny rozłożona na kilka dni realnej pracy, bo wzorzec musi obejmować różne pory dnia, nie jeden poranny zryw. Co jakiś czas, gdy uzbiera się dość nowego materiału, demon trenuje kandydata na model i ocenia go na świeżych, nigdy niewidzianych danych.

Co dokładnie jest zbierane i jak (dla programistów)
Skoro dokument trafia też do administratorów i programistów, ten fragment celowo schodzi poziom niżej bez tego reszta brzmi jak marketing.
Źródło. Kolektor czyta urządzenia bezpośrednio z evdev (interfejs zdarzeń wejściowych jądra Linuksa), po jednym asynchronicznym zadaniu na urządzenie. Klasyfikacja klawiatura/mysz jest automatyczna, po capabilities() urządzenia klawiatura to coś z kodami typu KEY_A, KEY_ENTER, KEY_BACKSPACE w zestawie EV_KEY; mysz to coś, co generuje EV_REL. Można też wskazać collector.devices ręcznie, jeśli autodetekcja złapie coś niechcianego (np. pilota).
Co trafia do bazy, event po evencie. Krotka (ts_ns, ts_utc, session_id, dev_path, dev_name, dev_type, ev_type, ev_code, ev_value) w tabeli raw_events. Istotny szczegół: ts_ns pochodzi z timestampu jądra (ev.sec/ev.usec), nie z zegara odczytu w Pythonie — zacięcie GIL-a w wątku roboczym nie potrafi więc rozjechać pojedynczej cechy czasowej. ev_code to surowy kod skankodu (np. 14 = backspace wg linux/input-event-codes.h), nie znak w jakimkolwiek layoucie — system nie wie i nie może wiedzieć, że ktoś nacisnął literę „a”, tylko że nacisnął klawisz o danym kodzie. Zapis jest buforowany w pamięci (Writer, domyślnie 200 zdarzeń) i zrzucany do DuckDB przy przepełnieniu bufora albo na każdym ticku demona (5 s) bez osobnego wątku i locka, bo add()/flush() żyją na tej samej pętli asyncio.
Ekstrakcja cech, dwuetapowo. Najpierw feature_windows: stałe okna 20-sekundowe z krokiem 5 s, zakotwiczone w czasie pierwszego zdarzenia sesji — dzięki temu granice okien są zawsze te same, niezależnie od tego, w którym momencie akurat odpali się tick. Okno o zbyt małej aktywności (mniej niż 12 zdarzeń klawiatury i mniej niż 10 zdarzeń myszy) jest pomijane w całości — cisza nic nie mówi o tym, kto siedzi przy komputerze, a wliczanie jej tylko rozmywałoby sygnał. Potem fused_sequences: 12 kolejnych okien sklejonych w jedną sekwencję (rozpiętość 12 × 5 s + 20 s = 80 s), też z krokiem 5 s — więc sąsiednie sekwencje nakładają się na 11 z 12 okien. To właśnie ten fakt stoi za wymogiem serii i rozpiętości czasowej przy wchodzeniu w alarm, o którym była mowa wyżej.
Z każdego okna wychodzi 21 cech, w trzech grupach:
| Grupa | Cechy | Co mierzą |
|---|---|---|
| klawiatura (8) | f_ks_count, f_ks_mean_dwell, f_ks_std_dwell, f_ks_mean_flight, f_ks_std_flight, f_ks_backspace_ratio, f_ks_repeat_ratio, f_ks_entropy | czas trzymania klawisza (dwell, różnica ev_value 1→0), odstęp między kolejnymi naciśnięciami (flight), udział backspace’a, udział auto-repeatu, entropia Shannona histogramu czasów dwell (8 binów) |
| mysz (9) | f_ms_count, f_ms_speed_mean/std, f_ms_acc_mean, f_ms_clicks, f_ms_click_dwell, f_ms_scrolls, f_ms_idle_ratio, f_ms_curvature | prędkość kursora (przeciwprostokątna dx,dy / dt), przyspieszenie, pary down/up dla BTN_LEFT, czas trzymania kliknięcia, zdarzenia kółka, frakcja próbek poniżej 2 px/s, średnia bezwzględna zmiana kąta ruchu (krzywizna toru) |
| kontekst (4) | f_ctx_hour_sin/cos, f_ctx_is_weekend, f_activity_density | cykliczne kodowanie pory dnia, dzień tygodnia, gęstość zdarzeń w oknie |
Ważny, łatwy do przeoczenia szczegół: te trzy cechy kontekstowe (hour_sin, hour_cos, is_weekend) są zapisywane do bazy dla celów raportowych, ale świadomie nie trafiają do modelu (EXCLUDED_FROM_MODEL w config.py). Nie mówią nic o tym, kto pisze — intruz o 10:00 we wtorek wygląda w nich identycznie jak właściciel a jeśli enrolment wypadnie w całości w dni robocze, is_weekend jest przez cały czas stały, co bez dodatkowego zabezpieczenia rozjeżdża skalowanie (patrz niżej). Model dostaje więc 18 z 21 zapisanych kolumn (MODEL_COLUMNS).
Inkrementalność i deduplikacja. Obie funkcje budujące (build_feature_windows, build_sequences) trzymają znak wodny — najnowszy już zapisany window_start_ns/seq_end_ns dla danej sesji i za każdym tickiem liczą tylko to, co nowe. Bez tego każdy tick przeliczałby całą sesję od zera, co przy tickowaniu co 5 s szybko stałoby się kwadratowe. Dodatkowo unikalny indeks na (session_id, dedup_key), gdzie dedup_key = seq_end_ns // dedup_gap_ns, i INSERT OR IGNORE chronią przed zdublowaniem rekordów, gdy demon się zrestartuje w środku przetwarzania.
Ochrona przed sklejaniem nieciągłych okien. Ponieważ okna o zbyt niskiej aktywności są całkowicie pomijane (nie ma wypełniania zerami), zapisane okna nie są czasowo ciągłe. Sekwencja, w której odstęp między dwoma sąsiednimi oknami przekracza max_seq_gap_sec (40 s), jest odrzucana inaczej dałoby się skleić w jedną próbkę poniedziałkowy poranek z wtorkowym wieczorem.
Skalowanie. Standaryzacja z-score (średnia/odchylenie) liczona jest wyłącznie na splicie treningowym — nigdy na holdoucie ani na danych ocenianych na żywo i zapisywana do scaler.json. Ma podłogę (std_floor, domyślnie 0.001): cecha stała w czasie enrolmentu miałaby odchylenie bliskie zeru, a dzielenie przez nie pomnożyłoby pierwsze realne odchylenie przez rząd 10⁶ i wysłało błąd rekonstrukcji w kosmos co wyglądałoby jak intruz, a byłoby artefaktem arytmetyki.
Kształt wejścia modelu. Sekwencja (n, seq_len=12, 18) jest transponowana do (n, 18, 12) — kanały (cechy) na czas — bo to jest format, jakiego oczekuje Conv1D.
Do testów, nie do produkcji: SyntheticSource emituje zdarzenia w dokładnie takim samym kształcie jak evdev, na symulowanym, przyspieszonym zegarze, z dwoma profilami motorycznymi (user/impostor — różne rozkłady dwell/flight/prędkości myszy). Cały pipeline cech liczy wszystko z timestampów zdarzeń, nigdy z zegara ściennego, więc nie odróżnia symulacji od realnego wejścia — dlatego jest to w ogóle użyteczne do testów. I dlatego właśnie jest to twardo zablokowane w general.mode: prod: nic nie może wstrzyknąć fałszywego zachowania do wzorca, który ma identyfikować prawdziwą osobę.
Bramka rozsądku
Zanim jakikolwiek wzorzec zostanie zamrożony, musi przejść test, który nie jest formalnością: model musi wykazać, że realnie odróżnia użytkownika od zniekształconych wariantów jego własnych danych (szybsze pisanie, wolniejsze pisanie, gwałtowniejsza mysz, przetasowana kolejność). Model, który tego nie potrafi, nauczył się tylko kopiować to, co widzi — miałby piękny, stabilny błąd dla każdego człowieka na Ziemi i nie wykryłby nikogo. Taki model nie dostaje promocji, niezależnie od tego, jak dobrze wygląda na papierze.
Zamrożenie i NADZÓR
Kiedy wzorzec przechodzi bramkę, zostaje zamrożony i to jest świadoma decyzja, nie zaległość do dorobienia. Nic nie douczą się samo. Jedyne, co go zmienia, to jawna akcja człowieka (reset, kiedy stanowisko zmienia właściciela, albo learn-more, kiedy chcemy go doszlifować). Dzięki temu obcy człowiek nie może stopniowo nauczyć systemu, że to on jest właścicielem, po prostu z niego korzystając.
Od tego momentu każda sesja jest oceniana na bieżąco względem tego wzorca i wszystko dzieje się lokalnie. Baza, model, obliczenia, cała logika decyzyjna siedzą na tej samej maszynie, którą obserwują. Żadne surowe zachowanie kody klawiszy, współrzędne myszy, klatki z kamery nigdzie nie wychodzi, nawet jeśli forwarding do SIEM jest włączony. To jest fundament całego produktu, nie szczegół implementacyjny: nadzór behawioralny, który nie jest inwigilacją wysyłaną gdzieś w chmurę, tylko lokalną czujką.
Kanał twarzy jako drugi, niezależny sygnał
Kamera dokłada drugi, całkiem osobny sygnał nie uśredniony z pierwszym, tylko połączony logicznym LUB. Nieznana twarz (ciemny pokój, kamera przysłonięta, nikt nie siedzi na wprost obiektywu) to brak dowodu, nigdy dowód intruza bo mieszanie tych dwóch przypadków dawałoby fałszywe alarmy za każdym razem, gdy ktoś odwróci się od monitora.
W firmie: SIEM jako druga para oczu
Forwarding do SIEM jest domyślnie wyłączony i pozostaje świadomą decyzją, nie ustawieniem fabrycznym. Kiedy jest włączony, na zewnątrz trafiają wyłącznie zdarzenia podniesienie i wygaśnięcie alarmu, zmiany stanu, operacje typu reset czy pauza z werdyktem i liczbą, nigdy z surowym zachowaniem, z którego ta liczba powstała. Reset wzorca też jest zdarzeniem bezpieczeństwa i też trafia do SIEM, bo to jedyna operacja, która sprawia, że system „zapomina”, kto jest właścicielem administrator ma prawo o tym wiedzieć, niezależnie od tego, czy zrobił to sam właściciel stacji, czy ktoś inny.

Co dokładnie leci po drucie. Jedno zdarzenie to jedna linia syslog w formacie RFC 5424, z JSON-em schowanym w polu wiadomości zamiast prozy po to, żeby dekoder po stronie Wazuh mógł czytać pola, a nie parsować zdanie. Przykład linii, jaką faktycznie wysyła demon przy podniesieniu alarmu:
<81>1 2026-07-14T10:22:31.123456+00:00 ws-114 behavioral-auth - alarm.raised - {"action":"raised","category":"alarm","detail":{"alarm_id":"7e2c1b4a-9e3d-4b1a-8f2e-6c1d9a0b3e5f","face_state":"unknown","ratio":3.12,"reason":"behavioral","span_sec":142.0,"summary":"behaviour deviates from the learned pattern for 142s (ratio up to 3.12x threshold)"}, "enrollment_id":"3f9a1c2b-4d5e-4f6a-8b9c-0d1e2f3a4b5c", "host":"ws-114","session_id":"9e21f3aa-1b2c-4d5e-6f7a-8b9c0d1e2f3a","severity":1,"ts":"2026-07-14T10:22:31.123456+00:00"}
<81> to facility*8 + severity (authpriv=10, ALERT=1), alarm.raised w polu MSGID to category.action — najprostszy uchwyt do reguł po stronie managera. behavioral-auth w polu APP-NAME to siem.ident z configu.
Wariant A — stacja ma już agenta Wazuh (sink: syslog, zalecany). Po stronie stacji nic specjalnego dla samego behavioral-auth poza siem.enabled: true i domyślnym siem.sink: syslog — demon po prostu pisze na lokalny socket /dev/log. Reszta to zwykła instalacja agenta:
- Zainstaluj
wazuh-agent(repozytorium Wazuh, pakiet.deb/.rpm) i zarejestruj go na managerze, np.WAZUH_MANAGER="<adres-managera>" apt-get install wazuh-agent, potemsystemctl daemon-reload && systemctl enable --now wazuh-agent. - Sprawdź, gdzie lokalny rsyslog/journald faktycznie odkłada facility
authpriv— zwykle/var/log/auth.logna Debianie/Ubuntu,/var/log/securena RHEL/Fedorze. To zależy od konfiguracji dystrybucji, nie odbehavioral-auth, więc warto to zweryfikować zamiast zakładać. - Dopisz w
ossec.confagenta stanzę wskazującą na ten plik:
<localfile>
<log_format>syslog</log_format>
<location>/var/log/auth.log</location>
</localfile>
allowed-ips warto zawęzić do realnej podsieci stacji roboczych, nie zostawiać otwartego na wszystko — to jedyny port, na jaki cokolwiek z tej maszyny w ogóle się dobija. 3. Restart wazuh-manager, otwarcie portu na firewallu między stacją a managerem.
Po stronie managera — jak Wazuh ma w ogóle zrozumieć te zdarzenia (dotyczy obu wariantów). Domyślny ruleset Wazuh nic nie wie o naszym formacie, więc potrzebny jest własny dekoder i własne reguły — trzymane w local_decoder.xml/local_rules.xml, żeby aktualizacja rulesetu ich nie nadpisała:
<!-- /var/ossec/etc/decoders/local_decoder.xml -->
<decoder name="behavioral-auth">
<program_name>^behavioral-auth$</program_name>
</decoder>
<decoder name="behavioral-auth-json">
<parent>behavioral-auth</parent>
<prematch>^\{</prematch>
<plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>
<!-- /var/ossec/etc/rules/local_rules.xml — lokalne id z zakresu 100000+ -->
<group name="behavioral-auth,">
<rule id="100100" level="3">
<decoded_as>behavioral-auth-json</decoded_as>
<description>behavioral-auth: zdarzenie zarejestrowane</description>
</rule>
<rule id="100101" level="12">
<if_sid>100100</if_sid>
<field name="category">alarm</field>
<field name="action">raised</field>
<description>behavioral-auth: ALARM na $(host) — zachowanie odbiega od wzorca</description>
</rule>
<rule id="100102" level="3">
<if_sid>100100</if_sid>
<field name="category">alarm</field>
<field name="action">cleared</field>
<description>behavioral-auth: alarm na $(host) wygasł</description>
</rule>
<rule id="100103" level="10">
<if_sid>100100</if_sid>
<field name="category">ops</field>
<field name="action">pattern_reset</field>
<description>behavioral-auth: reset wzorca na $(host) — system zapomniał właściciela</description>
</rule>
</group>
Przed wdrożeniem na produkcji warto przepuścić przykładową linię (dokładnie taką, jak wyżej) przez wazuh-logtest, żeby zobaczyć, czy dekoder faktycznie łapie pola i która reguła się odpala, zamiast dowiadywać się tego dopiero na żywym alarmie. Po każdej zmianie w local_decoder.xml/local_rules.xml — restart wazuh-manager. Ponieważ format zdarzeń jest jeden, zamknięty i identyczny dla obu sinków, ten dekoder i te reguły pisze się raz, dla całej organizacji, nie per stacja.
Dokładna składnia dekoderów/reguł bywa doprecyzowywana między wersjami Wazuha przed wdrożeniem warto zwyczajowo zweryfikować ją z aktualną dokumentacją Wazuh dla wersji managera, jaka jest w danej firmie.
Co to chroni, a czego nie
Wracając do Poloneza: ukryty przycisk chronił przed kradzieżą całego samochodu, ale nie przed zdjęciem kół. behavioral-auth chroni przed nieautoryzowanym korzystaniem z sesji mimo poprawnego hasła sytuacją, w której zamek zawiódł, bo klucz był prawdziwy. Nie jest fizycznym zabezpieczeniem, nie zastępuje MFA, DLP ani polityki haseł, i sam z siebie nigdy nie blokuje ani nie wylogowuje — to świadomie usunięta z kodu możliwość, nie brakująca funkcja. Nie mierzy też swojej własnej skuteczności liczbami w rodzaju FAR czy FRR, bo system widzi tylko jedną osobę i nie ma danych, do których mógłby taką liczbę odnieść — każda próba policzenia czegoś takiego byłaby ładnie wyglądającą fikcją.
To, co dostajemy w zamian, to cicha, lokalna czujka, która dzwoni wcześniej niż cokolwiek innego w stosie zabezpieczeń — i robi to bez wysyłania gdziekolwiek tego, co ktoś naprawdę wpisał na klawiaturze.
Projekt jest rozwijany przez cdest.eu dostępny w środowisku GitHub programiści dołączający do naszej społeczności zapaszmy do współtworzenia.