Skip to content

Ajoute l'authentification par clé SSH (machine cliente et serveur) - #3

Merged
NicoBOD merged 2 commits into
mainfrom
claude/ssh-key-generation-z33juw
Aug 1, 2026
Merged

Ajoute l'authentification par clé SSH (machine cliente et serveur)#3
NicoBOD merged 2 commits into
mainfrom
claude/ssh-key-generation-z33juw

Conversation

@NicoBOD

@NicoBOD NicoBOD commented Aug 1, 2026

Copy link
Copy Markdown
Member

Pourquoi

Le script sécurisait SSH (port, PermitRootLogin) mais laissait le mot de passe comme seul moyen de connexion. Une nouvelle étape 8/8 met en place l'authentification par clé, dans un sens ou dans l'autre selon le rôle de la machine — l'objectif étant de pouvoir se connecter avec une clé privée dès la fin de l'exécution.

L'étape démarre par un menu : Serveur / Client / Les deux (poste rebond) / Ignorer / Explication. Le reste du script est inchangé, à la renumérotation près des bannières x/7x/8.

Machine cliente — génération d'une paire de clés

  • Compte propriétaire, type (ed25519 recommandé, rsa 4096, ecdsa 521, ed25519-sk pour un jeton FIDO2), emplacement, nom du fichier et commentaire au choix, tous validés.
  • La phrase de passe est demandée par ssh-keygen lui-même, jamais passée via -N : la ligne de commande d'un processus est lisible de tous par ps.
  • Génération sous l'identité du compte via runuser (droits corrects dès la création), avec repli en root suivi d'un chown ; repli proposé sur ed25519 si aucun jeton FIDO2 ne répond.
  • Aucune clé existante n'est écrasée en silence : autre nom, ou sauvegarde préalable.
  • Options : bloc idempotent dans ~/.ssh/config (Host / IdentityFile / IdentitiesOnly / AddKeysToAgent), envoi par ssh-copy-id puis test de connexion réel.

Serveur — dépôt d'une clé publique

  • Provenance au choix : collage, fichier local, URL https, compte GitHub (/<login>.keys), clé générée à l'instant, ou aide affichant les commandes à lancer sur le poste client.
  • Validation : type reconnu (les clés DSA sont écartées, OpenSSH les refuse depuis la v7), contrôle croisé entre le type annoncé et l'entête base64 du blob, refus explicite d'une clé privée collée par erreur, retrait des retours chariot d'un copier-coller Windows.
  • Installation idempotente (comparaison sur le corps de la clé, pas sur la ligne : un commentaire différent ne crée plus de doublon), saut de ligne final garanti (sans quoi la clé suivante se colle à la précédente et les invalide toutes les deux), droits 700/600 et propriétaire corrigés.
  • Détection de StrictModes — un home ou un .ssh accessible en écriture au groupe fait ignorer la clé sans aucun message — et vérification de l'AuthorizedKeysFile réellement en vigueur.
  • Test de connexion en boucle locale, seule preuve obtenable depuis le serveur.

Durcissement et garde-fou anti-lockout

PubkeyAuthentication yes est posé systématiquement. La coupure du mot de passe (PasswordAuthentication et KbdInteractiveAuthentication, sans quoi elle serait illusoire sur Debian) n'est proposée qu'après une connexion prouvée ou une confirmation explicite, et le résultat est vérifié par sshd -T : si un fichier de sshd_config.d/ numéroté avant le nôtre l'emporte, il est nommé au lieu d'annoncer un succès qui n'a pas eu lieu.

Sur le modèle éprouvé de ip-fixe-confirmer, ssh-cles-rollback et ssh-cles-confirmer sont installés dans /usr/local/sbin, et une minuterie systemd-run réactive le mot de passe faute de confirmation dans le délai choisi.

Ajoute aussi backup_file_once, pour qu'une seconde sauvegarde du même fichier sshd n'écrase pas la copie d'origine par la version déjà modifiée.

Tests

30 nouveaux cas sur les fonctions pures (138 au total, 0 échec), fondés sur de vraies clés publiques plutôt que sur des chaînes inventées.

Vérifications passées localement (celles de qualite.yml) : bash -n, shellcheck -s bash -e SC2317, tests/test-fonctions.sh, bit exécutable.

Tests fonctionnels réels menés avec un sshd de test :

