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 |
| Grupa | 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 secure element w YubiKey.
Plik handle ma nagłówek -----BEGIN OPENSSH PRIVATE KEY----- i wygląda identycznie jak zwykły klucz prywatny ed25519 (co jest mylące), jednak w środku znajduje się wyłącznie credential ID + application string + klucz publiczny, żaden sekret. Typ klucza w base64 to sk-ssh-ed25519@openssh.com (sufiks -sk oznacza security key). Test rozstrzygający: po odłączeniu YubiKey próba ssh -i id_ed25519_sk user@host zwraca 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.
Logowanie bez PIN-u i bez dotyku jest możliwe, jednak każdy taki kompromis odbiera YubiKey jedną z warstw bezpieczeństwa.
| 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 należy dodać prefix no-touch-required przed kluczem. Bez niego serwer zwróci Permission denied. Jeśli serwer ma verify-required w authorized_keys, klient bez PIN-u nie uzyska dostępu. Decyzja zapada na obu końcach niezależnie.
Zarządzanie kluczami
YubiKey 5 z firmware'em 5.7+ przechowuje do 100 resident keys. Etykieta application=ssh:nazwa podana przy generowaniu jest kluczowa: bez niej wszystkie klucze mają RP: ssh: i nie ma możliwości ich rozróżnienia. Listę dostępnych kluczy wyświetla polecenie:
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
Klucz rezyduje na YubiKey, a plik ~/.ssh/id_ed25519_personal.pub pozostaje na hoście roboczym. Aby uzyskać dostęp do zdalnego serwera, część publiczna klucza musi znaleźć się w ~/.ssh/authorized_keys na koncie docelowym. Operacja jest jednokierunkowa: klucz publiczny jest dystrybuowany na serwery, klucz prywatny pozostaje w chipie urządzenia.
Lokalnie — to samo konto na tej samej maszynie
Najprostszy przypadek: klucz służy do logowania na tym samym hoście (np. żeby ssh localhost działało lub do testów):
cat ~/.ssh/id_ed25519_personal.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Na inny serwer — ssh-copy-id
Standardowa metoda, gdy dostęp hasłem lub innym kluczem jest jeszcze aktywny:
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 kolejno wszystkich kluczy z agenta i ~/.ssh/, co przy YubiKey skutkuje seryjnym miganiem chipa. Należy zawsze podawać jawnie, który klucz jest wysyłany.
Publikacja do GitHuba, potem import ssh-import-id-gh
Wystarczy wrzucić klucz publiczny raz do GitHuba — każdy nowy serwer można obsłużyć jednym poleceniem. Endpoint jest publiczny: bez autoryzacji, bez tokena. Ubuntu korzysta z tego mechanizmu przy instalacji, co upraszcza wstępny dostęp do nowych serwerów.
1. Dodaj .pub w GitHubie: Settings → SSH and GPG keys → New SSH key, wklej zawartość ~/.ssh/id_ed25519_personal.pub.
2. Na nowym serwerze zaimportuj wszystkie klucze jednym poleceniem:
ssh-import-id-gh <twoj-login>
ssh-import-id-gh (pakiet ssh-import-id w Ubuntu/Debian) pobiera https://github.com/<login>.keys i dopisuje każdy klucz do ~/.ssh/authorized_keys z komentarzem # ssh-import-id gh:<login>. Operacja jest idempotentna: ponowne uruchomienie nie duplikuje wpisów.
Dla kompletności: istnieje też ssh-import-id-lp dla Launchpada (lp:<login>) i ogólny ssh-import-id z prefiksem.
Regeneracja pliku handle
Przy nowym hoście ze świeżym ~/.ssh/, resident keys nadal rezydują na YubiKey. Po podłączeniu urządzenia wszystkie handle można odtworzyć jednym poleceniem. ssh-keygen -K tworzy plik handle + .pub dla każdego resident key (PIN podawany jest raz, touch wymagany dla każdego klucza). Bez fizycznego YubiKey i PIN-u procedura jest niewykonalna.
cd ~/.ssh
ssh-keygen -K
Konfiguracja klienta
Ten krok jest opcjonalny przy jednym kluczu na YubiKey. W takim przypadku ssh user@host zadziała bez dodatkowej konfiguracji.
Należy wygenerować osobne handle z różnymi etykietami i utworzyć ~/.ssh/config. Pole IdentitiesOnly yes jest istotne: bez niego SSH proponuje kolejno wszystkie klucze z ~/.ssh/, co przy YubiKey skutkuje wielokrotnym dotykaniem i wpisywaniem PIN-u przed trafieniem we właściwy klucz.
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 jest wymagane do logowania YubiKey-em, to wgranie klucza publicznego 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) przy założeniu, że logowanie odbywa się wyłącznie kluczem sprzętowym.
Po wdrożeniu logowania kluczem sprzętowym zaleca się wyłączenie pozostałych metod uwierzytelniania. /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
PIN + touch można wymusić 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, jednak należy go 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(opisowa etykieta identyfikująca klucz). - Key type: Authentication Key.
- Key: zawartość
~/.ssh/id_ed25519_personal.pub.
Procedurę należy powtórzyć z Key type: Signing Key (ten sam klucz publiczny, inna rola). Bez tego kroku git log --show-signature nie zweryfikuje podpisów.
Podpisywanie commitów kluczem SSH
Od Git 2.34 klucz SSH może zastąpić GPG do podpisywania commitów. Konfiguracja Git lokalnie:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_personal.pub
git config --global commit.gpgsign true
Od tej pory każdy commit jest podpisany kluczem z YubiKey. Przy git commit urządzenie oczekuje na dotyk (analogicznie jak przy ssh), a w GitHubie commit otrzymuje zieloną plakietkę Verified.
Windows
Na Windowsie 10/11 stosuje się natywny OpenSSH (≥ 8.9). Cały workflow opisany w tym poście — ssh-keygen -t ed25519-sk, ~/.ssh/config, ssh-keygen -K — działa w PowerShellu bez modyfikacji. Jedyna istotna różnica: operacje zarządzające resident keys wymagają uprawnień administratora.
Ścieżki i prompt
Plik konfiguracyjny leży w C:\Users\<username>\.ssh\config, składnia jest identyczna jak na Linuksie. W wpisach można nadal używać ~/.ssh/..., ponieważ OpenSSH na Windowsie rozumie tyldę. Prompt PIN-u i Confirm user presence wyświetlają się jako natywne okno Windows zamiast w terminalu, bez różnicy funkcjonalnej.
Operacje na resident keys wymagają admina
ssh-keygen -K (regeneracja handle) i ykman fido credentials list/delete wymagają elevated PowerShella. Codzienne ssh user@host i git push działają bez podwyższonych uprawnień.
Dlaczego admin? Te polecenia używają niskopoziomowego CTAP2 over raw HID, który Windows blokuje bez elevation. WebAuthn API (stosowane przy zwykłym logowaniu SSH) nie udostępnia operacji typu „wylistuj/pobierz resident keys", dlatego libfido2 musi korzystać z kanału wymagającego admina. Objaw braku elevation: ssh-keygen -K zwraca mylący błąd Unable to load resident keys: invalid format.
W praktyce wygodne jest zainstalowanie gsudo (winget install gerardog.gsudo) i wywoływanie gsudo ssh-keygen -K ze zwykłego okna, zamiast otwierać osobny terminal jako Administrator.