Automatické odemykání šifrovaných disků: Tang, Clevis a Gentoo

Full system encryption s pomocí LUKS už funguje delší dobu a není problém ji rozchodit. Zajímavější část nastává ve chvíli, kdy chceme disky odemykat trochu jinak, než jenom zadáváním hesla z klávesnice. Takže si vyzkoušíme Network Bound Disc Encryption, tedy stav, kdy se počítač automaticky odemkne pomocí klíče získaného ze sítě (který by měl dostat jen ve chvíli, kdy je počítač spuštěn v bezpečné síti). Zní to možná pitomě, nicméně pokud encryption server běží někde schovaný a je přístupný jen z lokální sítě (třeba přes VPN někam), při ukradení počítače/serveru má obvykle daná osoba smůlu, protože ke klíči se již nedostane. Takže jediná chvíle, kdy se lze ke klíči dostat, je v prostředí, kde stanice zrovna běží. Ano, v tuhle chvíli (narozdíl od TPM, u kterého se sází na to, že ten klíč prostě nikomu jinému nevydá) se lze ke klíči teoreticky dostat. Pokud má někdo dost času si s tím v té lokalitě hrát... u TPM na druhou stranu systém nabootuje vždy, takže i po odnesení je dost prostoru hledat bugy ve všem, co na dané stanici naběhne...

Kromě zmíněného LUKSu jsou klíčové nástroje tang a clevis. Tang je server, který přes síť na vyžádání posílá svůj klíč. Clevis je zase nástroj běžící naopak na zašifrované stanici, který se stará o odemykání svazků. Clevis toho umí více, používá se například při odemykání svazků pomocí TPM chipu ve stanici. Nicméně mimo jiné umí klíč získat i z Tang serveru a na to se teď zaměříme. O celém tomhle systému před časem vyšel článek na "rootu":https://www.root.cz/clanky/sifrovane-disky-na-serveru-automaticke-odemyk.... Protože celý systém není až tak komplikovaný a nepřinesl bych nic nového, zaměříme se hlavně na to, jak to rozchodit na Gentoo distribuci (doporučuji tedy přečíst i původní článek). Konkrétně na Gentoo s OpenRC (tedy bez systemd), kde nastává celá řada komplikací...

Tang

Tang je bezestavový server, který je vlastně poměrně jednoduchý. Jeho bezestavovost má výhodu a nevýhodu. Nevýhoda je, že při kompromitaci (ukradení) nějaké stanice nelze její klíč nějak revokovat. Je vlastně potřeba vygenerovat nový klíč na tangu, čímž ale přestanou fungovat klíče i na všech ostatních stanicích. Výhoda zase je, že tam v podstatě není ani co nastavovat. Prostě se to spustí na nějakém TCP portu a ono to běží. V Gentoo je Tang (i Clevis) v guru overlay repository. Takže si podle "instrukcí":https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users zapneme danou repository a emergneme balíček tang (je maskovaný ~ keywordem). Tedy v případě, že i ten chceme mít někde na gentoo (serveru) - není to podmínkou...

Takže v /etc/conf.d/tangd si nastavíme listen adresu a port a službu spustíme. To je vše.

Clevis

Na stanici, kterou chceme odemykat, nainstalujeme clevis ze stejné overlaye jako tang. Zkontrolujeme, že máme clevis minimálně verze 19-r1, protože právě od téhle verze v něm jsou patche, díky kterým fungují dracut moduly bez systemd. Samotné nabindování clevisu je jednoduché, pustíme příkaz clevis luks bind -d /dev/sdXY tang '{"url":"IP:PORT"}', kde sdXY zaměníme za náš block device, na kterém sedí šifrovaný fs a IP vyměníme za IP adresu tang serveru, PORT za port, na kterém běží. Po spuštění bude potřeba zadat heslo k šifrovanému svazku a potvrdit klíč tang serveru a to je vše.

Zajímavější část začíná až nyní - donutit to nabootovat. Potřebujeme totiž, aby v initrd při bootu bylo něco, co se připojí k síti, zkontaktuje tang server a odemkne zařízení. Genkernel toto neumí, takže na generování initramdisku použijeme dracut, který tedy nainstalujeme. Do /etc/dracut.conf jsem doplnil řádek:

  1. add_dracutmodules+=" network clevis "