Scénario Résultat
Génération ed25519 et rsa 700/600/644, bon propriétaire, relecture par ssh-keygen
Dépôt puis second dépôt « 0 ajoutée, 1 déjà présente » — fichier toujours à une ligne
Connexion par clé Réussie (Authenticated using "publickey")
Clé non autorisée Refusée, diagnostic affiché
~/.ssh/config Bloc unique après deux écritures, 600, relu par ssh -G
StrictModes .ssh en 707 détecté et corrigé
Anti-écrasement, URL non-https, clé invalide, clé privée collée Tous refusés proprement
PasswordAuthentication no Confirmé par sshd -T
ssh-cles-rollback Réactive le mot de passe si non confirmé, ne fait rien après ssh-cles-confirmer

Restent à valider sur une vraie Debian 13 (pas de systemd dans l'environnement de test) : l'armement de la minuterie systemd-run et le comportement en activation par ssh.socket.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PbhHGjLvttKAauNJjPJMuq


Generated by Claude Code

Le script sécurisait SSH (port, PermitRootLogin) mais laissait le mot de
passe comme seul moyen de connexion. Une nouvelle étape 8 met en place
l'authentification par clé, dans un sens ou dans l'autre selon le rôle
de la machine.

Machine cliente — génération d'une paire de clés :
- compte propriétaire, type (ed25519, rsa 4096, ecdsa 521, ed25519-sk),
  emplacement, nom de fichier et commentaire au choix, tous validés ;
- la phrase de passe est demandée par ssh-keygen lui-même et jamais
  passée via -N : la ligne de commande d'un processus est lisible de
  tous par ps ;
- génération sous l'identité du compte via runuser, avec repli en root
  suivi d'un chown ; repli proposé sur ed25519 si aucun jeton FIDO2 ne
  répond ;
- aucune clé existante n'est écrasée en silence : autre nom, ou
  sauvegarde préalable ;
- options : bloc idempotent dans ~/.ssh/config, envoi par ssh-copy-id
  puis test de connexion réel.

Serveur — dépôt d'une clé publique :
- provenance au choix : collage, fichier, URL https, compte GitHub, clé
  générée à l'instant, ou aide à la création côté poste client ;
- validation du type (les clés DSA sont écartées), contrôle croisé entre
  le type annoncé et l'entête base64 du blob, refus d'une clé privée
  collée par erreur, retraits des retours chariot Windows ;
- installation idempotente (comparaison sur le corps de la clé, pas sur
  la ligne), saut de ligne final garanti, droits 700/600 et propriétaire
  corrigés ;
- détection de StrictModes, qui fait ignorer la clé sans aucun message,
  et vérification de l'AuthorizedKeysFile réellement en vigueur ;
- test de connexion en boucle locale, seule preuve possible depuis le
  serveur.

Durcissement et garde-fou anti-lockout :
- PubkeyAuthentication yes systématique ; la coupure du mot de passe
  (PasswordAuthentication et KbdInteractiveAuthentication) n'est
  proposée qu'après une connexion prouvée ou une confirmation explicite,
  et le résultat est vérifié par sshd -T ;
- si un fichier de sshd_config.d numéroté avant le nôtre l'emporte, il
  est nommé au lieu d'annoncer un succès qui n'a pas eu lieu ;
- sur le modèle de ip-fixe-confirmer, ssh-cles-rollback et
  ssh-cles-confirmer sont installés et une minuterie systemd réactive le
  mot de passe faute de confirmation.

Ajoute aussi backup_file_once, pour qu'une seconde sauvegarde du même
fichier sshd n'écrase pas la copie d'origine par la version modifiée.

Tests : 30 nouveaux cas sur les fonctions pures (138 au total), fondés
sur de vraies clés publiques.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbhHGjLvttKAauNJjPJMuq
Copilot AI review requested due to automatic review settings August 1, 2026 22:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ce PR étend le script de personnalisation Debian 13 en ajoutant une 8ᵉ étape dédiée à l’authentification SSH par clé, couvrant à la fois le rôle client (génération de paire de clés) et le rôle serveur (dépôt de clé publique dans authorized_keys, puis durcissement optionnel avec garde-fou anti-lockout). Il s’inscrit dans la continuité de l’étape 7 (durcissement SSH) et complète la sécurisation en réduisant la dépendance au mot de passe.

Changes:

  • Ajout d’une section complète “Authentification par clé SSH” (validateurs, collecte/installation idempotente de clés, test de connexion, rollback via systemd-run).
  • Ajout de tests unitaires Bash pour les nouvelles fonctions “pures” liées aux clés SSH et à la validation.
  • Mise à jour de la documentation (README) pour décrire l’étape 8 et ses options.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
script-de-personnalisation-post-installation-pour-debian-13.sh Ajoute l’étape 8 (génération/dépôt de clés, durcissement, rollback) et les fonctions utilitaires associées.
tests/test-fonctions.sh Ajoute des tests couvrant validation de ports, reconnaissance/validation de clés publiques et helpers authorized_keys.
README.md Documente l’étape 8 et ses scénarios (client/serveur/les deux), ainsi que le garde-fou anti-lockout.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

command -v base64 >/dev/null 2>&1 || return 1
n=${#type}
(( n > 0 && n < 256 )) || return 1
b64="$(printf '%b%s' "\\0000\\0000\\0000\\0$(printf '%03o' "$n")" "$type" | base64 2>/dev/null | tr -d '\n')" || return 1

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faux positif : la remarque confond deux syntaxes d'échappement octal. Dans une chaîne de format printf, l'octal s'écrit \nnn (3 chiffres max) — c'est bien la limite évoquée. Mais ici l'octal est passé à %b, qui utilise la forme \0nnn : un 0 d'introduction suivi de jusqu'à trois chiffres octaux. \0000 est donc \0 + 000, soit un unique octet NUL, et non « NUL + caractère 0 ».

Vérification sur les octets réellement produits :

$ printf '%b%s' "\0000\0000\0000\0$(printf '%03o' 11)" "ssh-ed25519" | od -An -tx1
 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39

$ awk '{print $2}' cle.pub | base64 -d | head -c 15 | od -An -tx1
 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39

Octet pour octet identique au blob d'une vraie clé, sans 0x30 parasite.

Le cas est par ailleurs déjà verrouillé par les tests, sur de vraies clés publiques et non des chaînes inventées : tests/test-fonctions.sh compare le préfixe calculé aux valeurs attendues pour trois types (AAAAC3NzaC1lZDI1NTE5, AAAAB3NzaC1y, AAAAE2VjZHNhLXNoYTItbmlzdHA1), et le cas ko "type rsa sur un corps ed25519" échouerait précisément si le préfixe était faussé — le contrôle croisé laisserait alors passer n'importe quelle incohérence type ↔ blob. Les 138 tests passent.

Aucun changement apporté.


Generated by Claude Code

Comment on lines +2200 to +2206
if systemctl is-active --quiet ssh.service 2>/dev/null; then
systemctl reload ssh.service >/dev/null 2>&1 && return 0
systemctl restart ssh.service >/dev/null 2>&1 && return 0
return 1
fi
systemctl try-reload-or-restart ssh.service >/dev/null 2>&1 || true
return 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé en 18d9b26.

Précision sur le fond : try-reload-or-restart n'agit que sur une unité déjà démarrée et ne pouvait donc pas lancer ssh.service ni entrer en conflit avec ssh.socket. Mais la remarque vise juste sur la lisibilité — cette ligne contredisait frontalement le commentaire placé au-dessus, et il faut connaître la sémantique du préfixe try- pour comprendre qu'elle ne fait rien.

Le cas inactif est maintenant un retour explicite, avec le raisonnement écrit noir sur blanc : en activation par socket c'est ssh.socket qui tient le port (le démarrer serait effectivement une erreur), et sans activation par socket un service arrêté signifie qu'aucun sshd ne tourne — la configuration sera lue à son prochain démarrage. Dans les deux cas il n'y a rien à faire, et plus aucune commande n'est émise.


Generated by Claude Code

ssh_reload_config appelait « systemctl try-reload-or-restart ssh.service »
dans le cas inactif. C'est certes un no-op pour systemd sur une unité
arrêtée, mais la ligne contredisait le commentaire juste au-dessus et
laissait craindre un démarrage de ssh.service concurrent de ssh.socket
sur le même port.

Le cas inactif est désormais un retour explicite, commenté : en
activation par socket c'est ssh.socket qui tient le port, et sans
activation par socket un service arrêté signifie qu'aucun sshd ne tourne
— la configuration sera lue à son prochain démarrage.

Relevé en revue par Copilot sur la PR #3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbhHGjLvttKAauNJjPJMuq
@NicoBOD
NicoBOD merged commit d76b278 into main Aug 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants