Emulateur Terminal Web Local ttyd

Emulateur Terminal Web Local ttyd

Notes

Emulateur Terminal Web Local ttyd

ttyd est un émulateur de terminal Web et un shell interactif qui permet aux utilisateurs d’accéder à une interface de ligne de commande (CLI) Linux à partir d’un navigateur Web. Il utilise le protocole WebSocket pour assurer une connexion en temps réel entre le navigateur et le terminal Linux.

Cet outil léger et facile à utiliser peut être utilisé pour accéder à un système Linux distant ou pour fournir un accès à distance à un système Linux local. Il prend en charge plusieurs clients à la fois, permettant à plusieurs utilisateurs de se connecter à la même session. L’un des avantages de ttyd est qu’il ne nécessite l’installation d’aucun logiciel supplémentaire côté client, car il s’exécute entièrement dans un navigateur Web. Cela en fait un outil pratique pour l’administration et le dépannage du système à distance.

Les autres fonctionnalités et avantages associés à ttyd sont :

  • Prise en charge SSL basée sur OpenSSL / Mbed TLS
  • Prise en charge du transfert de fichiers ZMODEM (lrzsz)/ trzsz
  • Terminal complet avec prise en charge CJK et IME
  • Construit sur libuv et WebGL2 pour la vitesse
  • Prise en charge de la sortie d’image Sixel (img2sixel / lsix)
  • Exécutez n’importe quelle commande personnalisée avec des options
  • Prise en charge de l’authentification de base et de nombreuses autres options personnalisées
  • Logiciel Open Source pouvant être installé sur les systèmes Linux, FreeBSD/OpenBSD, OpenWrt, macOS et Windows. Il est également disponible sous forme d’outil de ligne de commande ou de conteneur Docker.

Proxmox (ThinkCentre M700)

Se connecter sur le serveur Proxmox (ThinkCentre M700) via SSH

1
ssh tin@192.168.0.216 -p 55216 -i /home/yann/.ssh/m700-ed25519

Créer le fichier install-terminal.sh et copier le contenu ci-joint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env bash
# Terminal web strictement local : ttyd + tmux derrière nginx (HTTPS, mot de passe, restriction au LAN)
# À lancer en root dans le conteneur Debian 13 dédié.
#
# Variables modifiables à l'appel, par exemple :
#   BIND_IP=192.168.0.206 WEB_USER=yannig bash install-terminal.sh
set -euo pipefail
# pct exec peut fournir un PATH réduit : on le fixe explicitement
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

FQDN="${FQDN:-terminal.home.arpa}"
LAN_CIDR="${LAN_CIDR:-192.168.0.0/24}"
BIND_IP="${BIND_IP:-$(hostname -I | awk '{print $1}')}"
TERM_USER="${TERM_USER:-termuser}"   # utilisateur Unix qui exécute le shell et les ssh
WEB_USER="${WEB_USER:-admin}"        # identifiant de l'authentification web
TTYD_PORT="${TTYD_PORT:-7681}"

[[ $EUID -eq 0 ]] || { echo "À lancer en root." >&2; exit 1; }
[[ -n "$BIND_IP" ]] || { echo "Impossible de déterminer BIND_IP." >&2; exit 1; }

if [[ -z "${WEB_PASS:-}" ]]; then
    read -rsp "Mot de passe web pour ${WEB_USER} : " WEB_PASS; echo
    read -rsp "Confirmer : " WEB_PASS2; echo
    [[ "$WEB_PASS" == "$WEB_PASS2" ]] || { echo "Les mots de passe diffèrent." >&2; exit 1; }
fi
[[ -n "$WEB_PASS" ]] || { echo "Mot de passe vide refusé." >&2; exit 1; }

echo "==> Paquets"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq tmux nginx openssl openssh-client apache2-utils curl ca-certificates

echo "==> ttyd"
if apt-cache show ttyd >/dev/null 2>&1; then
    apt-get install -y -qq ttyd
else
    # ttyd absent des dépôts Debian 13 : binaire officiel (GitHub), vérifié par somme SHA256
    TTYD_VERSION="${TTYD_VERSION:-1.7.7}"
    case "$(uname -m)" in
        x86_64)  TTYD_ASSET="ttyd.x86_64" ;;
        aarch64) TTYD_ASSET="ttyd.aarch64" ;;
        *) echo "Architecture non gérée : $(uname -m)" >&2; exit 1 ;;
    esac
    TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}"
    tmpd="$(mktemp -d)"
    curl -fsSL -o "$tmpd/$TTYD_ASSET" "$TTYD_URL/$TTYD_ASSET"
    curl -fsSL -o "$tmpd/SHA256SUMS" "$TTYD_URL/SHA256SUMS"
    ( cd "$tmpd" && grep " ${TTYD_ASSET}\$" SHA256SUMS | sha256sum -c - )
    install -m 755 "$tmpd/$TTYD_ASSET" /usr/local/bin/ttyd
    rm -rf "$tmpd"
fi
TTYD_BIN="$(command -v ttyd || true)"
[[ -x "$TTYD_BIN" ]] || { echo "ttyd introuvable après installation." >&2; exit 1; }
echo "ttyd : $TTYD_BIN"

echo "==> Utilisateur ${TERM_USER}"
if ! id "$TERM_USER" &>/dev/null; then
    useradd -m -s /bin/bash "$TERM_USER"
fi
HOME_DIR="$(getent passwd "$TERM_USER" | cut -d: -f6)"
install -d -m 700 -o "$TERM_USER" -g "$TERM_USER" "$HOME_DIR/.ssh"
install -d -m 755 -o "$TERM_USER" -g "$TERM_USER" "$HOME_DIR/scripts"

if [[ ! -f "$HOME_DIR/.ssh/id_ed25519" ]]; then
    sudo_run() { runuser -u "$TERM_USER" -- "$@"; }
    sudo_run ssh-keygen -t ed25519 -N "" -C "terminal-web@${FQDN}" -f "$HOME_DIR/.ssh/id_ed25519" -q
fi

if [[ ! -f "$HOME_DIR/.tmux.conf" ]]; then
    cat > "$HOME_DIR/.tmux.conf" <<'EOF'
set -g history-limit 50000
set -g mouse on
set -g default-terminal "tmux-256color"
set -g status-style "bg=colour236,fg=colour250"
EOF
    chown "$TERM_USER:$TERM_USER" "$HOME_DIR/.tmux.conf"
fi

