← Späť na blog

Sudo na diaľku cez FIDO2 kľúč, Face ID a Touch ID — a záhada, ktorú nakoniec vyriešil reštart Wi-Fi

20. augusta 2026 · 4 min čítania

self-hostedbezpečnosť

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 seteuid v pam_exec má vlastnú logiku — pri sudo autentifikácii bude PAM_USER root, nie invoking user. Netreba sa spoliehať na $HOME.
  • Nekontrolovaná chyba vie byť horšia ako žiadna funkcia. read zlyhá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.