---
title: OpenSSH
description: "Clés, config et tunnels SSH : local, distant, dynamique, ProxyJump et persistance."
category: linux
kind: tool
tags: [ssh, tunneling, port-forwarding, proxyjump, keys]
level: intermediate
updated: 2026-11-11
order: 2
aliases: ["ssh", "ssh tunnel", "port forwarding", "socks ssh"]
---

## Connexion et clés

```bash
ssh user@10.10.10.10
ssh -p 2222 user@host
ssh -i ~/.ssh/id_ed25519 user@host
ssh -J jump user@internal

# Clé ed25519 avec passphrase
ssh-keygen -t ed25519 -a 100 -C "operator@laptop"

# Déployer la clé publique
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
ssh-add -t 8h ~/.ssh/id_ed25519
ssh-add -l
```

```bash
# Permissions attendues
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519.pub ~/.ssh/authorized_keys
```

```ssh-config
# ~/.ssh/config
Host jump
  HostName 10.10.10.10
  User operator
  Port 2222
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 30
  ServerAliveCountMax 3
  ExitOnForwardFailure yes
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m

Host internal
  HostName 10.10.20.10
  User operator
  ProxyJump jump
```

```bash
ssh -G jump | grep -iE "hostname|user|port|forward|proxy"
ssh-keyscan -H 10.10.10.10 >> ~/.ssh/known_hosts
ssh-keygen -R 10.10.10.10
```

> [!TIP]
> Attention à la casse : `ssh -p`, mais `scp -P` et `sftp -P`. Une erreur de casse donne un « unknown option » ou un port différent de celui attendu.

## Comprendre les tunnels

> [!TIP]
> Pour un pivot complet avec chisel, ligolo-ng et proxychains, voir [Pivoting](/sheets/pivoting/). Ici, on se concentre sur les primitives natives d'OpenSSH.

| Option | Sens | Bind sur |
| --- | --- | --- |
| `-L` | Local → distant | Machine cliente (toi) |
| `-R` | Distant → local | Serveur SSH (bastion) |
| `-D` | Proxy SOCKS dynamique | Machine cliente |
| `-J` | Rebond SSH | Chaîne de bastions |
| `-W` | Forward stdio | Utilisé avec ProxyCommand |

```text
# -L : le service distant devient accessible localement
[toi:8080] ──SSH──▶ [jump] ──TCP──▶ internal.corp:80
ssh -N -L 8080:internal.corp:80 user@jump
```

> [!WARNING]
> Avec `-L`, le nom `internal.corp` est résolu **par le serveur SSH**, pas par ta machine. `localhost` désigne le localhost du bastion, pas le tien.

## Tunnels locaux `-L`

```bash
# Accéder à un service interne via un bastion
ssh -N -L 8080:internal.corp:80 user@jump

# Plusieurs tunnels dans la même session
ssh -N -L 8080:internal.corp:80 -L 5432:db.internal:5432 user@jump

# Arrière-plan, sans shell, échec si le port local est occupé
ssh -fNT -o ExitOnForwardFailure=yes -L 8080:internal:80 user@jump

# Rendre le port accessible aux autres machines de ton réseau
ssh -g -N -L 8080:internal:80 user@jump

# Forward d'un socket Unix (Docker, par exemple)
ssh -N -L /tmp/docker.sock:/var/run/docker.sock user@jump
```

```bash
# Vérifier que le port écoute
ss -tlnp | grep 8080
curl -s http://127.0.0.1:8080/
```

## Tunnels distants `-R`

```bash
# Exposer un service local sur le loopback du bastion
ssh -N -R 9001:127.0.0.1:9001 user@jump

# Exposer sur toutes les interfaces du bastion (GatewayPorts requis)
ssh -N -R 0.0.0.0:9001:127.0.0.1:9001 user@jump

# Laisser le serveur choisir le port (affiché en verbose)
ssh -v -N -R 0:127.0.0.1:9001 user@jump

# Callback pour un reverse shell (voir aussi /sheets/reverse-shells/)
ssh -N -R 4444:127.0.0.1:4444 user@jump
```