echo "==> Service ttyd (écoute sur loopback uniquement)"
cat > /etc/systemd/system/ttyd.service <<EOF
[Unit]
Description=ttyd - terminal web (tmux)
After=network.target

[Service]
User=${TERM_USER}
WorkingDirectory=${HOME_DIR}
ExecStart=${TTYD_BIN} -i lo -p ${TTYD_PORT} -W -O tmux new-session -A -s scripts
Restart=on-failure
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=${HOME_DIR}

[Install]
WantedBy=multi-user.target
EOF

echo "==> Certificat TLS"
if [[ ! -f /etc/ssl/certs/terminal.crt ]]; then
    openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 825 \
        -keyout /etc/ssl/private/terminal.key -out /etc/ssl/certs/terminal.crt \
        -subj "/CN=${FQDN}" \
        -addext "subjectAltName=DNS:${FQDN},IP:${BIND_IP}" 2>/dev/null
    chmod 600 /etc/ssl/private/terminal.key
fi

echo "==> Authentification web"
printf '%s' "$WEB_PASS" | htpasswd -iBc /etc/nginx/.htpasswd_terminal "$WEB_USER" >/dev/null
chown root:www-data /etc/nginx/.htpasswd_terminal
chmod 640 /etc/nginx/.htpasswd_terminal

echo "==> Vhost nginx"
rm -f /etc/nginx/sites-enabled/default
cat > /etc/nginx/sites-available/terminal <<EOF
server {
    listen ${BIND_IP}:443 ssl;
    server_name ${FQDN};

    ssl_certificate     /etc/ssl/certs/terminal.crt;
    ssl_certificate_key /etc/ssl/private/terminal.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # Strictement local : réseau ${LAN_CIDR} ET mot de passe
    satisfy all;
    allow ${LAN_CIDR};
    deny  all;
    auth_basic "Terminal";
    auth_basic_user_file /etc/nginx/.htpasswd_terminal;

    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;

    location / {
        proxy_pass http://127.0.0.1:${TTYD_PORT};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_read_timeout 1d;
    }
}
EOF
ln -sf /etc/nginx/sites-available/terminal /etc/nginx/sites-enabled/terminal
nginx -t

echo "==> Démarrage"
systemctl daemon-reload
systemctl enable --now ttyd
systemctl enable nginx
systemctl restart nginx

cat <<EOF

=====================================================================
Terminal prêt : https://${FQDN}/   (identifiant : ${WEB_USER})

1) Certificat à importer dans ton navigateur / système :
   /etc/ssl/certs/terminal.crt
   (depuis le nœud Proxmox : pct pull <ID> /etc/ssl/certs/terminal.crt ./terminal.crt)

2) Clé publique à ajouter dans authorized_keys des serveurs distants :
$(cat "$HOME_DIR/.ssh/id_ed25519.pub")

3) DNS : ${FQDN} doit pointer vers ${BIND_IP}
=====================================================================
EOF

Le rendre exécutable

1
chmod +x install-terminal.ssh

Le script install-terminal.sh est prêt.

Conteneur LXC Debian 13

LXC en ligne de commande, depuis le shell du nœud Proxmox

1
2
3
pveam update
pveam available --section system | grep debian-13
pveam download local debian-13-standard_13.6-1_amd64.tar.zst   # nom exact vu juste au-dessus

Pour ton usage, un conteneur LXC suffit largement : il est plus léger qu’une VM (environ 256 à 512 Mo de RAM, 4 Go de disque) et démarre en quelques secondes. Prends une VM seulement si tu veux l’isolation maximale, puisque cette machine contiendra tes clés SSH vers tous tes serveurs. Dans les deux cas, ce sera séparé de ton serveur public, ce qui était le but.

1
2
3
4
5
6
7
8
9
10
11
pct create 210 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname terminal \
  --unprivileged 1 \
  --cores 1 --memory 512 --swap 256 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.0.206/24,gw=192.168.0.254 \
  --nameserver 192.168.0.254 \
  --features nesting=1 \
  --onboot 1
pct start 210
pct enter 210

À adapter :

  • 210 : un ID libre
  • local-lvm : le nom de ton stockage (visible avec pvesm status, probablement lié à ton LVM tiny-vg)
  • 192.168.0.206 : une IP libre de ton /24, de préférence réservée en dehors de la plage DHCP
  • gw / nameserver : l’IP de ton routeur ou de ton DNS local 192.168.0.254

Si tu préfères l’interface web : Create CT, décoche « Privileged container », choisis le template Debian 13, 1 cœur, 512 Mo, 4 Go de disque, IP statique.

Une fois que le conteneur est démarré et qu’il a un accès réseau. Vérifie ces trois points avant de lancer le script :

1
2
3
pct status 210                                  # doit afficher : status: running
pct exec 210 -- ip -4 addr show eth0            # l'IP 192.168.0.206 doit apparaître
pct exec 210 -- apt-get update                  # doit réussir (le script a besoin d'Internet pour installer les paquets)

Si apt-get update échoue, vérifie la passerelle (gw) et le DNS (--nameserver) que tu as donnés à la création du conteneur.

1. En cas d’erreur - Libérer l’ID 210 et vérifier le template (sur le nœud Proxmox)

1
2
pct stop 210 2>/dev/null; pct destroy 210 --purge
pveam list local | grep debian-13

Si pveam list local n’affiche pas debian-13-standard_13.6-1_amd64.tar.zst, le nom du template est différent chez toi : recopie celui qui est affiché dans ta commande.

2. Créer et démarrer

Lance la commande telle quelle avec les bons paramètres

1
2
3
4
5
6
7
8
9
10
11
12
pct create 210 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname terminal \
  --unprivileged 1 \
  --cores 1 --memory 512 --swap 256 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.0.206/24,gw=192.168.0.254 \
  --nameserver 192.168.0.254 \
  --features nesting=1 \
  --onboot 1
pct start 210
# Instruction suivante pour entrer dans conteneur
#pct enter 210

Résultat création

1
2
3
4
5
6
7
Detected container architecture: amd64
Creating SSH host key 'ssh_host_ed25519_key' - this may take some time ...
done: SHA256:uOzYgWwvC2/xTdhU+N1dGm4UU4fKHZXVC2tUO8M0fPQ root@terminal
Creating SSH host key 'ssh_host_rsa_key' - this may take some time ...
done: SHA256:31tn/rJPsYVOpuWJ1NQaNTa12P8eD8AMJjp3uz0oo84 root@terminal
Creating SSH host key 'ssh_host_ecdsa_key' - this may take some time ...
done: SHA256:JmQYlD8UuUcX4rxMYgSNmQp92saL6whte7yQJ8nQXds root@terminal

