Featured image of post Mon bastion Warpgate : des clés SSH au simple alias
Retour d’expérience

Mon bastion Warpgate : des clés SSH au simple alias

Comment j’ai intégré Warpgate à mon homelab : clés SSH, Authentik, Terraform et alias générés par WarpgateSH, avec des exemples pour démarrer.

Dans mon terminal, accéder à un serveur tient en une seule ligne :

1
ssh dmz-nextcloud-01

Pas d’adresse IP à chercher, pas de serveur de rebond à saisir, aucune clé personnelle à copier sur la cible. Pourtant, sous la surface, une chaîne complète s’active : OpenSSH, Traefik, mon bastion Warpgate, Authentik et un fichier de configuration alimenté par WarpgateSH.

Dans le billet consacré à WarpgateSH, j’expliquais comment un script lourd à maintenir avait donné naissance à un outil dédié. Il me restait à présenter l’autre pan du système : le bastion lui-même, la gestion des clés SSH et son intégration dans mon système d’information — à l’échelle d’un homelab.

Je vais remonter cette commande étape par étape, depuis mon terminal jusqu’au serveur. Pour en reproduire le principe, je partirais simplement d’une VM et d’une machine de test.

Les exemples s’appuient sur mon infrastructure et le code de WarpgateSH au 19 septembre 2026 ; la version de Warpgate vérifiée le 10 septembre était la 0.27.5. Les valeurs example.net, alice et srv-demo sont fictives et réservées aux démonstrations.

Un bastion, pour quoi faire ?

Un bastion sert de point de passage contrôlé vers les ressources à administrer. Au lieu d’autoriser chaque poste à joindre directement chaque serveur, je centralise les flux sur un service unique. Il contrôle les identités, applique les droits et conserve des journaux d’accès.

Warpgate remplit cette fonction chez moi. Je lui déclare les cibles (les serveurs autorisés) et les rôles (qui a le droit d’y accéder), tout en conservant mon client SSH habituel.

À la différence d’un rebond classique via ProxyJump, la connexion SSH directe depuis mon poste s’arrête au bastion. Warpgate ouvre ensuite sa propre session vers la machine cible avec ses propres identifiants. Je n’ai donc jamais besoin d’y transférer mon agent SSH local.

Deux connexions distinctes : le poste s’authentifie auprès de Warpgate, puis Warpgate utilise sa clé pour joindre le serveur. Authentik valide l’identité humaine.

Vue logique : le réseau et le proxy de transport sont volontairement omis pour faire ressortir les deux authentifications.

Cette étanchéité constitue le point clé du montage : mon identité sur le bastion et le compte Linux de la cible sont deux entités totalement distinctes.

Clé privée, clé publique : qui garde quoi ?

Une paire de clés SSH repose sur deux éléments liés mathématiquement :

  • la clé privée, gardée secrète par le client qui prouve son identité ;
  • la clé publique, déposée sur le serveur qui vérifie cette identité.

Lors de la connexion, le client signe le défi d’authentification avec sa clé privée. Le serveur valide cette signature via la clé publique correspondante, sans que la clé privée ne transite sur le réseau.

Sur un serveur OpenSSH standard, les clés autorisées sont listées dans ~/.ssh/authorized_keys au niveau du compte utilisateur Linux (alice, root, etc.).

Pour illustrer la génération manuelle, je prends cette commande :

1
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_bastion -C "poste-perso"

Cette commande produit deux fichiers (décrits dans le manuel ssh-keygen) :

1
2
~/.ssh/id_ed25519_bastion      ← Fichier privé local
~/.ssh/id_ed25519_bastion.pub  ← Clé publique à déployer côté serveur

Chez moi, les clés passent par l’agent Bitwarden

Je ne définis pas de mot de passe directement sur le fichier de clé : je délègue sa protection au coffre fort de Bitwarden et son utilisation à son agent SSH. OpenSSH sollicite l’agent pour signer l’authentification sans lire de fichier privé sur mon disque. Le guide SSH Bitwarden détaille sa mise en œuvre.

Dans mon inventaire Ansible, la connexion pointe vers le socket de l’agent :