```bash
# Sur le serveur SSH, pour autoriser un bind non-loopback
# /etc/ssh/sshd_config.d/forwarding.conf
GatewayPorts clientspecified
```

> [!TIP]
> Par défaut, un `-R` n'écoute que sur `127.0.0.1` du serveur. Pour qu'une autre machine du réseau du bastion atteigne le tunnel, il faut `GatewayPorts clientspecified` et un bind explicite `0.0.0.0:9001`.

## Proxy SOCKS `-D`

```bash
# SOCKS5 local sur 127.0.0.1:1080
ssh -N -D 1080 user@jump

# Exposer le SOCKS aux autres interfaces
ssh -N -D 0.0.0.0:1080 user@jump

# Tester avec curl (DNS résolu côté proxy)
curl --socks5-hostname 127.0.0.1:1080 http://internal.corp/

# Scanner le réseau interne via proxychains
proxychains4 nmap -sT -Pn -p 22,80,443,445 10.10.20.0/24
```

```ini
# /etc/proxychains4.conf
[ProxyList]
socks5 127.0.0.1 1080
```

> [!WARNING]
> Un SOCKS SSH ne transporte que du **TCP**. Pas d'ICMP, pas de scan SYN (`-sS`), pas d'UDP. Utilise `nmap -sT`, et pour l'UDP passe par un VPN ou un tunnel TUN (`ssh -w`).

## Rebonds et ProxyJump

```bash
# Un ou plusieurs bastions
ssh -J user@jump user@10.10.20.10
ssh -J user@jump1,user@jump2 user@target

# Équivalent explicite
ssh -o ProxyJump=user@jump user@target

# ProxyCommand : SSH dans SSH
ssh -o ProxyCommand='ssh -W %h:%p user@jump' user@target

# Passer par un proxy HTTP CONNECT
ssh -o ProxyCommand='ncat --proxy proxy.corp:3128 --proxy-type http %h %p' user@target
ssh -o ProxyCommand='socat - PROXY:proxy.corp:%h:%p,proxyport=3128' user@target
```

```bash
# Tunnel à travers la cible finale
# Ici, c'est la cible qui doit joindre internal:80
ssh -J jump -L 8080:internal:80 user@target
```

> [!TIP]
> Si tu veux que ce soit le **bastion** qui joigne le service, fais le tunnel sur le bastion : `ssh -L 8080:internal:80 user@jump`. `-J` ne change pas qui résout la destination de `-L`.

## Transferts de fichiers

```bash
# scp / sftp (port en majuscule)
scp -P 2222 fichier user@host:/tmp/
scp -r dossier user@host:/tmp/
sftp -P 2222 user@host

# rsync à travers SSH
rsync -avz -e 'ssh -p 2222' dossier/ user@host:/tmp/dossier/

# Sans scp : tar dans un pipe
tar czf - dossier | ssh user@host 'tar xzf - -C /tmp'

# Lire un fichier distant
ssh user@host 'cat /etc/passwd' > passwd.local

# Monter un répertoire distant
sshfs user@host:/remote /mnt/remote -o reconnect
```

> [!NOTE]
> `scp` utilise SFTP par défaut sur les versions récentes d'OpenSSH. Sur un serveur ancien, force l'ancien protocole avec `scp -O`.

## Multiplexage et persistance

```bash
# Une seule connexion TCP réutilisée pour plusieurs sessions
ssh -M -S ~/.ssh/cm-jump -fN -L 8080:internal:80 user@jump

ssh -S ~/.ssh/cm-jump -O check user@jump
ssh -S ~/.ssh/cm-jump -O forward -L 9090:internal:90 user@jump
ssh -S ~/.ssh/cm-jump -O cancel -L 9090:internal:90 user@jump
ssh -S ~/.ssh/cm-jump -O exit user@jump
```

```bash
# Reconnexion automatique d'un tunnel
autossh -M 0 -fNT \
  -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -L 8080:internal:80 user@jump
```