puis avant d’entrer dans le conteneur teste son réseau depuis le nœud :

1
2
3
pct exec 210 -- ping -c2 192.168.0.254
pct exec 210 -- ping -c2 1.1.1.1
pct exec 210 -- apt-get update

Installation terminal.home.arpa

Le fichier install-terminal.sh doit d’abord être copié sur le nœud (avec scp depuis ton poste, par exemple).

pct enter 210 ouvre le shell du conteneur, mais le script se pousse depuis le nœud. Ne fais donc pas pct enter avant, ou fais exit pour revenir au nœud, puis :

1
2
pct push 210 install-terminal.sh /root/install-terminal.sh
pct exec 210 -- env BIND_IP=192.168.0.206 WEB_USER=tin bash /root/install-terminal.sh

Le script install-terminal.sh demande le mot de passe web, installe tout, puis affiche à la fin la clé publique à copier et le chemin du certificat.

Ce qu’il fait :

  • installe ttyd, tmux et nginx
  • crée l’utilisateur termuser avec une clé SSH ed25519 dédiée et un dossier ~/scripts
  • lance ttyd sur la boucle locale uniquement, avec ta session tmux persistante scripts
  • génère un certificat pour terminal.home.arpa
  • configure nginx en HTTPS, avec mot de passe et accès limité à 192.168.0.0/24

À la fin, le script affiche la clé publique à ajouter aux authorized_keys de tes serveurs et le chemin du certificat à importer dans ton navigateur.

1
2
3
4
5
6
7
8
9
10
11
12
=====================================================================
Terminal prêt : https://terminal.home.arpa/   (identifiant : tin)

1) Certificat à importer dans ton navigateur / système :
   /etc/ssl/certs/terminal.crt
   (depuis le nœud Proxmox : pct pull <ID> /etc/ssl/certs/terminal.crt ./terminal.crt)

2) Clé publique à ajouter dans authorized_keys des serveurs distants :
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBvZ/7YyRGE+7gCqRg/NqK5hXs60Cd++OTZAL64etxG8 terminal-web@terminal.home.arpa

3) DNS : terminal.home.arpa doit pointer vers 192.168.0.206
=====================================================================

Le script peut être relancé sans problème s’il s’arrête en cours de route : il ne régénère ni les clés ni le certificat s’ils existent déjà.

Cet avertissement perl: warning: Setting locale failed. est sans gravité. Le conteneur n’a pas la locale que ton shell lui transmet (probablement fr_FR.UTF-8), donc perl se rabat sur la locale par défaut. Ça n’affecte pas l’installation.

Pour le faire disparaître, depuis le nœud :

1
pct exec 210 -- bash -c 'apt-get install -y -qq locales && sed -i "s/^# *fr_FR.UTF-8/fr_FR.UTF-8/" /etc/locale.gen && locale-gen && update-locale LANG=fr_FR.UTF-8'

Si tu préfères ne pas installer de locale, LC_ALL=C devant tes commandes pct exec suffit à le masquer.

Maintenant que le script est allé au bout, il reste ces étapes :

  1. Récupérer le certificat et l’importer dans ton navigateur ou ton système :

    1
    
     pct pull 210 /etc/ssl/certs/terminal.crt ./terminal.crt
    
  2. DNS : fais pointer terminal.home.arpa vers 192.168.0.206. Pour un premier test, une ligne 192.168.0.206 terminal.home.arpa dans le /etc/hosts de ton poste suffit.
  3. Clé publique : ajoute celle qu’affiche le script (ou cat /home/termuser/.ssh/id_ed25519.pub dans le conteneur) aux authorized_keys de tes serveurs distants.
  4. Test : ouvre https://terminal.home.arpa/, connecte-toi avec tin et le mot de passe choisi, puis lance un ssh vers un de tes serveurs.

Si la page ne s’affiche pas, vérifie que les services tournent :

1
pct exec 210 -- systemctl status ttyd nginx --no-pager

Avec un certificat valide homearpaCA.crt

1
2
3
pct push 210 homearpaCA.crt /usr/local/share/ca-certificates/homearpaCA.crt
# Mettre à jour le magasin de certificats
pct exec 210 -- bash -c 'update-ca-certificates'

Message

1
2
3
4
5
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.

Vérifier que le certificat a bien été ajouté en vérifiant qu’un .pem a été créé dans /etc/ssl/certs

1
pct exec 210 -- bash -c 'ls /etc/ssl/certs/ | grep -i homearpa'  # Renoie homearpaCA.pem

Installer les outils réseau

1
pct exec 210 -- bash -c 'apt install dnsutils -y'

Tester la résolution

1
pct exec 210 -- bash -c 'dig @192.168.0.205 terminal.home.arpa'

J’ai une erreur car le serveur DNS est 192.168.0.254, il faut le remplacer par 192.168.0.205 (technitium) puis redémarrage

1
2
3
4
5
pct exec 210 -- bash -c 'cat > /etc/resolv.conf << EOF
search tailscale.xoyize.xyz
nameserver 192.168.0.205
EOF'
pct exec 210 -- bash -c 'systemctl reboot'

Tester la résolution

1
pct exec 210 -- bash -c 'dig @192.168.0.205 terminal.home.arpa'

NOERROR, la résolution est OK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> terminal.home.arpa
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41805
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;terminal.home.arpa.		IN	A

;; ANSWER SECTION:
terminal.home.arpa.	3600	IN	A	192.168.0.205

;; Query time: 1 msec
;; SERVER: 192.168.0.205#53(192.168.0.205) (UDP)
;; WHEN: Wed Oct 21 19:46:40 UTC 2026
;; MSG SIZE  rcvd: 63

On supprime la ligne 192.168.0.206 terminal.home.arpa dans le /etc/hosts du poste PC1
On ajoute dans le serveur DNS Technitium une zone A 192.168.0.206 terminal.home.arpa On exporte le wilcard home.arpa

1
2
3
pct exec 210 -- bash -c 'mkdir -p /etc/ssl/home.arpa/'
pct push 210 /sharenfs/proxmox/partage/wildcard.home.arpa.crt /etc/ssl/home.arpa/wildcard.home.arpa.crt
pct push 210 /sharenfs/proxmox/partage/wildcard.home.arpa.key /etc/ssl/home.arpa/wildcard.home.arpa.key

