Klucz SSH, którego nie da się skopiować z laptopa
Cel: serwer akceptuje wyłącznie sprzętowe klucze, a klucz prywatny nigdy nie opuszcza chipa YubiKey. Bez fizycznego dotknięcia klucza i znajomości PIN-u nikt nie zaloguje się na zdalny serwer.
Metoda: FIDO2 z resident key (ed25519-sk). W ~/.ssh/ zostają dwa pliki:
- klucz publiczny — standardowo, do wgrania w
authorized_keysna serwerach, - plik handle — kilkadziesiąt bajtów metadanych mówiących SSH-owi "spytaj YubiKey o klucz pod credential ID
0xab12...i etykietąssh:prod". Sam handle niczego nie podpisze — bez wpiętego YubiKey jest bezużytecznym plikiem. W każdej chwili można ten plik ponownie wygenerować z klucza.
Wymagania wstępne
| Komponent | Wymaganie | Jak sprawdzić |
|---|---|---|
| YubiKey | seria 5 z firmware ≥ 5.2.3 | ykman info |
ykman | zainstalowany | ykman --version |
libfido2 | zainstalowana | pkg-config --modversion libfido2 (Linux)na Windows wbudowana w OpenSSH |
| OpenSSH (client) | ≥ 8.2 | ssh -V |
| OpenSSH (serwer) | ≥ 8.2; ≥ 8.4 dla verify-required | ssh -V |
| Grupay | plugdev | groups |
| PIN FIDO2 | ustawiony na YubiKey | ykman fido info → PIN is set |
Generowanie klucza
cd ~/.ssh
ssh-keygen -t ed25519-sk -O resident -O application=ssh:personal -O verify-required -C "robert_personal@yubikey_1"
ssh-keygen zapisze klucz publiczny i plik handle, a klucz prywatny zostanie w chipie YubiKey.
| Flaga | Znaczenie |
|---|---|
-t ed25519-sk | typ klucza FIDO2 oparty na Curve25519; działa tylko na firmware'ze ≥ 5.2.3 |
-O resident | klucz przechowywany w całości na YubiKey |
-O application=ssh:personal | etykieta klucza na urządzeniu (konwencja Yubico: ssh:<nazwa>) |
-O verify-required | klucz wymaga PIN-u + dotyku przy każdym użyciu |
-C "..." | komentarz, który ląduje w authorized_keys na serwerze |
Plik handle (id_ed25519_sk) to nie jest klucz prywatny. Bez wpiętego YubiKey ten plik niczego nie podpisze. Sam klucz prywatny siedzi wewnątrz trusted module w' YubiKey.
Plik handle ma nagłówek -----BEGIN OPENSSH PRIVATE KEY----- i wygląda identycznie jak zwykły klucz prywatny ed25519 — to mylące, ale w środku siedzi tylko credential ID + application string + klucz publiczny, żaden sekret. Typ klucza w base64 to sk-ssh-ed25519@openssh.com (sufiks -sk = security key). Test rozstrzygający: wypnij YubiKey i spróbuj ssh -i id_ed25519_sk user@host — dostaniesz device not found. Passphrase, o który pyta ssh-keygen, szyfruje handle, nie sekret. OpenSSH używa tej samej koperty PEM celowo, żeby reszta toolingu traktowała oba typy plików jednolicie.
Czy da się logować bez PIN-u i bez dotyku? Tak, da się — ale każdy taki kompromis odbiera YubiKey jedną z warstw bezpieczeństwa. Jeśli chcesz podnieść wygodę kosztem bezpieczeństwa to możesz to zrobić.
| Wariant | Flagi przy ssh-keygen | Ryzyka |
|---|---|---|
| PIN + touch | -O resident -O verify-required | pełna ochrona |
| Tylko touch | -O resident | kradzież YubiKey = logowanie bez przeszkód |
| Tylko PIN | -O resident -O verify-required -O no-touch-required | malware na laptopie może podpisywać challenge, bez twojej wiedzy |
| Nic | -O resident -O no-touch-required | YubiKey staje się zwykłym kluczem trzymanym sprzętowo |
Klucze z no-touch-required serwer domyślnie odrzuca. Aby zaakceptować, w authorized_keys musisz dodać prefix no-touch-required przed kluczem. Bez tego — Permission denied. Jeśli serwer ma verify-required w authorized_keys, klient bez PIN-u się nie zaloguje. Decyzja zapada na obu końcach niezależnie.
Zarządzanie kluczami
YubiKey 5 z firmware'em 5.7+ trzyma do 100 resident keys. Etykieta application=ssh:nazwa przy generowaniu jest tu kluczowa — bez niej masz 100 kluczy z RP: ssh: i nie wiesz, który do czego. Listę dostępnych kluczy wyświetlisz tak:
ykman fido credentials list
# Credential ID: abcd1234... | RP: ssh:prod | User: robert
# Credential ID: efgh5678... | RP: ssh:staging | User: robert
# Credential ID: ijkl9012... | RP: ssh:github | User: robert
Usuwanie:
ykman fido credentials delete <Credential ID>
Dystrybucja klucza publicznego
Mamy klucz na YubiKey i plik ~/.ssh/id_ed25519_personal.pub na laptopie. Żeby zalogować się na jakiś serwer, jego publiczna część musi trafić do ~/.ssh/authorized_keys na koncie docelowym. To jest jednokierunkowe — klucz publiczny rozsiewasz, prywatny (na chipie) zostaje przy tobie.
Lokalnie — to samo konto na tej samej maszynie
Najprostszy przypadek: chcesz logować się sam do siebie kluczem (np. żeby ssh localhost działało albo do testów):
cat ~/.ssh/id_ed25519_personal.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Na inny serwer — ssh-copy-id
Standardowy sposób, gdy jeszcze masz dostęp hasłem albo innym kluczem:
ssh-copy-id -i ~/.ssh/id_ed25519_personal.pub robert@mini-2.lan
Flaga -i celowo wskazuje na plik .pub, nie na handle. ssh-copy-id bez -i próbuje wszystkich kluczy z agenta i ~/.ssh/ — przy YubiKey skończy się to seryjnym miganiem chipa. Zawsze podawaj jawnie który klucz wysyłasz.
Publikacja do GitHuba, potem import ssh-import-id-gh
To jest mój ulubiony trick. Wrzuć klucz publiczny raz do GitHuba — i każdy nowy serwer obsłużysz jednym poleceniem. To publiczny endpoint — bez autoryzacji, bez tokena. Każdy może pobrać twoje klucze publiczne (w końcu są publiczne). Ubuntu pozwala je pobrać przy instalacji więc możesz się łatwo zalogować na każdy nowy serwer.
1. Dodaj .pub w GitHubie: Settings → SSH and GPG keys → New SSH key, wklej zawartość ~/.ssh/id_ed25519_personal.pub.
2. Na nowym serwerze importujesz wszystkie swoje klucze jednym poleceniem:
ssh-import-id-gh <twoj-login>
ssh-import-id-gh (pakiet ssh-import-id w Ubuntu/Debian) ściąga https://github.com/<login>.keys i dopisuje każdy klucz do ~/.ssh/authorized_keys z komentarzem # ssh-import-id gh:<login>. Idempotentne — uruchom drugi raz, nie zduplikuje.
Dla kompletności — istnieje też ssh-import-id-lp dla Launchpada (lp:<login>) i ogólny ssh-import-id z prefiksem.
Regeneracja pliku handle
Nowy laptop, świeży ~/.ssh/, ale resident keys nadal siedzą na YubiKey. Wpinasz klucz i odtwarzasz wszystkie handle jednym poleceniem. ssh-keygen -K tworzy plik handle + .pub dla każdego resident key (PIN raz, touch na klucz). Bez fizycznego YubiKey i PIN-u procedura jest niewykonalna.
cd ~/.ssh
ssh-keygen -K
Konfiguracja klienta
Ten krok jest opcjonalny, jeśli masz tylko jeden klucz na YubiKey. Wtedy ssh user@host po prostu zadziała.
Wygeneruj osobne handle z różnymi etykietami i utwórz ~/.shh/config. Pole IdentitiesOnly yes jest ważne, bez tego SSH zaproponuje wszystkie klucze z ~/.ssh/ po kolei, co przy YubiKey oznacza wielokrotne dotykanie i wpisywanie PIN-u, zanim trafi we właściwy.
Host mini-1
HostName mini-1.lan
User robert
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
Host mini-2
HostName mini-2.lan
User robert
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
Pierwsze logowanie
ssh mini-2
# Confirm user presence for key ED25519-SK SHA256:abc...
# Enter PIN for "ssh:personal":
# [YubiKey miga — dotykasz]
# Last login: ...
robert@mini-2:~$
Co dzieje się pod spodem: SSH czyta plik handle wskazany w ~/.ssh/config, wyciąga z niego credential ID, przez libfido2 prosi YubiKey o podpis. YubiKey weryfikuje PIN, czeka na touch, podpisuje challenge serwera. Serwer weryfikuje podpis kluczem publicznym z authorized_keys i wpuszcza.
Hardening serwera
Jedyne co musisz zrobić, żeby logować się YubiKey-em, to wgrać klucz publiczny do ~/.ssh/authorized_keys (sekcja wyżej). Domyślny sshd zaakceptuje go bez żadnych zmian w konfiguracji.
To, co poniżej, jest zalecane, ale nieobowiązkowe — domyka wektory ataku (hasła, słabsze klucze) skoro i tak masz już klucz sprzętowy.
Skoro już masz logowanie kluczem sprzętowym — wyłącz wszystko inne. /etc/ssh/sshd_config:
# Akceptuj WYŁĄCZNIE klucze sprzętowe FIDO2
PubkeyAcceptedAlgorithms sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com
PasswordAuthentication no
KbdInteractiveAuthentication no
Restart:
sudo sshd -t
sudo systemctl reload ssh
verify-required na poziomie authorized_keys
Możesz wymusić PIN + touch po stronie serwera, niezależnie od tego jak klient wygenerował klucz. W authorized_keys:
verify-required sk-ssh-ed25519@openssh.com AAAAGnN... robert@yubikey5c-prod
Dodanie klucza do GitHuba
GitHub akceptuje klucze SSH w dwóch oddzielnych rolach, jeden klucz publiczny może pełnić obie role — musisz go jednak dodać dwa razy.
| Rola | Zastosowanie |
|---|---|
| Authentication key | używany przez git push, git clone git@github.com:... — klasyczny klucz SSH |
| Signing key | używany do podpisywania commitów (od Git 2.34 SSH działa jako alternatywa GPG) |
Zakładka Settings → SSH and GPG keys → New SSH key:
- Title:
YubiKey 5C primary(cokolwiek, co rozpoznasz za 6 miesięcy). - Key type: Authentication Key.
- Key: wklej zawartość
~/.ssh/id_ed25519_personal.pub.
Windows
Na Windowsie 10/11 używamy natywnego OpenSSH (≥ 8.9). Cały workflow z tego posta — ssh-keygen -t ed25519-sk, ~/.ssh/config, ssh-keygen -K — działa w PowerShellu bez modyfikacji. Jedyna realna różnica: operacje zarządzające resident keys wymagają admina.
Ścieżki i prompt
Plik konfiguracyjny leży w C:\Users\<ty>\.ssh\config, składnia 1:1 jak na Linuksie — w samych wpisach możesz nadal pisać ~/.ssh/..., OpenSSH na Windowsie rozumie tyldę. Prompt PIN-u i Confirm user presence wyświetla się jako natywne okno Windows zamiast w terminalu, funkcjonalnie bez różnicy.
Operacje na resident keys wymagają admina
ssh-keygen -K (regeneracja handle) i ykman fido credentials list/delete muszą iść z elevated PowerShella. Codzienne ssh user@host i git push — zwykły user.
Dlaczego admin? Te polecenia używają niskopoziomowego CTAP2 over raw HID, który Windows blokuje bez elevation. WebAuthn API (z którego korzysta zwykłe logowanie SSH) nie udostępnia operacji typu „wylistuj/pobierz resident keys" — stąd libfido2 musi iść kanałem wymagającym admina. Objaw bez elevation: ssh-keygen -K zwraca mylący Unable to load resident keys: invalid format.
W praktyce wygodnie jest zainstalować gsudo (winget install gerardog.gsudo) i wołać gsudo ssh-keygen -K ze zwykłego okna, zamiast każdorazowo otwierać osobny terminal jako Administrator.