Bastion SSH avec OpenSSH : certificats d’une heure, ProxyJump et journaux, testé pas à pas

Avant d’installer une plateforme complète, une question : que vous apporterait un bastion avec les seuls outils d’OpenSSH ? Il est possible d’obtenir l’essentiel (un point d’entrée unique, des accès qui expirent, une identité tracée dans les journaux) sans logiciel supplémentaire. Voici la configuration, les six essais que nous avons exécutés et ce que ce montage ne sait pas faire.

Ce qu’il faut retenir

  • Les certificats SSH remplacent les clés statiques. Une autorité de certification (AC) signe la clé publique de l’administrateur pour une durée donnée (ici une heure), avec un identifiant libre (par exemple son e-mail et le numéro du ticket) et une liste de « principals » autorisés.
  • Le bastion n’est qu’un rebond. Le compte de saut n’a pas de shell et ne peut ouvrir qu’une connexion vers les cibles prévues (PermitOpen) ; l’administrateur arrive sur la cible avec ssh -J (ProxyJump) sans avoir de session sur le bastion.
  • Les journaux nomment la personne et la demande : l’identifiant du certificat et son numéro de série apparaissent sur le bastion comme sur la cible.
  • Ce que le montage ne fait pas : il n’enregistre pas les sessions, ne gère pas de coffre de mots de passe, et la sécurité repose entièrement sur la clé de l’AC.
  • Quatre essais sur six sont des refus voulus (certificat expiré, mauvais principal, shell sur le bastion, port non prévu) : on teste ce qui doit échouer autant que ce qui doit marcher.

Pourquoi un bastion, rappel

Notre panorama des bastions explique pourquoi concentrer les accès d’administration : un seul point à durcir, à surveiller et à auditer, et la possibilité de couper un accès en un endroit. Les plateformes sont nombreuses ; nous montrons ici le minimum viable, utile pour comprendre ce que les produits automatisent et pour un petit parc.

Schéma : le poste de l'administrateur se connecte à la cible en passant par le bastion, avec un certificat d'une heure signé par une autorité de certification
L’autorité de certification signe ; elle ne se connecte jamais. Le bastion et la cible lui font confiance.

Le principe : une AC, un certificat par demande

Avec ssh-keygen on crée une paire de clés d’AC, puis on signe la clé publique d’un administrateur. Le certificat porte : l’identifiant (-I), le numéro de série (-z), la liste des principals (-n) et la période de validité (-V). Les serveurs reçoivent seulement la clé publique de l’AC (TrustedUserCAKeys) et un fichier qui dit quel principal peut ouvrir quel compte (AuthorizedPrincipalsFile) : il n’y a plus de authorized_keys à gérer machine par machine, et plus rien à retirer quand un certificat expire.

# Sur un poste d'administration sécurisé (jamais sur le bastion) :
ssh-keygen -t ed25519 -f ca -C "ca-ssh-prod"        # la clé de l'AC : à protéger au maximum

# Certificat de l'administratrice, valable 1 h, traçable :
ssh-keygen -s ca -I "alice@exemple.fr#ticket-4521" -n admin-sys -V +1h -z 1001 alice.pub

# Certificats d'hôte (pour que le client reconnaisse bastion et cible sans « confiance au premier contact ») :
ssh-keygen -s ca -I bastion-prod -h -n bastion.exemple.fr hostkey_bastion.pub

Le bastion : un rebond sans shell

Le bastion accepte seulement les certificats de l’AC, avec un principal précis, pour un compte de saut sans shell. Il autorise le transfert de port vers les cibles prévues, et rien d’autre :

# /etc/ssh/sshd_config (bastion)
TrustedUserCAKeys /etc/ssh/ca.pub
AuthorizedPrincipalsFile /etc/ssh/principals/%u     # contient « admin-sys »
AllowUsers saut
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding yes
PermitOpen cible1.exemple.fr:22 cible2.exemple.fr:22  # les seules destinations possibles
ForceCommand /usr/sbin/nologin
LogLevel VERBOSE                                     # journalise l'identifiant du certificat