Passer en mode terminal dans le conteneur

pct enter 210

Modifier la configuration nginx du terminal /etc/nginx/sites-enabled/terminal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server {
    listen 192.168.0.206:443 ssl;
    http2 on;
    server_name terminal.home.arpa;

#    ssl_certificate     /etc/ssl/certs/terminal.crt;
#    ssl_certificate_key /etc/ssl/private/terminal.key;
#
    ssl_certificate     /etc/ssl/home.arpa/wildcard.home.arpa.crt;
    ssl_certificate_key /etc/ssl/home.arpa/wildcard.home.arpa.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # Strictement local : réseau 192.168.0.0/24 ET mot de passe
    satisfy all;
    allow 192.168.0.0/24;
    deny  all;
    auth_basic "Terminal";
    auth_basic_user_file /etc/nginx/.htpasswd_terminal;

    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;

    location / {
        proxy_pass http://127.0.0.1:7681;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 1d;
    }
}

Le site https://terminal.home.arpa est accessible sans message d’erreur

SSH + Clés

A-Sur le poste appelant
Générer un jeu de clé

1
ssh-keygen -t ed25519 -o -a 100 -f ~/.ssh/terminal-210

Envoyer la clé publique sur le container lxc

1
ssh-copy-id -i ~/.ssh/terminal-210.pub termuser@192.168.0.206

B-Sur le distant
En mode root

1
2
3
4
echo 'Port 55206
PasswordAuthentication no' | tee /etc/ssh/sshd_config.d/10-user.conf
# Redémarrer sshd
systemctl restart sshd

C-Sur le poste appelant
Lancer la connexion SSH avec clé

1
ssh -p 55206 -i ~/.ssh/terminal-210 termuser@192.168.0.206

Modifications pas prises en compte ?

1
2
3
4
5
6
root@terminal:/# cat /etc/ssh/sshd_config.d/10-user.conf
Port 55206
PasswordAuthentication no
root@terminal:/# systemctl restart sshd
root@terminal:/# ss -lntp | grep ssh
LISTEN 0      4096               *:22              *:*    users:(("sshd",pid=992,fd=3),("systemd",pid=1,fd=45))

Le résultat indique que systemd garde le port 22 ouvert : ss montre systemd parmi les processus qui écoutent. Modifier sshd_config ne suffit donc pas si SSH est lancé par ssh.socket.

Dans le conteneur, configure le socket systemd pour écouter sur 55206 :

1
2
3
4
5
6
7
8
9
10
11
12
mkdir -p /etc/systemd/system/ssh.socket.d

cat > /etc/systemd/system/ssh.socket.d/listen.conf <<'EOF'
[Socket]
ListenStream=
ListenStream=55206
EOF

systemctl daemon-reload
systemctl stop ssh.service
systemctl restart ssh.socket
systemctl status ssh.socket --no-pager

Vérifie ensuite :

1
ss -lntp | grep -E 'ssh|55206'

Tu devrais voir *:55206

1
LISTEN 0      4096               *:55206            *:*    users:(("systemd",pid=1,fd=46))                      

Vérifie aussi la configuration effective de sshd :

1
sshd -T | grep '^port '

Si elle affiche encore port 22, cherche une autre directive Port qui surcharge le fichier :

1
grep -Rni '^[[:space:]]*Port' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/

Garde ta session actuelle ouverte et teste une nouvelle connexion avant de la fermer :

1
ssh -p 55206 utilisateur@IP_DU_CONTENEUR

Si tu as activé le pare-feu Proxmox ou celui du conteneur, autorise aussi le trafic TCP entrant sur 55206.


Utiliser csv existant

deux scripts qui adaptent ton inventaire CSV au terminal web

Poste PC1

Ton CSV se réutilise tel quel dans le conteneur, avec une différence : au lieu de tes ~20 clés, le terminal web utilise une seule clé dédiée (celle que le script d’installation a créée). Tes clés habituelles ne quittent donc pas ton poste. J’ai préparé deux scripts, testés sur un CSV d’exemple (la simulation et la génération de la config fonctionnent, mais je n’ai évidemment pas pu les essayer sur tes serveurs).

Sur ton poste : déposer la clé du terminal sur tes serveurs (deploy-key.sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env bash
# À lancer sur TON POSTE (qui a déjà accès à tous les serveurs avec ses clés habituelles).
# Ajoute la clé publique du terminal web dans ~/.ssh/authorized_keys de chaque serveur du CSV.
#
# Par défaut : SIMULATION (n'écrit rien). Ajouter --apply pour exécuter.
#
# Usage :
#   PUB=./terminal.pub ./deploy-key.sh                 # simulation, tous les serveurs
#   PUB=./terminal.pub ./deploy-key.sh --apply cwwk proxtin   # seulement ces alias
#
# Variables :
#   CSV       chemin du CSV (défaut : ~/terminalweb.csv)
#   PUB       clé publique à déposer (obligatoire)
#   FROM_LAN  IP du conteneur (ex. 192.168.0.206) : limite la clé à cette IP,
#             uniquement pour les serveurs en 192.168.0.x
set -euo pipefail

CSV="${CSV:-$HOME/.ssh/terminalweb.csv}"
PUB="${PUB:-$HOME/.ssh/terminal.pub}"
FROM_LAN="${FROM_LAN:-192.168.0.206}"
APPLY=0
TARGETS=()

for a in "$@"; do
    case "$a" in
        --apply) APPLY=1 ;;
        *) TARGETS+=("$a") ;;
    esac
done

[[ -f "$CSV" ]] || { echo "CSV introuvable : $CSV" >&2; exit 1; }
[[ -f "$PUB" ]] || { echo "Clé publique introuvable : $PUB" >&2; exit 1; }

PUBLINE="$(head -n1 "$PUB")"
[[ "$PUBLINE" == ssh-ed25519\ * ]] || { echo "Format de clé inattendu dans $PUB" >&2; exit 1; }
KEYBODY="$(awk '{print $2}' <<<"$PUBLINE")"

