← Späť na blog

Bezpečnostný kľúč namiesto hesla pri štarte: cesta k FIDO2 LUKS unlocku (a osem pokusov, kým to skutočne fungovalo)

23. júla 2026 · 6 min čítania

linuxbezpečnosť

Súčasť série o čerstvej migrácii pracovného laptopu na Pop!_OS s LUKS+Btrfs a hardvérovým FIDO2 kľúčom — o tejto jednej časti si to zaslúži vlastný priestor, lebo "enrolluj kľúč a hotovo" bola len prvá desatina práce.

Zadanie, ktoré znie jednoducho

Šifrovaný disk (LUKS), heslo pri každom boote. Cieľ: namiesto písania hesla len priložiť hardvérový bezpečnostný kľúč a dotknúť sa ho. FIDO2 podpora v cryptsetup na to existuje, libfido2 je dávno nainštalovaná, enrollment kľúča do LUKS slotu prebehne bez problémov.

A napriek tomu — po reštarte prišlo len obyčajné heslo. Žiadny FIDO2 pokus, žiadna chyba, nič.

Prvá vrstva: aký typ initrd vôbec máš

Skutočná príčina bola hlbšia, než chýbajúci balík. Tento konkrétny systém bootuje cez initramfs-tools (busybox-based, nie systemd-based initrd) a jeho cryptroot skript volá cryptsetup open s explicitným --key-file. Pri explicitnom key-file sa LUKS2 externé tokeny (FIDO2 je jeden z nich) vôbec neskúšajú — bez ohľadu na to, čo je v initrd zabalené.

Riešenie nebolo v úprave stock súborov, ale vo vlastnom skripte, ktorý sa spustí pred cryptroot-om (initramfs-tools garantuje poradie cez prereqs()), skúsi systemd-cryptsetup attach ... fido2-device=auto sám, a keď uspeje, cryptroot uvidí mapovanie už hotové a heslo vôbec nepýta. Keď zlyhá, ticho spadne na pôvodné heslo — fallback sa nikdy nemal rozbiť.

Vyzeralo to hotovo. Nasadené, initrd rebuild, teoreticky pripravené. Nasledovalo osem reštart-testov a každý odhalil inú vrstvu problému.

Test 1: kľúč nebliká, lebo nevie kedy

Prvý reálny reštart: opäť len heslo. Ukázalo sa, že skript pred pokusom o unlock nič nevypisoval — bezpečnostný kľúč len ticho zabliká LED, bez textovej výzvy. Bez signálu, kedy presne sa má kľúča dotknúť, pokus pravdepodobne ticho vypršal (timeout) a spadol na heslo presne podľa návrhu fallbacku.

Fix: skript vypíše "touch your security key now..." pred pokusom. Nasadené, overené priamo v zabalenom initrd.

Test 2 a 3: neviditeľný text za grafickým logom

Ďalší reštart — opäť nič, dokonca ani nová hláška nebola vidno. Vysvetlenie: kernel cmdline mal quiet splash, takže Plymouth preberá konzolu vlastným grafickým framebufferom. Obyčajný echo na /dev/console je fyzicky zapísaný, ale vizuálne prekrytý splash logom — jednoducho to nebolo vidieť, hoci sa to dialo.

Prvý pokus opravy (plymouth display-message) narazil na to, že aktuálna verzia/theme takýto príkaz vôbec nemá medzi platnými. Namiesto ladenia presného Plymouth API padlo rozhodnutie na univerzálnejší mechanizmus: plymouth hide-splash pred pokusom (zhodí grafiku na holú textovú konzolu) a show-splash po ňom — nezávislý od konkrétnej témy či verzie.

Test 4: falošné víťazstvo zo starého pokusu

Tu prišiel najzradnejší moment celej sagy. Boot vyzeral úspešne — PIN prompt sa zobrazil, kľúč sa dotkol, systém sa odomkol bez hesla. Zapísané ako vyriešené.

Bol to omyl. Ďalší "klasický" reštart o pár minút neskôr znova skočil rovno na heslo. Spätná diagnostika cez journalctl --list-boots odhalila, že ten úspešný boot bežal na úplne inom initrd (initrd-dracut-fido2.img) — samostatný, dávnejší a nikde poriadne nezapísaný experiment s dracutom namiesto initramfs-tools, ktorý som si sám kedysi rozbehol a potom naň zabudol. Fungoval preto, lebo dracut beží s plným systemd ako PID1, ktorý FIDO2 PIN prompt (cez systemd ask-password) obsluhuje automaticky. Bol to jednorazový výber v boot menu, nie skutočná oprava — ďalší reštart sa vrátil na pôvodný, stále rozbitý initramfs-tools variant.

Poučenie zapísané rovno na mieste: priebežné zapisovanie vlastného postupu nie je formalita — bez neho jeden rozbehnutý pokus nevie o druhom, a úspech na nesprávnej vetve sa ľahko vydá za vyriešený problém.

Testy 5-6: závod s časom a rozhodnutie zrušiť splash natrvalo

Ďalšia hypotéza: možno je zlyhanie príliš rýchle na zbadanie okom. Pridaný vynútený sleep 1 okolo pokusu. Kľúč fyzicky overený zapojený. Reštart — znova úplne nič.

To vylúčilo aj teóriu "príliš rýchlo", aj teóriu "zlý initrd" (priamo overené lsinitramfs-om, že skript aj knižnice tam skutočne boli). Namiesto ďalšieho naháňania Plymouth verzií padlo rozhodnutie na tvrdší, ale spoľahlivejší krok: natrvalo odstrániť splash z kernel cmdline. Plymouth potom vôbec neprevezme konzolu, takže čokoľvek sa vypíše, bude vidno vždy — bez závislosti na tom, ktorá verzia/téma akurát podporuje aké API.