Côté cible, la configuration est la même sans PermitOpen ni ForceCommand, avec AllowUsers sur le ou les comptes d’administration, et un fichier de principals qui nomme le principal autorisé pour chacun.

Le poste de l’administrateur utilise un fichier de configuration client, avec ProxyJump :

Host bastion
  HostName bastion.exemple.fr
  User saut
Host cible1
  HostName cible1.exemple.fr
  User admin
  ProxyJump bastion
Host *
  IdentityFile ~/.ssh/alice
  CertificateFile ~/.ssh/alice-cert.pub
  IdentitiesOnly yes
  StrictHostKeyChecking yes

Les six essais

Nous avons monté ce schéma sur un banc d’essai jetable (OpenSSH 10.0, bastion et cible sur la boucle locale, une AC créée pour l’occasion) et exécuté six scénarios. Le script d’essai est reproduit en fin d’article.

Tableau des six essais : un certificat valide réussit, un certificat expiré, un mauvais principal, un shell sur le bastion, un rebond vers un port non prévu et une clé sans certificat échouent
Résultats observés le 9 octobre 2026 sur le banc d’essai.
#EssaiRésultat observé
1Certificat valide, via le bastionConnexion réussie : connecté sur … en tant que root
2Certificat expiré (valable il y a 10 à 5 minutes)Permission denied (publickey) ; journal du bastion : Certificate invalid: expired
3Mauvais principal (« compta »)Permission denied (publickey) : aucun principal autorisé
4Shell sur le bastionThis account is currently not available (code 1)
5Rebond vers un port non prévuadministratively prohibited: open failed
6Clé sans certificatPermission denied (publickey)

Deux pièges rencontrés, utiles à connaître. D’abord, StrictModes : sshd refuse le fichier des principals si un dossier parent est inscriptible par d’autres (message « bad ownership or modes for directory /tmp ») ; rangez-le sous /etc/ssh. Ensuite, des échecs répétés déclenchent une pénalité par adresse source (« penalty: failed authentication ») : un test qui échoue en boucle bloque les suivants quelques secondes.

Ce que disent les journaux

Avec LogLevel VERBOSE, chaque authentification par certificat laisse une trace qui contient l’identifiant et le numéro de série, sur le bastion comme sur la cible :

bastion : Accepted publickey for nobody from 127.0.0.1 port 46536 ssh2: ED25519-CERT SHA256:kVgK… ID alice@exemple.fr#ticket-4521 (serial 1001) CA ED25519 SHA256:0fzi…
cible   : Accepted publickey for root from 127.0.0.1 port 54718 ssh2: ED25519-CERT SHA256:kVgK… ID alice@exemple.fr#ticket-4521 (serial 1001) CA ED25519 SHA256:0fzi…
refus   : Failed publickey for … ID alice-expire (serial 1002) · Certificate invalid: expired

Conséquence pratique : en partant d’une ligne de journal sur la cible, on retrouve qui a demandé l’accès et pour quel ticket, sans corréler des adresses IP. C’est exactement le type d’événement qu’une règle de détection peut surveiller : connexion avec un identifiant de certificat inattendu, ou hors des horaires prévus. Dans un SIEM comme Wazuh, un décodeur sur « ID … (serial …) » suffit.

Ce que le montage ne fait pas

  • Pas d’enregistrement de session. On sait qui s’est connecté et quand, pas ce qui a été tapé. Pour cela il faut un outil dédié ou l’audit du système (auditd) côté cible.
  • La clé de l’AC est le bien le plus précieux. Qui la possède peut se fabriquer un accès. Conservez-la hors ligne ou dans un module matériel, et signez depuis un poste d’administration durci.
  • Une demande de certificat doit passer par un processus. Ici la signature est manuelle ; en production, un petit service signe après validation d’un ticket et d’une authentification forte (MFA). C’est précisément ce que les produits automatisent.
  • Révocation. Un certificat d’une heure se passe de révocation dans la plupart des cas. Pour couper plus vite, sshd sait refuser des clés ou certificats listés dans un fichier ou une liste de révocation (RevokedKeys, format KRL produit par ssh-keygen -k).
  • Le bastion reste une cible. Mise à jour, aucune autre exposition, journaux envoyés hors de la machine, et pare-feu qui n’autorise que le bastion à joindre le port 22 des cibles.