want() {
    [[ ${#TARGETS[@]} -eq 0 ]] && return 0
    local t
    for t in "${TARGETS[@]}"; do [[ "$t" == "$1" ]] && return 0; done
    return 1
}

ok=0; ko=0; skip=0
while IFS=';' read -r name user host port key _local _dist _ping _opt ipv _rest; do
    [[ -n "${name// }" ]] || continue
    want "$name" || continue

    if [[ -z "$key" ]]; then
        echo "[$name] pas de clé dans le CSV : ignoré"
        skip=$((skip+1)); continue
    fi

    opts="no-agent-forwarding,no-X11-forwarding,no-port-forwarding"
    if [[ -n "$FROM_LAN" && "$host" == 192.168.0.* ]]; then
        opts="from=\"$FROM_LAN\",$opts"
    fi
    line="$opts $PUBLINE"

    remote="umask 077; mkdir -p ~/.ssh; touch ~/.ssh/authorized_keys; grep -qF -- '$KEYBODY' ~/.ssh/authorized_keys || echo '$line' >> ~/.ssh/authorized_keys"
    famopt=(); [[ "$ipv" == 4 ]] && famopt=(-4); [[ "$ipv" == 6 ]] && famopt=(-6)

    if [[ $APPLY -eq 0 ]]; then
        echo "[$name] (simulation) ssh ${famopt[*]} -p $port -i ~/.ssh/$key $user@$host"
        continue
    fi

    if ssh "${famopt[@]}" -p "$port" -i "$HOME/.ssh/$key" -o IdentitiesOnly=yes \
           -o BatchMode=yes -o ConnectTimeout=10 "$user@$host" "$remote"; then
        echo "[$name] OK"; ok=$((ok+1))
    else
        echo "[$name] ÉCHEC"; ko=$((ko+1))
    fi
done < <(tail -n +2 "$CSV" | tr -d '\r')

if [[ $APPLY -eq 1 ]]; then
    echo "Terminé : $ok OK, $ko échec(s), $skip ignoré(s)"
else
    echo "Simulation terminée. Relancer avec --apply pour déposer la clé."
fi

Il lit ton CSV et utilise les clés et ports habituels pour ajouter la clé publique du terminal dans authorized_keys. Par défaut c’est une simulation qui n’écrit rien.

1
2
3
4
5
pct pull 210 /home/termuser/.ssh/id_ed25519.pub ./terminal.pub     # sur le nœud Proxmox, puis copie-le sur ton poste

PUB=./terminal.pub CSV=~/terminalweb.csv ./deploy-key.sh            # simulation
PUB=./terminal.pub CSV=~/terminalweb.csv ./deploy-key.sh --apply cwwk   # un seul serveur pour tester
PUB=./terminal.pub CSV=~/terminalweb.csv ./deploy-key.sh --apply        # tous

La clé est déposée avec no-port-forwarding,no-agent-forwarding,no-X11-forwarding. Avec FROM_LAN=192.168.0.206 devant la commande, elle est en plus limitée à l’IP du conteneur, mais seulement pour les serveurs en 192.168.0.x : pour les VPS distants, la connexion arrive depuis ton IP publique, donc cette restriction ne s’y applique pas.

Conteneur LXC

Dans le conteneur : générer ~/.ssh/config depuis le CSV (gen-ssh-config.sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env bash
# À lancer dans le conteneur terminal, en tant que termuser.
# Génère ~/.ssh/config à partir du CSV des connexions :
#   Alias;User;IPouDomain;Port_SSH;SSH_Key;Local;Distant;Ping;Opt;IP4_6
# Toutes les connexions utilisent la clé dédiée du terminal (colonne SSH_Key ignorée).
#
# Usage : gen-ssh-config.sh [terminalweb.csv]
set -euo pipefail

CSV="${1:-$HOME/terminalweb.csv}"
KEY="${KEY:-$HOME/.ssh/id_ed25519}"
OUT="${OUT:-$HOME/.ssh/config}"

[[ -f "$CSV" ]] || { echo "CSV introuvable : $CSV" >&2; exit 1; }
install -d -m 700 "$(dirname "$OUT")"

{
    echo "# Généré depuis $CSV le $(date -Is) - ne pas éditer à la main"
    echo
    tail -n +2 "$CSV" | tr -d '\r' | while IFS=';' read -r name user host port _key _local _dist _ping _opt ipv _rest; do
        [[ -n "${name// }" ]] || continue
        echo "Host $name"
        echo "    HostName $host"
        echo "    User $user"
        echo "    Port $port"
        case "$ipv" in
            4) echo "    AddressFamily inet" ;;
            5) echo "    AddressFamily inet6" ;;
        esac
        echo
    done
    cat <<EOF
Host *
    IdentityFile $KEY
    IdentitiesOnly yes
    StrictHostKeyChecking accept-new
    ServerAliveInterval 30
    ConnectTimeout 10
EOF
} > "$OUT.tmp"

chmod 600 "$OUT.tmp"
mv "$OUT.tmp" "$OUT"

# Compatibilité avec l'habitude "sshm alias"
if ! grep -q "alias sshm=" "$HOME/.bashrc" 2>/dev/null; then
    echo "alias sshm='ssh'" >> "$HOME/.bashrc"
fi

echo "OK : $(grep -c '^Host ' "$OUT") entrées dans $OUT (dont Host *)"

A exécuter sur proxmox Tiny en mode su

1
2
3
4
5
pct push 210 terminalweb.csv /home/termuser/terminalweb.csv
pct push 210 gen-ssh-config.sh /home/termuser/gen-ssh-config.sh
pct exec 210 -- chown termuser:termuser /home/termuser/terminalweb.csv /home/termuser/gen-ssh-config.sh
pct exec 210 -- runuser -u termuser -- bash /home/termuser/gen-ssh-config.sh
# Fin exécution -> OK : 14 entrées dans /home/termuser/.ssh/config (dont Host *)

Ensuite, dans le terminal web, ssh cwwk ouvre la session avec le bon utilisateur, port et famille d’adresse (IPv4 ou IPv6). L’alias sshm est aussi créé, donc sshm cwwk fonctionne comme sur ton poste. À relancer à chaque modification du CSV.

Points d’attention avec ton inventaire

  • skrime est en IPv6 uniquement : le conteneur doit avoir une adresse IPv6 (par exemple ip6=auto sur net0 si ton réseau en distribue), sinon cette connexion échouera.
  • ds723 n’a pas de clé dans le CSV : deploy-key.sh l’ignore, il faudra y ajouter la clé à la main.
  • acer, ds723, um890 pointent vers 82.64.153.201, qui semble être ton IP publique. Depuis ton réseau local, ça ne fonctionne que si ton routeur gère le NAT loopback. Sinon, utilise leur IP locale dans le conteneur.
  • Première connexion : la config contient StrictHostKeyChecking accept-new, donc les nouveaux serveurs sont acceptés automatiquement et une clé hôte modifiée reste refusée.
  • La complétion des alias avec Tab demande apt install bash-completion dans le conteneur.

Accès conteneur LXC en ligne de commande

Identifiant et mot de passe du terminal web
Le login (par exemple tin) et le mot de passe que tu as choisis dans le script ne servent qu’à ouvrir la page : c’est l’authentification nginx. Ce n’est pas un compte du système. Une fois sur la page, tu es directement dans un shell sous termuser, sans autre saisie, et termuser n’a pas de mot de passe ni de droits root.

Console d’administration du conteneur (root)

  • Depuis le nœud Proxmox : pct enter 210. Aucun mot de passe n’est demandé, puisque tu es déjà root sur le nœud. C’est le moyen le plus simple.
  • Depuis l’interface web de Proxmox (CT 210, onglet Console) : un mot de passe root est demandé. Comme tu as créé le conteneur sans --password, root n’en a pas, donc la connexion échouera. Tu peux en définir un depuis le nœud :
1
pct exec 210 -- passwd root

Prends un mot de passe long et unique, puisque ce compte contrôle tout le conteneur, y compris la clé SSH du terminal. Si tu n’utilises que pct enter, tu peux laisser root sans mot de passe, ce qui est même plus sûr.

Il n’y a pas de serveur SSH dans le conteneur, et je te conseille de ne pas en installer : l’administration passe par le nœud, et le terminal web reste le seul point d’entrée côté réseau.

Depuis le shell root du conteneur, tu passes en termuser sans mot de passe :

1
su - termuser

Le tiret charge son environnement complet (son HOME, son .bashrc, etc.). exit te ramène en root.

Autres variantes :

1
2
3
4
5
# Une seule commande en tant que termuser
runuser -l termuser -c 'ssh cwwk uptime'

# Depuis le nœud Proxmox, directement en termuser, sans passer par root dans le conteneur
pct exec 210 -- runuser -l termuser

Retrouver la session tmux du terminal web

La session scripts ouverte par la page web appartient à termuser. Pour la rejoindre depuis la console, en tant que termuser :

1
tmux attach -t scripts

Tu verras alors la même session que dans le navigateur, et vous pouvez écrire dedans à deux (pour la quitter sans la fermer : Ctrl+b puis d). Si tu fais cette commande en root, tmux ne trouvera rien : chaque utilisateur a ses propres sessions.

Pour éditer un fichier de termuser depuis root (par exemple connexions.csv), pense à remettre les droits ensuite, sinon termuser ne pourra plus le modifier :

1
chown termuser:termuser /home/termuser/connexions.csv

Utiliser tmux

Oui, c’est même le cas d’usage idéal. Le terminal web ouvre simplement ta session tmux scripts, donc tout ce que tu fais dans tmux fonctionne, et la session continue de tourner si tu fermes l’onglet ou si ton navigateur plante. Tu la retrouves à la prochaine ouverture de la page.

Raccourcis de base (préfixe Ctrl+b, puis la touche)

Action Touches
Nouvelle fenêtre Ctrl+b c
Fenêtre suivante / précédente Ctrl+b n / p
Aller à la fenêtre n° 3 Ctrl+b 3
Lister les fenêtres Ctrl+b w
Renommer la fenêtre Ctrl+b ,
Couper en deux (haut/bas) Ctrl+b "
Couper en deux (gauche/droite) Ctrl+b %
Changer de volet Ctrl+b + flèches
Fermer fenêtre ou volet exit ou Ctrl+b &

Avec la souris activée dans la config, tu peux aussi cliquer sur un volet pour le sélectionner et faire glisser les séparations.

Ouvrir une fenêtre par serveur

Dans le terminal web, ajoute cette fonction à ~/.bashrc :

1
2
3
4
cat >> ~/.bashrc <<'EOF'
sshw() { for h in "$@"; do tmux new-window -n "$h" "ssh $h; exec bash"; done; }
EOF
source ~/.bashrc

Ensuite sshw cwwk proxtin alpine01 ouvre trois fenêtres nommées, chacune connectée à son serveur. Le exec bash garde la fenêtre ouverte si la connexion tombe.

Lancer la même commande sur plusieurs serveurs

Ouvre un volet par serveur, puis active la synchronisation : Ctrl+b puis : et tape setw synchronize-panes on. Tout ce que tu tapes va dans tous les volets en même temps. Désactive avec setw synchronize-panes off. C’est pratique pour tes vérifications, mais fais attention aux commandes qui modifient quelque chose.

Deux détails propres au navigateur

  • Copier-coller : avec la souris activée dans tmux, une sélection à la souris va dans le tampon de tmux, pas dans le presse-papiers du navigateur. Maintiens Shift pendant la sélection pour utiliser la sélection du navigateur.
  • Plusieurs onglets du navigateur : ils s’attachent tous à la même session tmux et affichent la même fenêtre active. Pour que chaque onglet puisse naviguer indépendamment, remplace dans /etc/systemd/system/ttyd.service la fin de la commande tmux new-session -A -s scripts par tmux new-session -t scripts, puis systemctl daemon-reload && systemctl restart ttyd. Chaque onglet crée alors sa propre session, groupée avec scripts, qui partage les mêmes fenêtres mais garde sa fenêtre active. Les sessions groupées se ferment avec l’onglet, alors que les fenêtres restent dans scripts. Pour que scripts existe toujours, lance-la une fois en arrière-plan avec tmux new-session -d -s scripts.

Script tmux-grille.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash
# Grille 3x3 de sessions SSH dans tmux, lancée par ttyd (terminal web).
# Contrairement à la version du poste, la session n'est PAS détruite à chaque ouverture
# de la page : si elle existe déjà, on s'y rattache simplement (les ssh restent actifs).
#
# Reconstruire la grille :  tmux kill-session -t YannSessionD   puis recharger la page
#                           (ou : tmux-grille.sh --rebuild)
#
# Disposition (numéro de volet) :
# | 0 | 3 | 6 |
# | 1 | 4 | 7 |
# | 2 | 5 | 8 |

S="${SESSION:-YannSessionD}"
SSH_CMD="${SSH_CMD:-ssh}"          # pour un test à blanc : SSH_CMD=echo
W="$S:YannTermD"

# Alias (voir connexions.csv), dans l'ordre des volets 0 à 8
HOSTS=(cwwk yannir yannig xoyaz skrime yiak sxb proxmox proxtin)

if [[ "${1:-}" == "--rebuild" ]]; then
    tmux kill-session -t "$S" 2>/dev/null
fi

if ! tmux has-session -t "$S" 2>/dev/null; then
    tmux new-session -d -s "$S" -n YannTermD -x 240 -y 60

    # 3 colonnes
    tmux split-window -h -l 33% -t "$W.0"
    tmux split-window -h -l 50% -t "$W.0"
    # 3 lignes dans chaque colonne
    tmux split-window -v -l 33% -t "$W.0"
    tmux split-window -v -l 50% -t "$W.0"
    tmux split-window -v -l 33% -t "$W.3"
    tmux split-window -v -l 50% -t "$W.3"
    tmux split-window -v -l 33% -t "$W.6"
    tmux split-window -v -l 50% -t "$W.6"

    # Nom de l'alias affiché sur le bord de chaque volet
    tmux set-option -w -t "$W" pane-border-status top
    tmux set-option -w -t "$W" pane-border-format ' #{@host} '

    for i in "${!HOSTS[@]}"; do
        tmux set-option -p -t "$W.$i" @host "${HOSTS[$i]}"
        tmux send-keys -t "$W.$i" "$SSH_CMD ${HOSTS[$i]}" C-m
    done

    tmux select-pane -t "$W.0"
fi

exec tmux attach-session -t "$S"

Oui, il s’adapte facilement. J’ai écrit tmux-grille.sh, qui reprend ta grille 3x3 (une seule fenêtre, neuf volets) avec les mêmes serveurs dans le même ordre. Je n’ai pas pu lancer tmux ici : la syntaxe est vérifiée et j’ai suivi à la main la numérotation des volets, qui correspond à ton schéma, mais fais un test à blanc avant (voir plus bas).

Ce qui change par rapport à ton script

  • Pas de destruction de la session à chaque lancement. Ton script la supprime puis la recrée, ce qui, avec le terminal web, rouvrirait les 9 connexions SSH à chaque rechargement de page. Ici, si la session existe, on s’y rattache simplement. Pour reconstruire la grille : tmux-grille.sh --rebuild, ou tmux kill-session -t YannSessionD puis recharger la page.
  • Bash au lieu de sh : ton [[ ... ]] ne marche pas avec sh sur Debian (c’est dash).
  • Cibles explicites (-t YannSessionD:YannTermD.N) au lieu de selectp, ce qui évite les erreurs si une autre session tmux existe.
  • Nom de l’alias affiché en haut de chaque volet, pratique quand les prompts se ressemblent.
  • ssh alias au lieu de sshm alias, qui ne dépend pas de l’alias du .bashrc (c’est équivalent).

Installation

D’abord le fichier et un test à blanc (depuis le nœud, avec tmux-grille.sh copié dessus) :

1
2
3
pct push 210 tmux-grille.sh /home/termuser/tmux-grille.sh
pct exec 210 -- chown termuser:termuser /home/termuser/tmux-grille.sh
pct exec 210 -- chmod 755 /home/termuser/tmux-grille.sh

Puis tu peux tester sans connexion SSH depuis la console du conteneur :

1
pct exec 210 -- runuser -l termuser -c 'SSH_CMD=echo SESSION=TestGrille /home/termuser/tmux-grille.sh'

Il faut un vrai terminal pour l’attache :
utilise plutôt

1
pct enter 210

puis

1
su - termuser

puis

1
SSH_CMD=echo SESSION=TestGrille ./tmux-grille.sh

Tu dois voir les neuf noms d’alias apparaître dans la bonne disposition

Ctrl+b d pour sortir

puis

1
tmux kill-session -t TestGrille  # pour nettoyer.

Ensuite, fais lancer le script par ttyd à la place de la session simple. En root dans le conteneur (pct enter 210) :

1
2
sed -i 's|^ExecStart=.*|ExecStart=/usr/local/bin/ttyd -i lo -p 7681 -W -O -t fontSize=13 /home/termuser/tmux-grille.sh|' /etc/systemd/system/ttyd.service
systemctl daemon-reload && systemctl restart ttyd

ttyd n’est pas installé par apt

Si ton ttyd est installé par apt, remplace /usr/local/bin/ttyd par /usr/bin/ttyd (grep ExecStart /etc/systemd/system/ttyd.service pour vérifier avant). L’option fontSize=13 réduit la police pour que neuf volets tiennent à l’écran : ajuste selon la taille de ton navigateur.

À savoir

  • Au premier chargement, le script lance les neuf ssh en même temps : les nouveaux serveurs sont acceptés automatiquement (accept-new), mais un serveur dont la clé du terminal n’a pas été déposée affichera une demande de mot de passe dans son volet.
  • Le volet skrime demande une connectivité IPv6 dans le conteneur, comme évoqué plus haut.
  • Si tu ouvres la page dans deux onglets, tous deux affichent la même grille et partagent la même fenêtre active (c’est le comportement normal d’un tmux attach partagé).
  • Pour retrouver un shell libre en plus de la grille, ouvre une nouvelle fenêtre tmux avec Ctrl+b c

Remplacer Ctrl+b par Ctrl+space

Ni la touche Fn ni la touche Windows ne sont utilisables. Fn est gérée par le clavier lui-même (le navigateur ne la voit jamais), et la touche Windows (Super) est interceptée par ton bureau KDE avant d’atteindre Firefox. Il reste les combinaisons avec Ctrl et les touches F1 à F12.

Préfixe Ctrl+Espace

Firefox ne s’en sert pas. Sur un bureau KDE, il peut parfois entrer en conflit avec le changement de méthode de saisie, mais c’est rare. Dans le terminal web (ou su - termuser) :

1
2
3
4
5
6
cat >> ~/.tmux.conf <<'EOT'
set -g prefix C-Space
unbind C-b
bind C-Space send-prefix
EOT
tmux source-file ~/.tmux.conf

Ensuite : Ctrl+Espace, relâche, puis c pour une nouvelle fenêtre, 0 pour revenir à la grille, n / p pour passer à la suivante / précédente.

Si Ctrl+Espace ne répond pas, utilise Ctrl+a, le préfixe classique de GNU Screen. Remplace alors C-Space par C-a dans les trois lignes. Pour envoyer un vrai Ctrl+a au shell (début de ligne), tape Ctrl+a deux fois. Évite Ctrl+q, Ctrl+w, Ctrl+t ou Ctrl+n, que Firefox garde pour lui, et Alt+1 à Alt+9, qui changent d’onglet Firefox sous Linux.

Raccourcis directs avec les touches F (sans préfixe)

Pour les trois actions les plus courantes, tu peux te passer du préfixe. Ajoute ces lignes au même fichier :

1
2
3
bind -n F2 new-window
bind -n F8 previous-window
bind -n F9 next-window

Puis tmux source-file ~/.tmux.conf. Je choisis F2, F8 et F9 parce que Firefox ne s’en sert pas, contrairement à F3, F5, F6, F7, F10, F11 et F12. Tu perds en contrepartie ces trois touches dans les applications que tu lances dans tmux (par exemple dans htop).

À la souris

La souris est activée dans tmux : un clic sur le nom d’une fenêtre dans la barre du bas la sélectionne. Avec tmux 3.x, un clic droit sur cette barre ouvre normalement un menu qui permet aussi de créer une fenêtre.

Si tu avais déjà ajouté des lignes prefix C-a à ~/.tmux.conf lors d’un essai précédent, supprime-les avec nano ~/.tmux.conf pour éviter les doublons.


Compris : ttyd ne lancera plus la grille au démarrage, il ouvrira une session tmux normale. La grille devient une commande que tu lances quand tu en as besoin, et que tu peux fermer. Comme la page web est occupée par la grille, fais les premières étapes depuis le nœud Proxmox.

1. Arrêter la grille et rétablir le démarrage normal

1
2
pct exec 210 -- runuser -l termuser -c 'tmux kill-session -t YannSessionD'
pct exec 210 -- grep ExecStart /etc/systemd/system/ttyd.service

Note le chemin du binaire ttyd affiché par le grep (/usr/local/bin/ttyd ou /usr/bin/ttyd), puis :

1
2
3
pct exec 210 -- sed -i 's|^ExecStart=.*|ExecStart=/usr/local/bin/ttyd -i lo -p 7681 -W -O -t fontSize=14 tmux new-session -A -s scripts|' /etc/systemd/system/ttyd.service
pct exec 210 -- systemctl daemon-reload
pct exec 210 -- systemctl restart ttyd

Remplace /usr/local/bin/ttyd par le chemin que tu as relevé si besoin. En rechargeant la page, tu obtiens un terminal tmux vide (session scripts).

2. La grille à la demande

J’ai réécrit tmux-grille.sh : lancé depuis une fenêtre tmux, il ouvre une nouvelle fenêtre tmux nommée Grille, avec les neuf volets en 3x3 égaux, à côté de tes autres fenêtres. S’il existe déjà, il y retourne.

Je n’ai pas pu tester tmux ici : essaie-le chez toi avec SSH_CMD=echo avant de lancer les vraies connexions. Pousse-le dans le conteneur (même méthode que précédemment) et ajoute deux alias dans ~/.bashrc de termuser :

1
2
3
4
5
cat >> ~/.bashrc <<'EOT'
alias grille='~/tmux-grille.sh'
alias grille-stop='~/tmux-grille.sh --close'
EOT
source ~/.bashrc

Utilisation, dans le terminal web :

  • grille ouvre la grille
  • grille-stop la ferme avec ses neuf connexions SSH. Lance-le depuis une autre fenêtre tmux que la grille, par exemple une fenêtre ouverte avec F2 (voir ci-dessous)
  • tu peux aussi la fermer depuis la grille elle-même, avec F4 (voir ci-dessous)

3. Raccourcis sans préfixe (comme le préfixe pose problème)

1
2
3
4
5
6
7
cat >> ~/.tmux.conf <<'EOT'
bind -n F2 new-window
bind -n F4 confirm-before -p "Fermer la fenêtre ? (y/n)" kill-window
bind -n F8 previous-window
bind -n F9 next-window
EOT
tmux source-file ~/.tmux.conf

Résumé des commandes

  • grille ouvre la grille
  • grille-stop la ferme avec ses neuf connexions SSH. Lance-le depuis une autre fenêtre tmux que la grille, par exemple une fenêtre ouverte avec F2 (voir ci-dessous)
  • tu peux aussi la fermer depuis la grille elle-même, avec F4 (voir ci-dessous)
  • F2 ouvre une fenêtre vide
  • F8 Fenêtre précédente
  • F9 Fenêtre suivante
  • F4 ferme la fenêtre courante après confirmation.(supprimer les trois lignes du préfixe Ctrl+Espace de ~/.tmux.conf pour revenir à Ctrl+b)

Dans un terminal classique (TUI)

  • dialog / whiptail : menus, boutons Oui/Non, listes de choix, en shell pur. Pratique pour un petit lanceur de scripts.
  • fzf : menu de sélection fuzzy, très rapide à mettre en place (printf '%s\n' "sauvegarde" "mise à jour" | fzf), puis un case sur le choix.
  • gum (Charm) : boutons, menus, confirmations avec un rendu moderne, utilisable directement dans un script bash.
  • Textual (Python) ou Bubble Tea (Go) : si tu veux une vraie interface avec boutons cliquables à la souris, plusieurs panneaux, etc.
  • tmux / Midnight Commander / ranger : les raccourcis ou la barre de boutons (F1-F10 dans mc) servent aussi de « boutons » configurables.

Dans une fenêtre terminal graphique

  • Konsole (KDE) : possibilité d’ajouter des profils, des actions et une barre d’outils personnalisée, mais pas de vrais boutons de commande intégrés.
  • Tilix, Terminator : pas de boutons non plus, mais des raccourcis et des profils.
  • Zenity / yad / kdialog : tu lances une petite fenêtre avec des boutons depuis un script, qui exécute ensuite la commande.

Dans un terminal web

Si c’est pour ton terminal web local : une page HTML avec des boutons qui envoient une commande à ttyd, ou qui appellent un petit backend lançant le script en SSH, fonctionne bien. Avec ttyd, on peut aussi passer des arguments par URL pour lancer un script précis.

Cas simple pour démarrer

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
choix=$(whiptail --title "Actions" --menu "Que faire ?" 15 50 4 \
  1 "Mise à jour" \
  2 "Sauvegarde" \
  3 "Redémarrer le service" 3>&1 1>&2 2>&3)

case $choix in
  1) sudo apt update && sudo apt upgrade ;;
  2) ./sauvegarde.sh ;;
  3) sudo systemctl restart monservice ;;
esac