1
-o IdentityAgent=~/.bitwarden-ssh-agent.sock

Une précision importante : stocker une clé non chiffrée dans Bitwarden protège sa copie dans le coffre, mais ne sécurise pas le fichier initial resté sur le disque s’il n’a pas été supprimé.

Pour accéder au bastion via SSH, j’enregistre ma clé publique dans mon profil Warpgate. Mon agent Bitwarden produit la signature ; le bastion la vérifie avec ma clé publique. Une fois authentifié, Warpgate initie la connexion vers la cible finale en utilisant sa propre clé privée. J’autorise donc la clé publique du bastion dans le fichier authorized_keys du serveur, et non ma clé personnelle.

Ce flux par clé sert principalement aux comptes de service. Pour mes sessions d’administration courantes, je valide mon identité dans le navigateur via Authentik.

Ed25519, RSA, ECDSA : trois algorithmes distincts

Ces appellations désignent des algorithmes utilisés pour les signatures d’authentification SSH :

TypeCe que j’en retiens
Ed25519C’est mon choix pour les systèmes récents et le type de clé déployé par mon socle Ansible.
RSAJe le réserverais aux besoins de rétrocompatibilité, avec des signatures SHA-2 comme l’expliquent les notes OpenSSH 8.8.
ECDSAVariante basée sur des courbes elliptiques standardisées, disponible nativement dans OpenSSH.
ed25519-sk / ecdsa-skDéclinaisons exploitant une clé de sécurité matérielle (FIDO2/YubiKey).

Pour une nouvelle installation, j’écarte les algorithmes obsolètes comme DSA.

Et la clé du serveur ?

SSH contrôle également l’authenticité de l’hôte distant pour éviter les interceptions.

  • authorized_keys contrôle qui peut entrer ;
  • known_hosts enregistre les serveurs de confiance.

Le poste client valide l’empreinte du bastion, puis le bastion valide celle du serveur cible. À la première connexion, je recommande de comparer l’empreinte proposée avec celle relevée depuis la console de la VM. Pour afficher cette dernière sur la cible, j’utiliserais cette commande :

1
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

Note d’usage : ne pas comparer cette empreinte revient à accepter aveuglément l’identité du serveur. C’est un point d’amélioration que je prévois d’automatiser entièrement dans mon lab.

Déploiement dans le homelab

Mon bastion tourne sur une VM Proxmox dédiée (dmz-bastion-01), provisionnée via Terraform (2 vCPU, 2 Gio de RAM, 30 Gio de stockage).

Ansible déploie Docker et configure l’environnement. Voici un extrait de mon fichier Docker Compose :

1
2
3
4
5
6
7
8
9
services:
  warpgate:
    container_name: warpgate
    restart: unless-stopped
    ports:
      - "2222:2222"
      - "8888:8888"
    volumes:
      - ./data:/data

Le port 2222 reçoit les connexions SSH et le port 8888 héberge l’interface d’administration HTTPS. Le dossier ./data conserve l’état persistant de l’application.

Traefik en frontal : un nom, deux parcours

Traefik écoute sur bastion.int.homeblack.fr et redirige les requêtes vers le bastion :

1
2
3
4
5
6
7
8
Portail HTTPS (SSO)
Navigateur → bastion.int.homeblack.fr:443
           → Traefik → Warpgate:8888 (HTTPS)

Session SSH
OpenSSH    → bastion.int.homeblack.fr:2222
           → Traefik (Relais TCP) → Warpgate:2222
                                  → Serveur cible:22

Traefik achemine le trafic vers Warpgate ; Warpgate gère les accès aux cibles. Traefik ignore tout des machines finales et se contente de relayer les flux.

Reverse proxy HTTPS pour le portail

Le portail web passe par un reverse proxy standard. L’inventaire Ansible définit le routage :

1
2
3
- name: "bastion"
  host: "bastion.int.homeblack.fr"
  upstream_url: "https://10.60.0.17:8888"

Les liens d’approbation et le retour Authentik passent par le navigateur, via l’accès HTTPS du portail.

Relais TCP dédié pour le flux SSH