Test 7: konečne čitateľná chyba

Prvý reštart bez splash priniesol niečo, čo predtým chýbalo najviac — konkrétnu chybovú hlášku: Device UUID=... does not exist or access denied. crypt_init() failed: Block device required.

Skript beží zámerne pred cryptroot-om, teda aj pred tým, než udev stihne vytvoriť /dev/disk/by-uuid/<UUID> pre LUKS partíciu. Jednorazový udevadm settle negarantuje, že "add" event bol na rýchlom boote vôbec spracovaný. Fix: aktívny polling na existenciu by-uuid cesty (každých 100 ms, max 10 s) namiesto jedného settle-volania.

Test 8: tri bugy naraz, odhalené priamym sledovaním konzoly

S čitateľnou konzolou stačilo po každom reštarte pozorne sledovať obrazovku — a odhalili sa tri ďalšie, dovtedy neviditeľné chyby naraz:

  1. UUID=xxx je syntax pre /etc/crypttab, ktorú samotná systemd-cryptsetup binárka neprekladá — treba jej dať priamo /dev/disk/by-uuid/xxx.
  2. Keyfile argument - spúšťal interaktívny heslový prompt namiesto FIDO2 cesty — správna hodnota (overená priamo zo systemctl cat unitu) je literálny reťazec none.
  3. Po oprave oboch bodov LUKS attach konečne štartoval správne, ale zamrzol bez PIN promptu a bez blikania kľúča. Príčina: FIDO2 PIN ide cez systemd ask-password mechanizmus, ktorý potrebuje bežiaceho agenta na obsluhu na konzole. Dracut initrd (ten jediný "náhodne úspešný" test vyššie) má plný systemd ako PID1 a rieši to automaticky. Initramfs-tools beží na busybox inite — nič z toho tam samo od seba nie je, treba to doplniť ručne.

Fix: pridaný systemd-tty-ask-password-agent --watch na pozadí, spustený pred samotným attach volaním.

Konečné víťazstvo — a prečo bolo treba ho overiť nezávisle

Piaty reálny test s touto verziou konečne fungoval tak, ako mal od začiatku: PIN naslepo, dotyk kľúča, odomknutie bez hesla, rovno na login obrazovku. Po skúsenosti s falošným pozitívom z testu 4 prišlo tentoraz aj nezávislé overenie — journalctl --list-boots, uptime -s a /proc/cmdline potvrdili čerstvý boot na defaultnom systemd-boot zázname, žiadny jednorazový alternatívny výber. Nebol to náhodný úspech na inej vetve — bola to opakovateľná, bežná cesta.

Ako drobný dodatok ešte prišlo doladenie: PIN sa pri zadávaní zobrazoval čitateľne (kozmetický bezpečnostný nedostatok) — opravené ručným stty -echo/stty echo okolo behu ask-password agenta. A samostatne overený aj edge case bez zapojeného kľúča: PIN prompt sa zobrazí, po zlyhaní korektne spadne na normálne heslo, presne podľa návrhu.

Bonus kolo: prečo login obrazovka nežiadala dotyk, hoci sudo áno

Po tom, čo fungoval unlock disku pri boote, prišla ďalšia otázka — prečo login obrazovka (greeter) nežiada dotyk kľúča, keď to na starom systéme fungovalo. Porovnanie s pripojeným starým diskom (read-only) ukázalo rozdiel: na starom systéme bol pam_u2f.so riadok v /etc/pam.d/common-auth, ktorý includujú všetky PAM služby vrátane login greetera. Na novom systéme bol ten istý riadok pridaný len do samostatného sudo súboru — do common-auth sa pri migrácii nedostal, lebo je to root-owned zmena mimo bežného $HOME presunu.

Namiesto úpravy globálneho common-auth (čo by ovplyvnilo všetky PAM služby naraz) bol riadok pridaný scoped, priamo do konfigurácie login greetera, s korektným fallbackom na heslo pri zlyhaní.

Čo z toho zostáva ako poučenie

  • "Enrolluj kľúč" je len prvý krok. Skutočná cesta k tomu, aby to fungovalo pri každom reštarte, závisí od typu initrd, boot splash nástroja a od toho, či beží plný systemd alebo len busybox init.
  • Splash obrazovka dokáže skryť aj funkčný kód. Ak niečo "nefunguje" bezo stopy, over najprv, či to vôbec vidíš — nie, či to beží.
  • Falošné pozitívum je horšie než zjavné zlyhanie. Overenie „na akom boot zázname presne bežal tento test" zachránilo pred nesprávnym uzavretím veci ešte raz.
  • Priebežný zápis stavu nie je byrokracia. Práve to odhalilo, že "úspech" pochádzal z celkom inej, paralelnej vetvy práce.
  • Priamy pohľad na konzolu je diagnostický nástroj, nie núdzové riešenie — keď nemáš log, to, čo vidno priamo na obrazovke, býva jediný dôkaz, ktorý máš.

Výsledok stál osem kôl testovania, ale dnes stačí priložiť kľúč a dotknúť sa ho — heslo ostáva len ako fallback, presne tak, ako to malo byť od prvého pokusu.

Riešite podobný problém?

Toto je presne typ práce, ktorú riešim aj pre iných — Linux servery, Docker nasadenia, zálohovanie a bezpečné nastavenie prístupu.

Pozrieť Linux & Docker služby