YubiKey — OpenPGP, agent SSH & signature Git
J’utilise deux YubiKeys : une au quotidien, une en secours dans un tiroir fermé. Elles me servent pour du FIDO2/WebAuthn (connexion aux sites), du OpenPGP (signature Git, chiffrement) et comme clé SSH (le tout via gpg-agent).
Voici la marche à suivre complète, de la sortie du blister jusqu’aux commits signés.
Les clés privées OpenPGP générées sur la YubiKey ne peuvent jamais en ressortir. C’est tout l’intérêt, mais cela veut dire qu’une clé perdue ou cassée est une identité perdue. D’où le fait qu’il faut avoir deux clés avant de générer quoi que ce soit !
Réception et prise en main
Yubico expédie ses clés dans un blister scellé. Avant toute chose, vérifier qu’il n’a pas été ouvert, puis contrôler l’authenticité de la clé sur yubico.com/genuine (le site interroge l’attestation matérielle de la clé via WebAuthn).
Toute la configuration se fait avec YubiKey Manager CLI (ykman).
# Fedora / RHEL dnf install yubikey-manager # Debian / Ubuntu apt install yubikey-manager- Installe
ykman.
- Installe
ykman list- Liste les clés branchées avec leur numéro de série. À noter quelque part : c’est ce numéro qui permet de savoir laquelle des deux clés est insérée. Il est également inscrit physiquement sur la YubiKey.
ykman info- Affiche le modèle, la version de firmware et les applets activés sur chaque interface (USB / NFC).
- Il faut au minimum le firmware 5.2.3 pour utiliser Ed25519 / Curve25519 en OpenPGP. En dessous, on est limité au RSA.
ykman config usb --list ykman config usb --disable OTP --force- Désactive l’applet OTP. Sans ça, un appui involontaire sur la clé tape une longue chaîne
cccccc…dans le champ actif — dans un terminal ou un formulaire de mot de passe, c’est pénible. - Applets disponibles :
OTP,U2F,FIDO2,OATH,PIV,OPENPGP,HSMAUTH. Ne garder que ce qu’on utilise. - La clé redémarre après la commande : la débrancher / rebrancher.
- Désactive l’applet OTP. Sans ça, un appui involontaire sur la clé tape une longue chaîne
Réinitialiser l’applet OpenPGP
Sur une clé neuve comme sur une clé récupérée, on repart d’un applet OpenPGP propre.
ykman openpgp info- État actuel : version de l’applet, compteurs d’essais restants, politiques de touch, présence de clés.
ykman openpgp reset- Remet l’applet OpenPGP à zéro. Détruit irrémédiablement les clés qui s’y trouvent et remet les PIN par défaut. N’affecte pas les autres applets (FIDO2, PIV, OATH).
Après un reset, les valeurs d’usine sont :
| Secret | Valeur par défaut | Longueur mini | Rôle |
|---|---|---|---|
| User PIN | 123456 | 6 caractères | Usage quotidien : signer, déchiffrer, s’authentifier |
| Admin PIN | 12345678 | 8 caractères | Administration : générer des clés, changer les politiques, débloquer le User PIN |
| Reset Code | (non défini) | 8 caractères | Optionnel : débloque le User PIN sans connaître l’Admin PIN |
PIN, KDF et compteurs d’essais
L’ordre des opérations compte : kdf-setup et set-retries réinitialisent les PIN sur certains firmwares. On les fait avant de définir ses propres secrets.
gpg --card-edit gpg/card> admin gpg/card> kdf-setup gpg/card> quit- Active le Key Derived Format : le PIN est haché côté machine avant d’être envoyé à la clé, il ne transite donc jamais en clair sur le bus USB.
- À faire immédiatement après le reset, sur un applet vierge.
ykman openpgp access set-retries 5 5 5- Passe les compteurs d’essais à 5 pour le User PIN, le Reset Code et l’Admin PIN (3 par défaut). Demande l’Admin PIN.
ykman openpgp access change-pin ykman openpgp access change-admin-pin ykman openpgp access change-reset-codeDéfinit les trois secrets. Ou, de façon équivalente, en interactif :
gpg --card-edit gpg/card> admin gpg/card> passwd # 1 = User PIN, 2 = débloquer le User PIN, 3 = Admin PIN, 4 = Reset CodeLa YubiKey accepte jusqu’à 127 caractères : autant utiliser une vraie phrase de passe plutôt qu’un code numérique, d’autant que
gpg-agentla met en cache.
Si le User PIN est bloqué, on le débloque avec l’Admin PIN (
passwd→2) ou le Reset Code. Si l’Admin PIN est bloqué, il n’y a aucun recours : seulykman openpgp resetremet la clé en service, et il détruit les clés OpenPGP au passage.
Politique de présence (touch)
Par défaut, dès que le PIN est en cache, la clé signe et déchiffre sans aucune action physique : un malware sur la machine peut s’en servir silencieusement. La politique de touch corrige ça en exigeant un contact sur le disque doré.
| Politique | Comportement |
|---|---|
off | Aucun contact requis (défaut) |
on | Contact requis à chaque opération |
cached | Contact requis, puis mis en cache 15 secondes par la clé |
fixed | Comme on, mais irréversible sans reset de l’applet |
cached-fixed | Comme cached, mais irréversible sans reset de l’applet |
ykman openpgp keys set-touch sig cached ykman openpgp keys set-touch enc cached ykman openpgp keys set-touch aut cached ykman openpgp keys set-touch att cached- Applique la politique aux quatre emplacements :
sig(signature),enc(chiffrement),aut(authentification SSH) etatt(attestation). Demande l’Admin PIN. cachedest le bon compromis au quotidien : ungit rebasequi signe dix commits ne demande qu’un seul contact, mais rien ne se signe pendant qu’on est parti chercher un café.- Sur les versions de
ykmanantérieures à la 5, la commande s’écritykman openpgp set-touch sig cached.
- Applique la politique aux quatre emplacements :
Ne jamais utiliser
fixedoucached-fixedsans en mesurer la portée : la politique ne peut plus jamais être assouplie, y compris par soi-même. Le seul retour en arrière estykman openpgp reset, qui efface les clés.
gpg --card-edit gpg/card> admin gpg/card> forcesig- Bascule le drapeau Signature PIN entre
forced(la clé réclame le PIN à chaque signature, valeur par défaut) etnot forced. Le laisser surforced: le cache degpg-agentrend la contrainte invisible à l’usage.
- Bascule le drapeau Signature PIN entre
Générer l’identité OpenPGP sur la clé
C’est l’approche que je retiens : les trois clés sont générées par le processeur de la YubiKey et n’en sortent jamais.
Avec cette méthode, la clé de signature devient la clé maître (celle qui certifie les identités). Elle vit sur la YubiKey : sans elle, impossible d’ajouter une adresse e-mail, de signer une autre clé ou de prolonger une date d’expiration.
gpg --card-edit gpg/card> admin gpg/card> name # DUPONT # Jean gpg/card> lang # fr gpg/card> url # https://github.com/jeandupont.gpg- Renseigne les métadonnées du porteur. Le champ
urlest le plus utile : il indique où récupérer la clé publique, ce qui permet plus tard unfetchen une commande sur n’importe quelle machine.
- Renseigne les métadonnées du porteur. Le champ
gpg/card> key-attr # Signature key → (2) ECC → (1) Curve 25519 # Encryption key → (2) ECC → (1) Curve 25519 # Authentication → (2) ECC → (1) Curve 25519- Choisit l’algorithme des trois emplacements avant génération. Curve 25519 donne Ed25519 pour la signature et l’authentification, et Curve25519 (cv25519) pour le chiffrement : plus rapide et bien plus court que du RSA 4096, pour une sécurité au moins équivalente.
- Rester sur RSA 4096 si le firmware est antérieur à 5.2.3, ou si l’on doit interopérer avec des outils anciens.
gpg/card> generate # Sauvegarde hors carte de la clé de chiffrement ? → n # La clé est-elle valable jusqu'à ? → 0 (pas d'expiration) # Nom réel : Jean DUPONT # Adresse e-mail : jean.dupont@folf.fr # Commentaire : YubiKey Principale de Jean DUPONT !! gpg/card> quit- Génère les trois paires de clés sur la puce. Compter une à deux minutes en RSA 4096, quelques secondes en Curve 25519. Demande l’Admin PIN puis le User PIN.
- Répondre
nà la sauvegarde hors carte : la YubiKey ne laissera de toute façon pas sortir la clé. - Mettre le numéro de série de la clé dans le commentaire évite bien des hésitations quand on jongle entre deux YubiKeys.
gpg --card-status- Vérifie le résultat : empreintes des trois clés, politiques de touch, compteur de signatures, numéro de série. C’est aussi cette commande qui crée les stubs (références locales vers la carte) dans le trousseau.
Gérer plusieurs adresses e-mail
Une identité OpenPGP peut porter autant d’UID que nécessaire — pro, perso, alias — tous couverts par la même clé.
gpg --edit-key jean.dupont@folf.fr gpg> adduid # Nom réel : Jean DUPONT # Adresse e-mail : jean.dupont@email-pro.com gpg> adduid # Adresse e-mail : jean.dupont@email-associatif.be gpg> save- Ajoute des identités. Chaque
adduidest certifié par la clé maître : la YubiKey doit être branchée, elle demandera le User PIN et un contact.
- Ajoute des identités. Chaque
gpg --edit-key jean.dupont@folf.fr gpg> uid 2 gpg> primary gpg> save- Désigne l’UID n° 2 comme identité principale (celle affichée par défaut par les outils).
uid 2sélectionne,uid 2à nouveau désélectionne.
- Désigne l’UID n° 2 comme identité principale (celle affichée par défaut par les outils).
gpg --edit-key jean.dupont@email-pro.com gpg> uid 3 gpg> revuid gpg> save- Révoque un UID devenu obsolète (changement d’employeur, domaine abandonné). On ne supprime pas un UID déjà publié — on le révoque, pour que les personnes ayant la clé sachent qu’il ne faut plus l’utiliser.
Certificat de révocation
À faire le jour même de la génération, tant que la clé est disponible.
gpg --output revocation-0xABCDEF.asc --gen-revoke 0xABCDEF0123456789- Produit un certificat qui invalidera publiquement la clé. Choisir le motif
1(clé compromise) ou3(clé plus utilisée) — un certificat par motif utile, tant qu’à faire. - Sans ce fichier, une clé perdue reste indéfiniment valide aux yeux du monde.
- Produit un certificat qui invalidera publiquement la clé. Choisir le motif
Ce fichier permet à quiconque le détient de tuer l’identité. Il se stocke hors ligne : clé USB chiffrée dans un coffre, ou imprimé sur papier (
paperkey, ou un QR code).
Deux YubiKeys, deux identités croisées
Puisque chaque clé génère sa propre identité, il faut que les deux se reconnaissent mutuellement : une signature croisée permet à qui fait confiance à l’une de faire confiance à l’autre.
# YubiKey A branchée gpg --armor --export 0xAAAA1111 > cle-a.asc # YubiKey B branchée gpg --card-status gpg --armor --export 0xBBBB2222 > cle-b.asc- Exporte les deux clés publiques. Le
gpg --card-statusaprès changement de clé crée les stubs de la nouvelle carte.
- Exporte les deux clés publiques. Le
# YubiKey A branchée gpg -u 0xAAAA1111 --sign-key 0xBBBB2222 # YubiKey B branchée gpg -u 0xBBBB2222 --sign-key 0xAAAA1111- Chaque clé certifie l’autre.
-uforce la clé qui signe, ce qui est indispensable quand le trousseau en contient plusieurs.
- Chaque clé certifie l’autre.
gpg --armor --export 0xAAAA1111 0xBBBB2222 > cles-publiques.asc- Agrège les deux clés publiques, signatures croisées comprises, dans un seul fichier à distribuer.
Exporter et publier son identité
gpg --list-secret-keys --keyid-format=long --with-subkey-fingerprints- Affiche les empreintes complètes des clés et sous-clés. Le
>devantssbsignale une sous-clé qui vit sur une carte. C’est ici qu’on récupère l’identifiant de la sous-clé de signature pour Git.
- Affiche les empreintes complètes des clés et sous-clés. Le
gpg --armor --export 0xAAAA1111 > cle-publique.asc- Exporte la clé publique au format texte. C’est le seul fichier à diffuser librement.
gpg --export-ssh-key 0xAAAA1111- Sort la sous-clé d’authentification au format OpenSSH, prête à coller dans un
~/.ssh/authorized_keysou dans GitHub. Ajouter un!après l’identifiant (0xCCCC3333!) pour désigner une sous-clé précise.
- Sort la sous-clé d’authentification au format OpenSSH, prête à coller dans un
gpg --export-ownertrust > ownertrust.txt- Exporte la base de confiance. Utile pour rejouer à l’identique sur une autre machine, sans repasser par le menu
trust.
- Exporte la base de confiance. Utile pour rejouer à l’identique sur une autre machine, sans repasser par le menu
gpg --keyserver hkps://keys.openpgp.org --send-keys 0xAAAA1111- Publie sur un serveur de clés.
keys.openpgp.orgdemande une validation par e-mail pour chaque UID, et retire les signatures tierces : les signatures croisées n’y survivront pas. D’où l’intérêt de diffuser aussi le fichier agrégé.
- Publie sur un serveur de clés.
Je publie surtout mes clés via GitHub, qui les expose automatiquement :
curl https://github.com/jeandupont.gpg # clés OpenPGP curl https://github.com/jeandupont.keys # clés SSH publiques- GitHub sert les clés déclarées dans Settings → SSH and GPG keys. C’est cette URL que je mets dans le champ
urlde la carte, ce qui rend le déploiement sur une nouvelle machine trivial.
- GitHub sert les clés déclarées dans Settings → SSH and GPG keys. C’est cette URL que je mets dans le champ
Redéployer son identité sur une nouvelle machine
Le scénario du quotidien : machine fraîche, YubiKey en poche.
gpg -k- Crée l’arborescence
~/.gnupgavec les fichiers par défaut. Sans ça, les commandes suivantes râlent.
- Crée l’arborescence
gpg --import cle-publique.ascImporte l’identité publique. Sans clé publique dans le trousseau,
gpgne saura pas quoi faire de la carte : c’est bien la clé publique qui arrive en premier.Alternative sans transfert de fichier, si le champ
urla été renseigné sur la carte :gpg --card-edit gpg/card> fetch- Télécharge la clé publique depuis l’URL stockée sur la YubiKey.
gpg --edit-key jean.dupont@folf.fr gpg> trust # 5 = confiance ultime # y gpg> quitDéclare la clé comme étant la sienne. Sans confiance ultime,
gpgaffiche un avertissement à chaque signature et Git refuse de considérer les commits comme vérifiés.Version scriptable, sans menu interactif :
echo "AAAA1111BBBB2222CCCC3333DDDD4444EEEE5555:6:" | gpg --import-ownertrust- Prend l’empreinte complète (40 caractères). Le
6correspond à la confiance ultime dans le format du fichier — c’est bien le5du menu interactif, la numérotation diffère.
- Prend l’empreinte complète (40 caractères). Le
gpg --card-status- YubiKey branchée, crée les stubs de clés secrètes qui pointent vers la carte. À partir d’ici,
gpg --list-secret-keysmontre l’identité complète.
- YubiKey branchée, crée les stubs de clés secrètes qui pointent vers la carte. À partir d’ici,
Configurer gpg-agent (et l’agent SSH)
gpg-agent sait se faire passer pour un agent SSH. Une seule brique gère alors le PIN, le cache et les deux protocoles.
~/.gnupg/gpg-agent.conf pinentry-program /usr/bin/pinentry-qt enable-ssh-support default-cache-ttl 28800 max-cache-ttl 28800 default-cache-ttl-ssh 28800 max-cache-ttl-ssh 28800pinentry-qtsous KDE Plasma ;pinentry-gnome3sous GNOME,pinentry-cursesen TTY pur.enable-ssh-supportexpose la socketS.gpg-agent.ssh.- Les TTL sont en secondes : 28800 = 8 heures, soit une journée de travail avec une seule saisie de PIN.
max-cache-ttlplafonne le cache même en cas d’usage continu, il doit donc être au moins égal àdefault-cache-ttl.
gpgconf --kill gpg-agent gpgconf --launch gpg-agent- Applique la configuration.
gpgconf --reload gpg-agentsuffit pour la plupart des options, mais pas pourenable-ssh-supportqui exige un vrai redémarrage.
- Applique la configuration.
mkdir -p ~/.config/environment.d~/.config/environment.d/10-gpg-ssh.conf SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/gnupg/S.gpg-agent.ssh- Déclare la socket auprès du gestionnaire de session
systemd --user. C’est ce qui rend la variable disponible aux applications graphiques lancées depuis le menu (VS Code, IDE JetBrains, clients Git graphiques) et pas seulement aux terminaux. environment.dne fait pas de substitution de commande :$(gpgconf …)n’y fonctionnerait pas, seule l’expansion de variables déjà définies comme${XDG_RUNTIME_DIR}est supportée.
- Déclare la socket auprès du gestionnaire de session
mkdir -p ~/.config/plasma-workspace/env~/.config/plasma-workspace/env/10-gpg-agent.sh #!/bin/sh export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" gpgconf --launch gpg-agentchmod +x ~/.config/plasma-workspace/env/10-gpg-agent.sh- Filet de sécurité pour Plasma, qui exécute ces scripts au démarrage de la session. Redondant avec
environment.dsur une session Plasma démarrée par systemd, mais inoffensif et utile si la session bascule sur le mode de démarrage historique.
- Filet de sécurité pour Plasma, qui exécute ces scripts au démarrage de la session. Redondant avec
~/.bashrc export GPG_TTY=$(tty) if [ -z "$SSH_CONNECTION" ]; then export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" fi gpg-connect-agent updatestartuptty /bye >/dev/nullGPG_TTYindique àpinentrysur quel terminal afficher la demande de PIN. Indispensable en console.- Le garde
SSH_CONNECTIONévite d’écraser l’agent transféré parssh -A: sans lui, se connecter à un serveur distant casserait le rebond par agent forwarding. updatestartupttyrecale l’agent déjà lancé sur le terminal courant. Sans ça, le second terminal d’une session hérite du TTY du premier et le prompt de PIN part dans le vide.
Après quoi, on redémarre pour que tout soit correctement appliqué.
Utiliser sa YubiKey en SSH
ssh-add -L- Liste les clés proposées par l’agent. La sous-clé d’authentification de la carte insérée doit apparaître, suivie d’un commentaire
cardno:….gpg-agentexpose automatiquement les clés d’authentification des cartes, sans passer par~/.gnupg/sshcontrol.
- Liste les clés proposées par l’agent. La sous-clé d’authentification de la carte insérée doit apparaître, suivie d’un commentaire
ssh-add -L | ssh utilisateur@serveur 'cat >> ~/.ssh/authorized_keys'- Autorise la clé sur un serveur distant.
ssh-copy-id utilisateur@serveurfait la même chose en allant chercher la clé publique dans l’agent — à condition d’y avoir déjà accès autrement (mot de passe, autre clé).
- Autorise la clé sur un serveur distant.
ssh -T git@github.com- Vérifie l’accès Git par SSH. La YubiKey clignote : contact requis.
gpg --export-ssh-key 0xAAAA1111 > ~/.ssh/yubikey-a.pub~/.ssh/config Host github.com IdentitiesOnly yes IdentityFile ~/.ssh/yubikey-a.pub- À faire dès que le trousseau SSH contient déjà plusieurs clés : sans ça, le serveur peut couper la connexion (
Too many authentication failures) avant d’arriver à celle de la YubiKey. IdentityFileaccepte ici une clé publique :sshs’en sert uniquement pour désigner, parmi celles de l’agent, la seule identité à proposer.
- À faire dès que le trousseau SSH contient déjà plusieurs clés : sans ça, le serveur peut couper la connexion (
Signer ses commits Git
gpg --list-secret-keys --keyid-format=long --with-subkey-fingerprints- Récupère l’identifiant de la sous-clé portant la capacité
[S]— c’est elle qui signe, pas la clé maître.
- Récupère l’identifiant de la sous-clé portant la capacité
git config --global user.name "Jean DUPONT" git config --global user.email "jean.dupont@folf.fr" git config --global user.signingkey '0xCCCC3333DDDD4444!' git config --global commit.gpgsign true git config --global tag.gpgSign true git config --global gpg.format openpgp git config --global gpg.program /usr/bin/gpg- Le
!final épingle exactement cette sous-clé : sans lui,gpgchoisit lui-même une sous-clé de signature, ce qui devient ambigu avec plusieurs cartes. Les guillemets simples sont obligatoires, sinon Bash interprète le!comme une expansion d’historique. gpg.format openpgpest la valeur par défaut, mais l’expliciter évite les surprises si la configuration a un jour basculé surssh.- L’adresse de
user.emaildoit correspondre à un UID de la clé, sinon GitHub et consorts affichent la signature comme non vérifiée.
- Le
git commit --allow-empty -m "test signature" git log --show-signature -1- Vérifie la chaîne complète. La sortie doit indiquer
Good signature from "Jean DUPONT <…>"sans avertissement de confiance.
- Vérifie la chaîne complète. La sortie doit indiquer
Deux identités selon le dépôt
Avec une clé pro et une clé perso, autant laisser Git choisir en fonction du chemin du dépôt.
~/.gitconfig [user] name = Jean DUPONT [commit] gpgsign = true [tag] gpgSign = true [gpg] program = /usr/bin/gpg [includeIf "gitdir:~/Documents/GitHub/Perso/"] path = ~/.gitconfig-perso [includeIf "gitdir:~/Documents/GitHub/Pro/"] path = ~/.gitconfig-pro~/.gitconfig-perso [user] email = jean.dupont@folf.fr signingkey = 0xAAAA1111BBBB2222!includeIfcharge le bon couple e-mail / clé selon l’emplacement du dépôt. Le slash final dugitdir:est significatif : il fait porter la règle sur tout le sous-arbre.
FIDO2 / WebAuthn
L’applet FIDO2 est indépendant d’OpenPGP : PIN distinct, reset distinct.
ykman fido info- État de l’applet : PIN défini ou non, essais restants, nombre de clés résidentes stockées.
ykman fido access change-pin- Définit ou change le PIN FIDO2 (4 caractères minimum, 63 maximum). Requis pour les passkeys (clés résidentes) et pour toute authentification à deux facteurs de la clé elle-même.
ykman fido credentials list ykman fido credentials delete <credential-id>- Liste et supprime les passkeys stockées sur la clé. L’espace est limité (25 à 100 selon le firmware) : le ménage se fait à la main.
ykman fido reset- Remet l’applet FIDO2 à zéro et invalide toutes les passkeys enregistrées. À ne faire qu’après avoir enregistré la seconde clé sur les services concernés.
Le PIN FIDO2 tolère 8 essais au total, avec un débranchement / rebranchement imposé toutes les 3 tentatives échouées. Au huitième échec, l’applet se verrouille et seul un
ykman fido resetle débloque — au prix de toutes les passkeys.
Le vrai réflexe : enregistrer les deux YubiKeys sur chaque service, au moment de l’inscription. Rajouter la seconde clé après avoir perdu la première n’est plus possible.