SSH ne traitant pas le protocole HTTP, Traefik utilise un routeur TCP. La configuration statique déclare d’abord le point d’entrée :

1
2
3
entryPoints:
  warpgate-ssh:
    address: ":2222"

La configuration dynamique (chargée via le provider file) définit ensuite le routage :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tcp:
  routers:
    warpgate-ssh:
      entryPoints:
        - warpgate-ssh
      rule: "HostSNI(`*`)"
      service: warpgate-ssh

  services:
    warpgate-ssh:
      loadBalancer:
        proxyProtocol:
          version: 2
        servers:
          - address: "10.60.0.17:2222"

La règle HostSNI(`*`) accepte tous les flux TCP bruts sans interception TLS (détails dans la doc Traefik TCP). Le chiffrement SSH reste géré de bout en bout entre le client et Warpgate.

Conservation des IP sources avec PROXY Protocol

Lorsqu’un proxy transfère une connexion, le serveur final ne voit que l’IP du proxy.

L’activation du PROXY protocol v2 permet à Traefik de transmettre l’IP d’origine du client dans un en-tête ajouté au début du flux TCP. Côté Warpgate (ansible/inventory/group_vars/bastion.yml) :

1
2
3
4
5
warpgate_ssh_external_host: "bastion.int.homeblack.fr"
warpgate_ssh_external_port: 2222
warpgate_ssh_proxy_protocol: true
warpgate_ssh_allowed_sources:
  - "10.40.0.28/32" # IP de Traefik autorisée

Segmentation réseau

Dans mon architecture, Traefik réside sur le réseau TRUST et Warpgate sur le réseau DMZ. Le flux traverse le pare-feu inter-vlan. Pour une nouvelle installation, je privilégierais un reverse proxy dans le même segment réseau que le service, afin d’éviter ce passage entre zones.

J’intègre aussi le filtrage des accès directs depuis les postes à la mise en place du bastion, avec l’objectif de faire passer les sessions d’administration par Warpgate.

Comment les serveurs valident le bastion

Une fois authentifié sur Warpgate, ce dernier initie la connexion SSH vers le serveur cible. La cible doit donc connaître la clé publique du bastion.

Mon rôle Ansible ssh-keys déploie automatiquement la clé publique du bastion (warpgate-target) dans le fichier authorized_keys du compte système distant :

1
2
3
4
5
6
- name: Deploy authorized_keys
  ansible.posix.authorized_key:
    user: "{{ ssh_user }}"
    state: present
    key: "{{ item }}"
  loop: "{{ ssh_public_keys }}"

Côté Warpgate, les paramètres de la cible (IP, port, utilisateur Linux) sont injectés via Terraform.

Pour me connecter à Nextcloud, la commande sous-jacente générée équivaut à :

1
ssh -p 2222 -l 'gregory.narcin:dmz-nextcloud-01' bastion.int.homeblack.fr

Le paramètre -l transmet l’instruction : « Connecte l’utilisateur Warpgate gregory.narcin à la cible dmz-nextcloud-01 ». Après validation des droits, Warpgate ouvre la session SSH vers la VM avec son propre compte d’administration.

SSO avec Authentik et accès automatisés

J’utilise Authentik comme fournisseur d’identité via OIDC, OpenID Connect, pour l’authentification unique (SSO).

Pour une connexion humaine, l’option In-browser auth affiche une URL et un code de validation dans le terminal. J’ouvre le lien, compare le code affiché et approuve la demande après authentification auprès d’Authentik. OpenSSH attend cette validation via la méthode keyboard-interactive.

Pour raccorder Warpgate à Authentik, je configure une application cliente et son URL de redirection, par exemple https://bastion.example.net/@warpgate/api/sso/return. Je sélectionne ensuite In-browser auth dans la politique d’authentification SSH de mon compte, comme l’explique le guide SSO Warpgate.

Je gère la correspondance des rôles avec Ansible :

1
2
3
warpgate_oidc_roles_claim: "groups"
warpgate_oidc_role_mappings:
  warpgate-operators: "ops"

Les appartenances aux groupes Authentik sont ainsi converties en rôles d’accès dans Warpgate.

