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
Grupaplugdevgroups
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 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.

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 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.

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 (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.