SSH fără haos: un CA intern, un bastion și certificate de 30 de zile
Adaugi o cheie nouă pe un server. Merge. Peste două săptămâni ai un laptop nou și trebuie să repeți treaba. Peste o lună ai o cheie veche de la cineva care nu mai lucrează cu voi și nu mai știi pe ce mașini stă. Dacă ai administrat vreodată mai mult de zece servere cu authorized_keys, cunoști senzația.
Fiecare device nou înseamnă încă o linie în fișierul de pe fiecare host. Revocarea unei chei devine o vânătoare prin zeci de fișiere, iar nimeni nu are certitudinea că a șters tot. Nu e o problemă de securitate teoretică. E o problemă de oboseală, iar oboseala produce excepții.
Am ajuns la un homelab cu aproximativ 50 de hosturi și 13 chei publice active, RSA și ED25519 amestecate. Nu era un dezastru, dar era exact genul de situație în care următoarea modificare costă mai mult decât ar trebui. Am decis să schimb modelul: în loc de chei copiate pe fiecare host, un singur CA SSH intern semnează certificate, iar certificatele expiră după 30 de zile.
Articolul ăsta nu e un ghid universal. Descrie ce am construit, ce ne-a costat și ce nu am rezolvat încă.
Cum arată soluția
Ideea de bază e simplă. Pe un singur host, bastionul, există o cheie de CA. Serverele țintă nu mai au nevoie de cheile utilizatorilor în authorized_keys. În schimb, au încredere în CA. Când un utilizator se conectează, serverul verifică certificatul, nu o listă de chei.
Un certificat conține o identitate (principals), o perioadă de valabilitate și, în cazul nostru, o restricție de source-address, adică de la ce rețele poate fi folosit. Dacă un device se pierde, certificatul lui expiră singur în cel mult 30 de zile.
Pentru un device nou, fluxul arată așa:
- Rulezi un script de instalare, care generează o cheie dedicată device-ului și scrie un fișier de configurare separat.
- Autorizarea se face o singură dată, pe bastion.
- Scriptul urcă cheia publică, cere semnarea certificatului și îl salvează lângă cheie.
- La final, verifică patru hosturi țintă, care răspund cu numele lor, ca să fii sigur că lanțul funcționează.
Configurația SSH a device-ului rămâne în propriul ei fișier. Restul ~/.ssh/config nu e atins, iar înainte de orice modificare se face backup. Mi se pare important, pentru că fișierul principal de config e locul unde se ascund lucrurile pe care nu le mai înțelegi peste un an.
Comenzile pe care le folosim sunt puține:
./install.sh # instalare pe un device
./install.sh --device <nume> # nume explicit
./install.sh --renew # reînnoire certificat
./install.sh --dry-run # arată ce ar face
Pentru verificare, folosim două comenzi. Prima arată validitatea, principals-urile și restricția de rețea:
ssh-keygen -L -f ~/.ssh/id_ed25519_<device>-cert.pub
A doua arată dacă serverul acceptă certificatul:
ssh -v <host> 2>&1 | grep -i "accepts key"
Dacă a doua comandă nu arată tipul de certificat, ai o problemă de configurare, nu de autentificare.
Lecțiile plătite
Fiecare punct de mai jos a costat timp. Le scriu cu simptomul pe care l-am văzut și cu fixul.
-
Simptom: cheia de homelab pusă în
Host *strica autentificarea pe GitHub și pe Gitea, pentru că SSH încerca cheia greșită. Fix: blocul wildcard păstrează doar setările generale, de exemplu keepalive. Cheile se leagă explicit, pe hosturi. -
Regula: în
ssh_configprimul match câștigă. Dacă blocul wildcard vine primul, blocurile speciale nu mai au efect. Așa că bastionul (cuProxyJump none) și hosturile cu alt utilizator stau înaintea wildcard-ului. -
Simptom:
ssh <nume-scurt>a căutat literal numele scurt în DNS și a picat cuCould not resolve hostname. Fix: fiecare nume scurt are o mapare explicită,HostNamecu sufixul intern. Nu presupunem că numele scurte se rezolvă singure. -
Simptom: pe Linux, linii valide doar pe macOS (
UseKeychain,AddKeysToAgent) dauBad configuration option. Fix: template-ul injectează aceste linii doar pe Darwin. Un template comun pentru toate sistemele trebuie să ție cont de diferențele dintre clienți. -
Simptom: un mecanism de ocolire a bastionului, cu cheia brută, funcționa pe un tip de host și pe altul nu, iar cheia trebuia scrisă într-un fișier partajat la nivel de cluster. Fix: ocolirea e dezactivată implicit și se activează doar explicit, cu flag-ul
--node-direct. Concluzia pe care o trag: un mecanism de urgență care nu se comportă identic pe toate hosturile e mai periculos decât lipsa lui. -
Simptom: scriptul de instalare nu se poate autentifica pe bastion pe un device nou, pentru că nu există încă nicio autentificare. Fix: nu încercăm să rezolvăm asta din script. Scriptul afișează comanda exactă pe care trebuie s-o rulezi de pe un device care are deja acces. Mi se pare o limitare sănătoasă, nu un defect.
Când dispare un device
Revocarea, la noi, înseamnă ștergerea unei singure linii din authorized_keys de pe bastion, adică linia cu cheia publică a device-ului pierdut. Nu trebuie să umblăm prin zeci de fișiere, cum se întâmpla înainte. Asta e diferența față de modelul vechi, unde fiecare host avea propria copie a cheii și trebuia curățat separat.
Rămâne însă o fereastră. Certificatele se reînnoiesc prin ./install.sh --renew, iar un device pe care nu îl mai controlezi nu are cum să ceară un certificat nou. Accesul lui cade în cel mult 30 de zile, când expiră certificatul deja emis. Până atunci, device-ul pierdut poate intra, dacă certificatul lui încă e valabil. Pentru un device pierdut, asta înseamnă că riscul are un termen, nu e nelimitat.
Aici trebuie să fim onești. Nu avem revocare instantanee. Nu avem o listă de revocare de certificate (KRL), iar dacă ai nevoie de o tăiere imediată, trebuie să o construiești. La noi, încă nu există. Asta e o limitare a soluției, nu o funcționalitate pe care o putem vinde.
Ce nu am rezolvat
Nu vreau să vând o soluție completă. Iată ce rămâne deschis.
Dispozitivele de rețea care nu vorbesc OpenSSH modern rămân pe parolă sau pe interfața controllerului. Din inventarul nostru, 4 din 49 de hosturi intră în categoria asta. Seria de față nu le acoperă, iar dacă ai switch-uri și routere în aceeași situație, vei avea aceeași problemă.
Certificatul e valabil doar din rețelele locale, pentru că restricția de source-address e setată așa. Accesul din exterior e o discuție separată, cu alte riscuri, și nu l-am rezolvat în această fază.
Reînnoirea e manuală, la fiecare 30 de zile. Dacă uiți, certificatul expiră și rămâi pe cheia brută din authorized_keys a bastionului, care e tot o cheie de urgență. Un cron care să reînnoiască automat e următorul pas, dar încă nu l-am făcut. Până atunci, depinde de disciplina noastră, iar asta nu e un sistem bun.
Încheiere
Dacă administrezi un mic birou sau un homelab cu zeci de hosturi și simți că accesul SSH a devenit un fișier de inventar, nu un sistem, merită să discutăm. Nu îți pot promite că soluția noastră se potrivește mediului tău, dar pot să-ți arăt ce am încercat, ce a mers și ce a picat. Scrie-ne și hai să vedem împreună unde se blochează lucrurile la tine.
0 comentarii on "SSH fără haos: un CA intern, un bastion și certificate de 30 de zile"