Pour mes automatisations Ansible et mes pipelines CI/CD, je garde des comptes de service avec des clés SSH dédiées, comme svc-ansible : je ne veux pas qu’un traitement attende une validation dans mon navigateur.

Déclaration des cibles avec Terraform

Je déclare mes cibles Warpgate avec Terraform pour éviter de les ajouter manuellement dans l’interface :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
resource "warpgate_target" "ssh" {
  for_each = local.ssh_targets
  name     = each.key

  ssh_options {
    host                 = each.value.ip_address
    port                 = 22
    username             = try(each.value.ssh_username, "root")
    allow_insecure_algos = false
    public_key_auth {}
  }
}

Mon parcours de création suit cet ordre : VM Terraform → Cible Warpgate Terraform → Socle Ansible → Synchronisation du client.

J’exclus le bastion lui-même de cette liste et conserve un accès d’urgence indépendant pour pouvoir intervenir si Warpgate tombe en panne.

Automatisation des alias avec WarpgateSH

WarpgateSH interroge l’API de Warpgate pour générer automatiquement la configuration SSH locale et les alias associés.

Après avoir installé la CLI (guide de démarrage), je configure un profil ; voici les commandes avec les valeurs de démonstration :

1
2
3
4
5
6
warpgatesh profile add lab https://bastion.example.net
warpgatesh profile default lab
warpgatesh profile ssh-auth lab in-browser
warpgatesh agent install
warpgatesh sync
warpgatesh ls

L’outil ajoute un appel d’inclusion dans ~/.ssh/config :

1
Include ~/.ssh/warpgatesh/config

Le fichier généré (~/.ssh/warpgatesh/config) contient les entrées de ce type :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Profile lab

Host srv-demo srv-demo.lab
  HostName "bastion.example.net"
  Port 2222
  User "alice:srv-demo"
  UserKnownHostsFile ~/.ssh/warpgatesh/known_hosts/lab
  StrictHostKeyChecking yes
  PreferredAuthentications keyboard-interactive
  KbdInteractiveAuthentication yes
  PasswordAuthentication no
  PubkeyAuthentication no

Explication des directives :

  • Host : définit l’alias court et l’alias qualifié.
  • HostName : pointe vers le bastion.
  • User : passe le sélecteur utilisateur:cible.
  • PreferredAuthentications : impose l’authentification interactive pour déclencher la validation SSO par navigateur.

Je peux ensuite utiliser la commande raccourcie :

1
ssh srv-demo

Diagnostic et retours d’expérience

J’ai rencontré des incohérences entre mes alias SSH : certains forçaient le mode keyboard-interactive, tandis que d’autres autorisaient aussi les clés publiques.

Pour comprendre ce qu’OpenSSH utilise réellement, je consulte sa configuration effective avec ssh -G, sans ouvrir de connexion :

1
ssh -G srv-demo | awk '/^(hostname|port|user|preferredauthentications|pubkeyauthentication) /'

Je peux ainsi repérer les conflits avec d’éventuelles directives Host * globales.

Pour valider un montage similaire, je vérifierais quatre points :

  1. L’accès aux cibles autorisées.
  2. Le refus d’accès à une cible non attribuée.
  3. Le compte Linux obtenu à l’arrivée, avec whoami.
  4. La présence de la session dans les traces d’audit de Warpgate.

Bilan

Avec ce montage, je centralise mes accès SSH sans intervenir sur chaque serveur à l’arrivée ou au départ d’un utilisateur. Je garde mes accès automatisés séparés grâce aux comptes de service dédiés.

En contrepartie, mon bastion devient un composant critique de l’infrastructure. Je dois sauvegarder régulièrement sa clé privée d’administration et ses données d’audit, et en restreindre strictement les accès. C’est bien ce qui se passe en coulisses, et même un peu plus… mais si je raconte tout ici, ce billet va finir avec un sommaire en trois volumes.

Selon moi, le plus simple est d’avancer par étapes : je commencerais par valider le flux SSH sur une seule cible, puis j’intégrerais le SSO OIDC avant d’automatiser la gestion des cibles et des alias sur mon poste.

Généré avec Hugo
Thème Stack conçu par Jimmy