Quand passer à un produit

Dès qu’il faut enregistrer les sessions, gérer plusieurs protocoles (RDP, bases de données, Kubernetes) ou déléguer des demandes d’accès, un outil dédié devient plus raisonnable. État des lieux relevé le 9 octobre 2026 sur les dépôts et la documentation officiels :

OutilLicenceÀ savoir
TeleportAGPL-3.0 pour le code ; l’édition « Community » distribuée en binaire est réservée aux organisations de moins de 100 salariés et moins de 10 M$ de chiffre d’affaires (licence modifiée, d’après le fichier LICENSE-community du dépôt)Certificats à expiration automatique pour tous les protocoles, MFA et SSO, compatible OpenSSH. Dernière version stable relevée : 18.10.0 (9 juillet 2026).
WarpgateApache-2.0Bastion SSH, HTTPS, Kubernetes, MySQL, PostgreSQL, RDP et VNC ; enregistrement des sessions, TOTP et OpenID Connect, binaire unique. Version relevée : 0.29.2 (7 octobre 2026). Nous ne l’avons pas testé pour cet article.
JumpServerGPL-3.0Plateforme de gestion des accès privilégiés (SSH, RDP, Kubernetes, bases). Version relevée : 5.0.0 (17 septembre 2026).
HashiCorp BoundaryBusiness Source License (donneur de licence : IBM) pour les versions 0.14 et suivantesUtilisation interne autorisée ; usage concurrent de l’offre payante interdit. Le dépôt GitHub est marqué « archivé » (lecture seule) à la date de notre relevé ; nous n’avons trouvé aucune annonce officielle et ne présumons pas de la suite : vérifiez avant de bâtir dessus.

Ces constats sont à jour au 9 octobre 2026 et peuvent évoluer ; les licences surtout méritent d’être relues avant tout déploiement.

Ce que nous avons vérifié

Les six essais ont été réellement exécutés avec OpenSSH 10.0p2 (Debian 13) sur la boucle locale, avec une autorité de certification créée pour l’occasion ; les sorties et les lignes de journal citées en sont extraites (empreintes tronquées ; le compte de saut du banc d’essai était nobody, les exemples de configuration l’appellent « saut »). Le script banc-essai-bastion-ssh.sh, reproduit en fin d’article, refait l’ensemble et supprime tout à la fin. Les options de configuration ont été relues dans les pages de manuel d’OpenSSH. Les licences et versions des autres outils viennent des README, fichiers de licence et API publics de leurs dépôts GitHub ; nous n’avons exécuté aucun de ces produits.

$ sudo ./banc-essai-bastion-ssh.sh
--- 1. certificat valide via le bastion
    connecté sur adm-agent-claude en tant que root       code=0
--- 2. certificat expiré                    Permission denied (publickey)
--- 3. mauvais principal                    Permission denied (publickey)
--- 4. shell direct sur le bastion          This account is currently not available.
--- 5. rebond vers un port non autorisé     administratively prohibited: open failed
--- 6. clé sans certificat                  Permission denied (publickey)

Transparence : la recherche, la rédaction et, lorsqu’il y en a, les tests de cet article ont été réalisés avec l’aide d’un assistant IA (Claude) ; l’éditeur du site relit et valide chaque article avant publication. Le montage minimal et les mises en garde sont notre synthèse ; ils ne remplacent pas une revue de votre architecture.

Le script du banc d’essai

Le script complet qui crée l’autorité de certification, monte le bastion et la cible sur la boucle locale, exécute les six essais puis supprime tout. À lancer en root sur une machine de test (il écrit temporairement dans /etc/ssh/lab-principals).