```ini
# ~/.config/systemd/user/tunnel-8080.service
[Unit]
Description=SSH tunnel 8080 -> internal:80
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/ssh -NT -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -L 8080:internal:80 user@jump
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
```

```bash
systemctl --user daemon-reload
systemctl --user enable --now tunnel-8080.service
loginctl enable-linger "$USER"
```

## Agent et restrictions de clé

```bash
eval "$(ssh-agent -s)"
ssh-add -t 8h ~/.ssh/id_ed25519
ssh-add -l

# Forward de l'agent : pratique, mais risqué sur un hôte non fiable
ssh -A user@jump
```

```text
# ~/.ssh/authorized_keys
# Clé de backup : uniquement cette commande, depuis ce réseau
restrict,command="/usr/local/bin/backup",from="10.10.10.0/24" ssh-ed25519 AAAA...

# Clé de tunnel : pas de shell, uniquement le forwarding
restrict,pty,port-forwarding ssh-ed25519 AAAA...
```

| Option sshd | Effet |
| --- | --- |
| `AllowTcpForwarding yes` | Autorise `-L` et `-R` |
| `AllowTcpForwarding local` | Autorise seulement `-L` |
| `AllowTcpForwarding remote` | Autorise seulement `-R` |
| `PermitOpen host:port` | Restreint les destinations |
| `PermitListen` | Restreint les ports d'écoute distants |
| `GatewayPorts no` | `-R` limité au loopback |
| `AllowAgentForwarding no` | Coupe `-A` |
| `PermitTunnel no` | Coupe `ssh -w` (TUN/TAP) |

> [!WARNING]
> `ssh -A` place ton agent sur l'hôte distant : root sur cet hôte peut utiliser tes clés pour rebondir. Préfère `ProxyJump`, qui ne transfère jamais l'agent.

## Durcissement serveur

```bash
# /etc/ssh/sshd_config.d/hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding local
PermitOpen git.internal:22 db.internal:5432
ClientAliveInterval 60
ClientAliveCountMax 3
```

```bash
sshd -t
systemctl reload ssh       # Debian / Ubuntu
systemctl reload sshd      # RHEL / Fedora
sshd -T | grep -iE "forward|gateway|permitopen"
```

> [!WARNING]
> `AllowTcpForwarding no` ou un `PermitOpen` trop strict casse les tunnels. Sur un bastion de pentest, vérifie la policy avant de durcir, et documente l'écart entre le besoin opérationnel et la config de production.

## Dépannage

```bash
ssh -vvv user@host
ssh -G host | grep -iE "forward|proxy|user|port"
ss -tlnp
ss -tnp
journalctl -u ssh -f        # ou -u sshd
```

| Erreur | Piste |
| --- | --- |
| `Permission denied (publickey)` | Clé absente, permissions, `ssh-add -l`, SELinux (`restorecon -R ~/.ssh`) |
| `Host key verification failed` | `ssh-keygen -R host` après vérification |
| `channel open failed: administratively prohibited` | `AllowTcpForwarding` / `PermitOpen` côté serveur |
| `remote port forwarding failed` | Port déjà pris sur le serveur, ou `GatewayPorts` |
| `bind: Address already in use` | Port local déjà utilisé |
| Connexion qui tombe | `ServerAliveInterval`, `ClientAliveInterval`, MTU/réseau |

```bash
# Scripts non interactifs
ssh -o BatchMode=yes -o ConnectTimeout=10 user@host 'uptime'

# Accepter une nouvelle clé d'hôte sans la vérifier (lab uniquement)
ssh -o StrictHostKeyChecking=accept-new user@host

# Vérifier les algorithmes négociés
ssh -Q cipher
ssh -Q kex
```

> [!WARNING]
> Un tunnel SSH contourne la segmentation réseau et peut exposer un service interne à un réseau moins contrôlé. Utilise-le uniquement dans le scope autorisé, ferme les forwards après usage et surveille les connexions sortantes inhabituelles côté défense.
