Sudo na diaľku cez FIDO2 kľúč, Face ID a Touch ID — a záhada, ktorú nakoniec vyriešil reštart Wi-Fi
Zadanie
Chcel som vedieť schváliť sudo na svojich domácich serveroch z iPadu — rovnakým
fyzickým FIDO2 kľúčom, aký už používam na lokálne prihlásenie — bez toho, aby
som pri každom vzdialenom zásahu musel odčítať a napísať TOTP kód. Prvá cesta,
ktorú som skúšal, bola SSH agent forwarding s hardvérovým kľúčom (pam_rssh na
serveri, FIDO2-backed SSH kľúč v terminálovej appke) — reálne to existuje, ale
kombinácia "appka na iPade, čo vie forwardovať FIDO2 signing request ďalej" bola
buď za predplatným, alebo nikde poriadne nezdokumentovaná.
Riešenie sa nakoniec ukázalo jednoduchšie, než celý ten obchádzkový smer:
Safari na iOS/iPadOS vie hovoriť s WebAuthn (rovnaký štandard ako FIDO2 aj
Passkeys) úplne natívne. Netreba žiadnu appku ani predplatné — stačí vlastná
webstránka, ktorá vie s kľúčom komunikovať priamo cez prehliadač. Postavil som
teda malý self-hosted backend (FastAPI, python-fido2) a pam_exec skript,
ktorý pri sudo pošle push notifikáciu s odkazom — ťuknutie naň otvorí stránku,
ktorá sama spustí WebAuthn overenie, a dotyk kľúča/Face ID/Touch ID to potvrdí.
Samotné postavenie šlo rýchlo. Dostať to do stavu, že to spoľahlivo funguje, bola poriadna detektívka.
Prvý bug: seteuid beží ako root, nie ako ja
Prvý reálny test len preskočil rovno na TOTP prompt, bez akejkoľvek stopy po
novom kroku. pam_exec.so má voľbu seteuid, ktorá prepne efektívne UID na
PAM_USER — a pri autentifikácii sudo je PAM_USER samotný root, nie
používateľ, čo sudo spúšťa. $HOME teda nebolo nastavené na moje, a cesta
$HOME/.config/ntfy/credentials.env sa vyhodnotila na nič — skript spadol
skôr, než sa vôbec pokúsil o čokoľvek sieťové. Oprava: cestu k credentials
súboru dať natvrdo, nespoliehať sa na $HOME.
Funkcia, ktorá to zhoršila namiesto zlepšenia
Chcel som vedieť čakanie na schválenie (85 sekúnd) preskočiť stlačením
ľubovoľnej klávesy. Implementácia cez read -t 2 < /dev/tty v pam_exec
kontexte narazila na to, že /dev/tty tam nie je použiteľný terminál — read
zlyhávalo okamžite, nie po timeoute. Celý 85-sekundový cyklus prebehol za
menej ako dve sekundy reálneho času, bez akejkoľvek šance to stihnúť schváliť.
Vrátil som sa k obyčajnému sleep.
ntfy push, čo tíško zlyhával na druhom stroji
Súbor s ntfy prihlasovacími údajmi mal NTFY_URL=http://127.0.0.1:8480 —
správne pre skripty bežiace priamo na stroji, kde ntfy beží, ale bezvýznamné
kdekoľvek inde, kde 127.0.0.1 znamená ten druhý stroj samotný. Oprava:
verejná HTTPS adresa natvrdo v skripte namiesto lokálnej skratky.
Zverejnenie si vynútilo druhý, prísnejší pohľad — a oplatilo sa
Pred otvorením repozitára verejne som si kód prešiel ešte raz, poriadne. Našiel
sa reálny problém: /register endpoint nemal žiadnu vlastnú autentifikáciu,
spoliehal sa len na sieťové obmedzenie IP rozsahu. Ktokoľvek na lokálnej sieti
by si tak mohol zaregistrovať vlastné poverenie a získať schopnosť schvaľovať
sudo na diaľku. Pridal som teda token, ktorý /register vyžaduje.
Rovnaký prechod odhalil aj 16 známych zraniteľností (pip-audit) v
cryptography a starlette — tranzitívnych závislostiach fido2 a
fastapi. Oprava znamenala major-verzný skok fido2 (1.x → 2.x), ktorý po
ceste zrušil jeden feature flag, na ktorom kód staval — zvyšok API ostal
rovnaký. Celý flow som po tejto zmene overil odznova, kľúčom aj Face ID aj
Touch ID, než som tomu znova dôveroval.
"Notifikácia príde, appka nič neukáže"
Server mal všetko v poriadku — každá správa uložená, s reálnym obsahom, bez
výnimky (overené priamo v úložisku ntfy). Príčina bola inde: self-hosted ntfy
nevie hovoriť s Apple push službou priamo, takže posiela cez ntfy.sh len
prázdny spúšťací push (kvôli súkromiu — obsah správy sa cez tretiu stranu
neposiela), a appka si má sama dotiahnuť skutočný obsah zo servera. Počas
rýchleho opakovaného testovania (viacero schválení za pár minút) začal iOS
toto dotiahnutie na pozadí obmedzovať — banner sa ukázal, obsah nie.
Rozloženie požiadaviek v čase problém odstránilo.
Posledný bug: "Safari nevie nájsť server"
DNS. Doména existuje len ako lokálny Pi-hole záznam (zámerne nikdy nešla do verejného DNS) a telefón mal zaseknuté zastarané/záporné vyhľadávanie — ticho vyriešené tým, že som to nechal tak. Vypnutie a zapnutie Wi-Fi vynútilo nové DNS vyhľadávanie, a celý flow prešiel čisto na ďalší pokus. Po celom tom hľadaní bola oprava ten najstarší trik v IT podpore.
Čo z toho zostáva
- Voľba
seteuidvpam_execmá vlastnú logiku — prisudoautentifikácii budePAM_USERroot, nie invoking user. Netreba sa spoliehať na$HOME. - Nekontrolovaná chyba vie byť horšia ako žiadna funkcia.
readzlyhávajúce okamžite namiesto timeoutu vytvorilo ilúziu čakania, kde žiadne nebolo. - Zverejnenie kódu je dobrý dôvod prejsť ho ešte raz, s inými očami. Presne vtedy sa našla chýbajúca autentifikácia aj zastarané závislosti.
- Push notifikácie majú vlastné, neviditeľné limity. iOS vie obmedziť dotiahnutie obsahu na pozadí pri príliš častom posielaní — vyzerá to ako bug v appke, nie je to.
- Skús najprv to najjednoduchšie. Reštart Wi-Fi vyriešil to, na čo som hľadal príčinu naprieč PAM, ntfy aj kódom backendu.
Výsledok: sudo sa dnes schvaľuje ťuknutím na notifikáciu a dotykom kľúča
(alebo Face ID, alebo Touch ID) — TOTP zostáva ako fallback presne tak, ako
malo od začiatku. Zdrojový kód je na GitHube.