#!/bin/bash
# Banc d'essai : bastion SSH avec certificats de courte durée (OpenSSH 10, loopback, jetable). À lancer en root dans un dossier vide.
# Crée une AC de laboratoire, un « bastion » (127.0.0.1:2201) et une « cible » (127.0.0.1:2202), exécute 6 tests, puis arrête tout.
set -u
L=$(mktemp -d /tmp/banc-ssh.XXXXXX); cd "$L" || exit 1
P=/etc/ssh/lab-principals            # dossier lisible par le compte de saut (StrictModes refuse /tmp)
mkdir -p $P && chmod 755 $P && echo admin-sys | tee $P/bastion $P/cible >/dev/null && chmod 644 $P/*
for k in ca user user-exp user-bad hb ht; do ssh-keygen -q -t ed25519 -N '' -f $k; done
ssh-keygen -q -s ca -I bastion-lab -h -n 127.0.0.1 hb.pub; ssh-keygen -q -s ca -I cible-lab -h -n 127.0.0.1 ht.pub
ssh-keygen -q -s ca -I "alice@exemple.fr#ticket-4521" -n admin-sys -V +1h  -z 1001 user.pub
ssh-keygen -q -s ca -I "alice-expire"                 -n admin-sys -V -10m:-5m -z 1002 user-exp.pub
ssh-keygen -q -s ca -I "mauvais-principal"            -n compta    -V +1h  -z 1003 user-bad.pub
common="PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM no
LogLevel VERBOSE
TrustedUserCAKeys $L/ca.pub"
cat > sshd_bastion <<C
Port 2201
ListenAddress 127.0.0.1
HostKey $L/hb
HostCertificate $L/hb-cert.pub
PidFile $L/bastion.pid
AuthorizedPrincipalsFile $P/bastion
AllowUsers nobody
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding yes
PermitOpen 127.0.0.1:2202
ForceCommand /usr/sbin/nologin
$common
C
cat > sshd_cible <<C
Port 2202
ListenAddress 127.0.0.1
HostKey $L/ht
HostCertificate $L/ht-cert.pub
PidFile $L/cible.pid
AuthorizedPrincipalsFile $P/cible
AllowUsers root
PermitRootLogin prohibit-password
$common
C
/usr/sbin/sshd -f sshd_bastion -E $L/bastion.log && /usr/sbin/sshd -f sshd_cible -E $L/cible.log
echo "@cert-authority * $(cat ca.pub)" > known_hosts
mkcfg(){ cat > cfg-$1 <<C
Host bastion
  HostName 127.0.0.1
  Port 2201
  User nobody
Host cible
  HostName 127.0.0.1
  Port 2202
  User root
  ProxyJump bastion
Host cible22
  HostName 127.0.0.1
  Port 22
  User root
  ProxyJump bastion
Host *
  UserKnownHostsFile $L/known_hosts
  StrictHostKeyChecking yes
  IdentitiesOnly yes
  BatchMode yes
  ConnectTimeout 5
  IdentityFile $L/$2
  CertificateFile $3
C
}
mkcfg ok user $L/user-cert.pub; mkcfg exp user-exp $L/user-exp-cert.pub; mkcfg bad user-bad $L/user-bad-cert.pub; mkcfg nocert user /dev/null
t(){ echo "--- $1"; shift; "$@" 2>&1 | head -4 | sed 's/^/    /'; echo "    code=${PIPESTATUS[0]}"; }
t "1. certificat valide via le bastion"  ssh -F cfg-ok cible 'echo "connecté sur $(hostname) en tant que $(id -un)"'
t "2. certificat expiré"                 ssh -F cfg-exp cible 'echo NE-DOIT-PAS-APPARAITRE'
t "3. mauvais principal"                 ssh -F cfg-bad cible 'echo NE-DOIT-PAS-APPARAITRE'
t "4. shell direct sur le bastion"       ssh -F cfg-ok bastion 'id'
t "5. rebond vers un port non autorisé"  ssh -F cfg-ok cible22 'echo NE-DOIT-PAS-APPARAITRE'
t "6. clé sans certificat"               ssh -F cfg-nocert cible 'echo NE-DOIT-PAS-APPARAITRE'
echo "--- journal de la cible"; grep -E "Accepted publickey" cible.log | cut -c1-200
kill "$(cat bastion.pid)" "$(cat cible.pid)"; rm -rf "$P" "$L"

Sources