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_keys na 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

Wymagania wstępne dla FIDO2 + ed25519-sk.
KomponentWymaganieJak sprawdzić
YubiKeyseria 5 z firmware ≥ 5.2.3ykman info
ykmanzainstalowanyykman --version
libfido2zainstalowanapkg-config --modversion libfido2 (Linux)
na Windows wbudowana w OpenSSH
OpenSSH (client)≥ 8.2ssh -V
OpenSSH (serwer)≥ 8.2; ≥ 8.4 dla verify-requiredssh -V
Grupayplugdevgroups
PIN FIDO2ustawiony na YubiKeyykman fido infoPIN 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.

Flagi ssh-keygen przy generowaniu resident key FIDO2.
FlagaZnaczenie
-t ed25519-sktyp klucza FIDO2 oparty na Curve25519; działa tylko na firmware'ze ≥ 5.2.3
-O residentklucz przechowywany w całości na YubiKey
-O application=ssh:personaletykieta klucza na urządzeniu (konwencja Yubico: ssh:<nazwa>)
-O verify-requiredklucz 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ć.

Warianty zabezpieczenia klucza FIDO2.
WariantFlagi przy ssh-keygenRyzyka
PIN + touch-O resident -O verify-requiredpełna ochrona
Tylko touch-O residentkradzież YubiKey = logowanie bez przeszkód
Tylko PIN-O resident -O verify-required -O no-touch-requiredmalware na laptopie może podpisywać challenge, bez twojej wiedzy
Nic-O resident -O no-touch-requiredYubiKey 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.

Role klucza SSH w GitHubie.
RolaZastosowanie
Authentication keyużywany przez git push, git clone git@github.com:... — klasyczny klucz SSH
Signing keyuż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.