který říká, že se mají přidat moduly network a clevis. Pozor na mezery okolo uvozovek, jsou třeba. Taky konfigurační soubor říká, že by se neměl tenhle soubor editovat, ale radši vyrobit separátní konfigurák v /etc/dracut.conf.d/*.conf - kdo to chce mít lepší, může to zkusit :-)

A nyní nastává velká zábava s laděním boot parametrů do cmdliny kernelu. Já si je doplnil přímo v grubu, tedy v /etc/default/grub do proměnné GRUB_CMDLINE_LINUX (kde následně je potřeba přegenerovat konfigurák grubu); alternativně by to mělo jít napsat i do dracut.conf před vygenerováním initrd. Postupně jsem přidával tyto parametry:

  • rd.neednet=1 - nutnost, bez tohoto se vůbec neicinalizuje síťové připojení a bez něj se k síťovému serveru připojuje velice blbě
  • ip= - a tady nastává celká zábava, na které jsem se na dlouhou dobu zasekl. Tohle je standardní kernel parametr na nastavení sítě, který se třeba používá i při bootu z NFS a podobně. Pokud máte v síti DHCP, pak by parametr ip=dhcp měl fungovat. Jenže... dracut okolo toho dělá velké tanečky. Jednak je otázka, kolik lidí s dracutem to ještě používá, protože dracut na systému se systemd tu síť umí nastavovat i network-managerem a dalšíma nějakýma takovýma věcma. Takže ten modul network-legacy dost možná moc lidí nepoužívá... a věc druhá, dracut i s network-legacy má různé scripty, co to vylepšují, pro případ, že chcete třeba statickou routu nebo tak. Bohužel v mém případě ip=auto vedlo k tomu, že PC si při bootu koretkně vzalo konfiguraci z DHCP, ale bohužel mu velice pravděpodobně jaksi zmizela default routa. Alespoň chování bylo takové, že na L2 síti počítač pingal, ale clevis házel chybu o nedosažitelnosti tang serveru a na routeru po komunikaci ani památky. A taky se to dost blbě debugguje, protože (viz debugging dále) jsem nenašel breakpoint, který by byl mezi nastavením sítě a spuštěním clevisu. Prostě buď dostanu shell před nastavením sítě a nevím co je tam blbě (klasický dhclient zafunguje dobře) nebo už clevis běží v nekonečné smyčce s chybou a shell nedostanu. Nakonec jsem to vzdal a vzhledem k velikosti sítě do parametru dal statickou konfiguraci, která funguje dobře. Takže pokud do parametru dáte 192.168.1.100::192.168.1.1:255.255.255.0:moje-pc:eth0:off - znamená to, že si PC nastaví IP adresu 192.168.1.100 s maskou 255.255.255.0, výchozí bránou 192.168.1.1, hostnamem moje-pc a bez další autokonfigurace na síťové kartě eth0. Pro úplnost dodávám, že můj tang server byl specifikovaný napřímo IP adresou. Pokud bych ho hledal DNS názvem, ještě bych raději zkontroloval, že v tom ramdisku je nějaký resolv.conf :-)
Ták, pokud jsme upravili parametry grubu, vygenerujeme nový config (grub-mkconfig - /boot/grub/grub.cfg) a spuštěním příkazu dracut vygenerujeme RAM disk. A můžeme to zkusit otočit... a ono to dost možná nenajede, žejo :-) Tady jsem narazil na další chyták a sice specifikace root zařízení v cmdline kernelu. Tedy v těch parametrech výše je potřeba mít i parametr root= ukazující na zařízení, kde je odemčený root filesystem. Nicméně když grub-mkconfig generuje konfigurační soubor, vymyslí si tam ten parametr sám, tak jak odpovídá na běžícím systému. Takže v zásadě nemá velký význam to psát do GRUB_CMDLINE_LINUX, protože to tam pak bude 2x. Jenže, pokud jste měli funkční systém třeba s initrd vygenerovanou genkernelem, bude to zařízení velice pravděpodobně jiné a boot skončí na tom, že nemáte root zařízení. Zvolil jsem metodou žaves - zjistil jsem, jak se jmenuje odemčené root zařízení v nové initrd s clevisem. To zjistíte nejspíše také, když to po nějaké době spadne do rescue shellu. Nicméně pravidlem se zdá být, že zařízení s odemčeným rootem bude v /dev/mapper/luks-UUID, kde UUID patří partitioně/zařízení se šifrovaným filesystemem (tedy /dev/sdXY typicky a podobně). Takže nabootujeme funkční verzi systému, přímo v /boot/grub/grub.cfg si upravíme záznam, který bootujeme a root= změníme na funkční zařízení. Restartneme s naší initrd, mělo by to naběhnout. A po restartu zase spustíme grub-mkconfig -o /boot/grub/grub.cfg, čímž se korektně vygenerují všechny záznamy, co tam jsou

TRIM a discards

Háček číslo 3. Pokud náš systém je na SSD. LUKS defaultně nenastavuje discards flag na šifrovaných oddílech, což znamená, že TRIM na zadaných zařízeních nefunguje. Je to z důvodu bezpečnosti, jelikož u správně zašifrovaného disku nelze poznat, kde nějaká data jsou a kde ne (ikdyž se zašifruje úplně prázdný disk, na kterém jsou jen nuly, výsledkem bude náhodně vypadající data). TRIMem naopak zase operační systém říká SSD disku kde on žádná data nemá. Z důvodu toho, že SSD disky mají nějakou danou životnost paměťových buněk na počet přepisů. A jelikož většina systémů typicky pořád zapisuje na stejná místa, tak by velice rychle diskům některé buňky odešly, zatímco jiné by byly nepoužity. Z toho důvodu SSD disky zápisy rozkládají (takže ikdyž OS si myslí, že zapsal data na adresu X, disk je zapíše ve skutečnosti na Y a jen si pamatuje, že až si OS řekne o X, tak mu vrátí Y), aby se buňky opotřebovaly rovnoměrně. Jenže zase pak SSD disk potřebuje vědět, které buňky jsou prázdné, protože to on zase nemá jak zjistit, protože to ví OS... a OS mu to řekne trimem. V případě šifrování tedy TRIM prozradí, která místa disku jsou prázdná a tedy kde leží ta skutečná data... ale zase výrazně prodlouží život SSD. Můžete si vybrat :-)

Nicméně, pokud to chcete zapnout, tak je tu ten háček... V případě dracutu šlo používat volby cmdline kernelu jako rd.luks.options=discards a podobně. Ale když je zkusíte v tomto případě, nebude to fungovat. (Funkčnost ověřujeme příkazem cryptsetup status root_zařízení, ve výpisu by se mělo objevit flags: discards, pokud je TRIM funkční). Proč? Protože ty parametry výše fungují v případě, že vám to LUKS zařízení odemyká přímo nějaký dracutí modul. Který se typicky zeptá na heslo a tak dále. Jenže vy ty LUKS zařízení neodemykáte dracutem. Dracut pouze pustí clevis a všechno odemykání řídí clevis. Takže, jak donutit clevis, aby nastavil discards flag? Clevis na to má otevřený ticket na githubu... několik let :-) Takže to neumí...

Ale naštěstí máme workaround. Nabootujeme si naše PC jako normálně, cryptsetup status vrací výpis bez flags, takže to nefunguje. Příkazem cryptsetup --allow-discards --persistent refresh /dev/sdXY (kde sdXY je naše blokové zařízení se šifrovaným filesystémem) discards zapneme. A klíčový parametr --persistent říká, že by to takto mělo být nastaveno vždy. Restartujeme náš systém a znovu zkusíme cryptsetup status - nyní by již vše mělo být v pořádku.

Debugging

Pokud se nám v initrd nějak cokoliv nedaří a chceme vidět, co to dělá, máme nějaké možnosti. Bohužel typicky jenom do chvíle, než se pustí clevis - ten má takovou nectnost, že některé chyby v něm visí v nekonečné smyčce a dále nás nepustí :-) Nicméně do cmdline kernelu (hint: pokud jste to v grubu nezakázali, pak v menu grubu klávesou e si můžete zeditovat bootovací záznam a stisknutím ctrl+x jej spustit - není nutné pokaždé měnit přímo konfigurační soubor grubu) lze přidat jednak parametr rd.shell, který říká, že se má pustit emergency shell v případě problému. A dále rd.break= kde nastavíme v kterém místě má přerušení spouštění nastat (a v té chvíli se spustí náš shell). Takže tímto lze proces bootu přerušit a pátrat dále, co může nastat. Možné hodnoty pro rd.break jsou: cmdline, pre-udev, pre-trigger, initqueue, pre-mount, mount, pre-pivot, cleanup. Pro úplnost dodám, že co se kde spouští lze dohledat ve scriptech u dracutu a například clevis se spouští v initqueue (takže rd.break=initqueue zastaví spouštění předtím, než se spustí clevis). A pro druhou úplnost dodám, že rd.break=cmdline breakne spouštění hned po načtení ramdisku. Ještě před tím, než se načtou kernel moduly. Takže pokud jste tak chytří jako já, že ovladače pro USB máte jako modul v ramdisku, pak si do toho shellu klávesnicí moc nenapíšete :-)